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

Ric*_*ett 101 c# delegates

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

该函数有两个重载,一个需要一个Action,另一个需要一个Func<string>.

我可以愉快地使用匿名方法(或lambda语法)调用两个重载,但如果我使用方法组语法,则会获得Ambiguous调用的编译器错误.我可以明确的解决方法铸造到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() { }
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 97

首先,我要说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);
    }
}
Run Code Online (Sandbox Code Playgroud)

让我们一行一行地完成它.

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

我已经讨论过"兼容"这个词在这里是多么令人遗憾.继续.我们想知道在Y(X)上进行重载解析时,方法组X是否转换为D1?它会转换为D2吗?

给定委托类型D和被分类为方法组的表达式E,如果E包含至少一个适用于通过使用参数构造的参数列表的方法,则存在从E到D的隐式转换. 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++));
Run Code Online (Sandbox Code Playgroud)

增量操作在表达式树中是非法的.但是,lambda仍然可以转换为表达式树类型,即使使用了转换,也是一个错误!这里的原则是我们可能希望稍后改变表达式树中的规则; 更改这些规则不应更改类型系统规则.我们要强迫你做你的程序明确的现在,这样当我们改变表达式树的规则在将来使他们更好,我们不引进重载重大更改.

无论如何,这是这种奇怪规则的另一个例子.出于重载解析的目的,可以存在转换,但实际使用时会出错.事实上,这并不是我们所处的情况.

继续:

选择单个方法M对应于形式E(A)的方法调用[...]参数列表A是表达式列表,每个表达式被分类为正式中相应参数的变量[...] -drameter-list of D.

好.所以我们在D1上对D1进行重载分辨率.D1的形式参数列表是空的,所以我们对X()和joy进行重载解析,我们找到一个有效的方法"string X()".类似地,D2的形式参数列表为空.同样,我们发现"字符串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返回空白

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

无论如何,我们没有"更好"规则来确定哪个转换更好,X到D1或X到D2.因此,我们对Y(X)的分辨率给出了模糊误差.

  • 破解 - 非常感谢答案和(希望)由此产生的规格改进:)我个人认为重载分辨率考虑到返回类型*对于方法组转换*是合理的,以使行为更直观,但我确实理解它会以牺牲一致性为代价.(当方法组中只有一个方法时,应用于方法组转换的泛型类型推断也是如此,我想我们之前已经讨论过了.) (8认同)

Jon*_*eet 35

编辑:我想我已经知道了.

正如zinglon所说,这是因为即使编译时应用程序失败GetString,Action也会有隐式转换.以下是6.6节的介绍,重点介绍(我的):

从方法组(第7.1节)到兼容的委托类型存在隐式转换(第6.1节).给定委托类型D和被分类为方法组的表达式E,如果E包含至少一个以其正常形式(第7.4.3.1节)适用于构造的参数列表的方法,则存在从E到D的隐式转换通过使用D的参数类型和修饰符,如下所述.

现在,我对第一句话感到困惑 - 它谈到了转换为兼容的委托类型.Action不是为任何方法兼容的委托GetString方法组,但该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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

这次它确定了它想要调用的方法 - 但是它无法执行所需的转换.不幸的是,我无法找到最终检查执行的规范 - 看起来它可能在7.5.5.1中,但我看不清楚到底在哪里.


旧的答案被删除了,除了这一点 - 因为我希望埃里克可以阐明这个问题的"原因"......

还在寻找...同时,如果我们三次说"Eric Lippert",你认为我们会去访问(因而答案)吗?

  • "Eric Lippert." (14认同)