编译器模棱两可调用错误 - 匿名方法和具有Func<>或Action的方法组

104

我有一个场景,想要使用方法组语法来调用一个函数,而非使用匿名方法(或lambda语法)。

该函数有两个重载,一个接受Action类型参数,另一个接受Func<string>类型参数。

我可以愉快地使用匿名方法(或lambda语法)调用这两个重载,但如果使用方法组语法,则会出现编译器错误“模棱两可的调用”。我可以通过显式转换为ActionFunc<string>绕过此问题,但我认为这不应该是必需的。

谁能解释一下为什么需要显式类型转换?

以下是代码示例:

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

C# 7.3 更新

根据0xcde在2019年3月20日下方的评论(我发表这个问题九年后!),由于改进的重载选择机制,此代码现在可以编译通过 C# 7.3。


我已经尝试了你的代码,但是我遇到了一个额外的编译时错误:'void test.ClassWithSimpleMethods.DoNothing()' 的返回类型不正确(该错误位于第25行,也就是歧义错误所在的位置)。 - Matt Ellen
@Matt:我也看到了那个错误。 我在帖子中所引用的错误是VS在你尝试进行完整编译之前突出显示的编译问题。 - Richard Ev
1
顺便说一下,这是一个很好的问题。我喜欢任何能让我深入规格说明的东西 :) - Jon Skeet
1
请注意,如果您使用C# 7.3(<LangVersion>7.3</LangVersion>)或更高版本,您的示例代码将编译通过,这要归功于改进的重载候选项 - 0xced
4个回答

100

首先,让我说Jon的答案是正确的。这是规范中最棘手的部分之一,所以很高兴Jon能够毫不犹豫地深入其中。

第二点,让我说这行代码:

存在从方法组到兼容的委托类型的隐式转换

(强调添加)非常令人误解和不幸。我将与Mads交谈,希望他能在此处删除“兼容”一词。

这种误导和不幸之所以会发生,是因为它看起来像是在引用规范的第15.2节“委托兼容性”。第15.2节描述了方法和委托类型之间的兼容关系,但这是一个关于方法组和委托类型可转换性的问题,它们是不同的。

既然我们搞明白了这一点,我们就可以走过规范的第6.6节,看看我们得到了什么。

为了进行重载决策,我们需要首先确定哪些重载是适用的候选项。如果所有参数都可以隐式转换为形式参数类型,则候选项是适用的。考虑您程序的这个简化版本:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

让我们逐行分析。

从方法组到兼容的委托类型存在隐式转换。

我已经讨论过这里的“兼容”一词不太恰当了。现在我们正在想,当对Y(X)进行重载决策时,方法组X是否会转换为D1?它是否会转换为D2?

给定委托类型D和被分类为方法组的表达式E,如果E包含至少一个对于D的参数类型和修饰符构造成的参数列表是适用的[...]的方法,则从E到D存在隐式转换,如下所述。

到目前为止还好。X可能包含一个适用于D1或D2参数列表的方法。

从方法组E到委托类型D的编译时应用转换在以下描述。

这行真的没说什么有趣的东西。

请注意,存在从E到D的隐式转换并不能保证转换的编译时应用不会出错。

这句话很有意思。它意味着存在隐式转换,但是这些转换可能会出现错误!这是C#的一个奇怪规则。为了离题一下,这里有一个例子:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

在表达式树中,增量操作是不合法的。但是lambda表达式仍然可以转换为表达式树类型,即使如果进行转换,这是一个错误!原则是,我们可能希望稍后更改可以放入表达式树中的规则; 更改这些规则不应更改类型系统规则。我们希望强制您使程序现在明确无歧义,以便将来当我们改变表达式树规则以使其更好时,我们不会在重载分辨率中引入破坏性变化。

无论如何,这是另一种奇怪规则的例子。转换可以存在于重载分辨率的目的中,但实际使用时却可能出错。不过事实上,在这里我们并不完全处于这种情况。

继续:

  

选择与形式 E(A)对应的单个方法M [... ]参数列表A是表达式列表,每个表达式都分类为相应参数D中的变量[...]

好的。因此,我们针对D1在X上进行重载分辨率。 D1的形式参数列表为空,因此我们在X()和joy上进行重载分辨率,找到了一个可行的方法“string X()”。类似地,D2的形式参数列表也为空。再次,我们发现“string X()”是在此处也可行的方法。

原则是,确定方法组可转换性需要使用重载分辨率从方法组中选择一个方法,而重载分辨率不考虑返回类型

  

如果算法[...]产生错误,则会发生编译时错误。否则,算法将产生一个与D具有相同参数数目的单个最佳方法M,并且认为转换存在。

方法组X中只有一个方法,因此它必须是最佳的。我们已成功证明了从X到D1和从X到D2的转换存在

现在,这一行是否相关?

  

所选方法M必须与委托类型D兼容,否则会发生编译时错误。

实际上,在此程序中没有用处。我们从来没达到过激活这一行的地步。因为请记住,我们在尝试对Y(X)进行重载分辨率。我们有两个候选项Y(D1)和Y(D2)。两者都适用。哪一个更好?规范中没有描述这两种可能转换之间的优越性

现在,有人肯定会说,一个有效的转换比产生错误的转换要好。那么,在这种情况下,实际上就是在说,重载解析确实考虑了返回类型,而这正是我们想要避免的。那么问题就是哪个原则更好:(1)保持重载解析不考虑返回类型这个不变量,还是(2)尝试选择我们知道可以工作的转换而不是我们知道不能工作的转换?

这是一种判断。对于lambda,我们在这些类型的转换中确实考虑返回类型,在第7.4.3.3节中:

E是一个匿名函数,T1和T2是具有相同参数列表的委托类型或表达式树类型,在该参数列表的上下文中存在E的推断返回类型X,并且以下情况之一成立:

  • T1具有返回类型Y1,T2具有返回类型Y2,并且从X到Y1的转换优于从X到Y2的转换

  • T1具有返回类型Y,T2返回void

很不幸,方法组转换和lambda转换在这方面是不一致的。但是,我可以接受它。

无论如何,我们没有“优先级”规则来确定哪个转换更好,X到D1还是X到D2。因此,在Y(X)的解析中会出现歧义错误。


8
非常感谢您的回答和(希望)规范改进。个人认为,在方法组转换中,考虑返回类型以使行为更加直观是合理的,但我也明白这会以一致性为代价。 (当方法组中只有一个方法时,将泛型类型推断应用于方法组转换时也可以这样说,正如我们之前讨论过的那样。) - Jon Skeet

36

编辑:我想我明白了。

正如 zinglon 所说,这是因为从 GetStringAction 存在隐式转换,即使编译时应用会失败。以下是第 6.6 节的引言,有一些强调(我加的):

如果存在一个方法组(§7.1)与兼容的委托类型之间的隐式转换(§6.1)。给定一个委托类型 D 和一个被分类为方法组的表达式 E,如果 E 包含至少一个方法,该方法能够以其正常形式 (§7.4.3.1) 应用于由 D 的参数类型和修饰符构建的参数列表中,则 E 到 D 的隐式转换存在,并按照以下方式描述。

现在,我被第一句话搞糊涂了 - 它谈到了转换为兼容的委托类型。对于 GetString 方法组中的任何方法来说,Action 都不是兼容的委托,但 GetString() 方法在由 D 的参数类型和修饰符构建的参数列表中以其正常形式适用。请注意,这并没有涉及 D 的返回类型。这就是为什么它混淆了...因为在应用转换时,它只会检查 GetString() 的委托兼容性,而不是检查其存在性。

我认为短暂地排除重载,看看这种转换的存在性和适用性之间的差异如何体现是有益的。以下是一个简短但完整的示例:

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Main中的两个方法调用表达式都不能编译,但错误消息不同。这是IntMethod(GetString)的错误消息:

Test.cs(12,9): error CS1502: “Program.IntMethod(int)”的最佳重载方法匹配具有一些无效参数

换句话说,规范的7.4.3.1节找不到任何适用的函数成员。

现在这里是ActionMethod(GetString)的错误:

Test.cs(13,22): error CS0407: 'string Program.GetString()' 具有错误的返回类型

这一次它已经找到了要调用的方法,但未能执行所需的转换。不幸的是,我找不到规范的哪个部分执行了最后的检查 - 看起来它可能在7.5.5.1中,但我无法确定确切的位置。


旧答案已被删除,除了这部分 - 因为我希望Eric可以阐明此问题的“原因”...

还在寻找...同时,如果我们说“Eric Lippert”三次,你认为我们会得到一次访问(从而得到答案)吗?


@Daniel:不是的 - 这些表达式是方法组表达式,只有在从方法组到相关参数类型存在隐式转换时,重载方法才应被视为适用。请参阅规范的7.4.3.1节。 - Jon Skeet
@zinglon:它并没有说转换存在。它说从方法组到兼容的委托类型的转换。委托兼容性在15.2中描述,而GetString()Action不兼容-因此在我看来不存在转换。 - Jon Skeet
@Jon:虽然在前言中确实有提到,但是直到说明转换被认为存在之后,算法中才提到§15.2。这对我来说似乎有点模糊不清。 - zinglon
我是什么,kibo?我会看一下,但考虑到迄今为止这里的冗词数量,这可能有点费劲。 - Eric Lippert
将来如果您想引起我的注意,您可以直接通过电子邮件与我联系(Jon),或通过我的博客上的联系链接给我留言。 - Eric Lippert
显示剩余10条评论

1

ClassWithDelegateMethods中使用Func<string>Action<string>(显然与ActionFunc<string>非常不同)可以消除歧义。

歧义也会发生在ActionFunc<int>之间。

我还遇到了这个歧义错误:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

进一步的实验表明,当单独传递一个方法组时,在确定使用哪个重载时完全忽略了返回类型。
class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 

0

FuncAction的重载类似(因为它们都是委托)

string Function() // Func<string>
{
}

void Function() // Action
{
}

如果你注意到了,编译器不知道该调用哪一个函数,因为它们只有返回类型不同。


我认为事实并非完全如此——因为你无法将Func<string>转换为Action……也无法将仅由返回字符串的方法组成的方法组转换为Action - Jon Skeet
2
你无法将没有参数且返回 string 的委托强制转换为 Action。我不明白为什么会存在歧义。 - jason
3
@dtb: 是的,移除重载可以解决这个问题,但这并不能真正解释为什么会有这个问题。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接