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


102

我有一种情况,我想使用方法组语法而不是匿名方法(或lambda语法)来调用函数。

该函数有两个重载,一个接受一个Action,其他的需要Func<string>

我可以使用匿名方法(或lambda语法)愉快地调用这两个重载,但是如果我使用方法组语法,则会出现歧义调用的编译器错误。我可以通过显式强制转换为Action或来变通Func<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甚至在您尝试完全编译之前突出显示的编译问题。
理查德·埃夫

1
顺便说一句,这是一个很大的问题。我喜欢强迫我进入规格的任何东西:)
Jon Skeet 2010年

1
请注意,<LangVersion>7.3</LangVersion>由于使用了改进的重载候选,因此如果使用C#7.3()或更高版本,示例代码将可以编译。
0xced

Answers:


97

首先,我只想说乔恩的答案是正确的。这是该规范中最毛茸茸的部分之一,因此对于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包含至少一种适用于使用参数构造的参数列表的方法,则存在从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++));

在表达式树中,增量操作是非法的。但是,lambda仍然可以转换为表达式树类型,即使曾经使用过转换,这也是一个错误!这里的原理是,我们可能想更改以后在表达式树中可以执行的规则。更改这些规则不应更改类型系统规则。我们想强迫您现在使程序变得模棱两可,因此,当我们将来更改表达式树的规则以使其变得更好时,我们不会在重载解析中引入重大更改

无论如何,这是这种奇怪规则的另一个例子。出于过载解决的目的,可以存在转换,但实际上是错误的。尽管实际上,这并非我们所处的情况。

继续:

对应于形式为E(A)的方法调用,选择一个单独的方法M。参数列表A是一个表达式列表,每个表达式都被归类为形式中相应参数的变量-D的参数列表。

好。因此,我们针对D1在X上进行了重载解析。D1的形式参数列表为空,因此我们对X()进行重载解析并获得喜悦,我们找到了一种有效的方法“ string X()”。同样,D2的形式参数列表为空。同样,我们发现“字符串X()”也是一种在这里也可以使用的方法。

这里的原理是,确定方法组可转换性需要使用重载解析从方法组中选择一个方法,并且重载解析不考虑返回类型

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

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

现在,这条线相关吗?

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

实际上,不,不在此程序中。我们永远都不会激活这条线。因为请记住,我们在这里正在尝试在Y(X)上进行重载解析。我们有两个候选Y(D1)和Y(D2)。两者都适用。哪个更好在规范中没有任何地方描述这两种可能的转换之间的更好之处

现在,可以肯定地说,有效转换比产生错误的转换要好。在这种情况下,那实际上就是说过载解析确实考虑了返回类型,这是我们要避免的事情。那么问题是哪个原则更好:(1)保持重载解析不考虑返回类型的不变性,或者(2)尝试选择我们知道将适用于我们不知道的转换?

这是一个判断电话。对于lambdas,我们在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 Skeet 2010年

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);
    }
}

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

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

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

现在这是错误ActionMethod(GetString)

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

这次,它算出了要调用的方法-但是无法执行所需的转换。不幸的是,我无法确定规范的最后执行位置-看起来可能在7.5.5.1中,但是我看不到确切的位置。


除了这点以外,旧答案已删除-因为我希望Eric可以阐明这个问题的“原因” ...

仍在寻找……同时,如果我们三遍说“ Eric Lippert”,您是否认为我们会拜访(并因此得到答案)?


@Jon-可以吗classWithSimpleMethods.GetStringclassWithSimpleMethods.DoNothing不是代表吗?
Daniel A. White

@Daniel:否-这些表达式是方法组表达式,只有在从方法组到相关参数类型进行隐式转换时,重载方法才应视为适用。参见规范的7.4.3.1节。
乔恩·斯基特

阅读第6.6节,由于参数列表兼容,因此看起来存在从classWithSimpleMethods.GetString到Action的转换,但是该转换(如果尝试)在编译时失败。因此,这两种委托类型的确存在隐式转换并且该调用不明确。
zinglon 2010年

@zinglon:你是如何阅读6.6节,以确定从转换ClassWithSimpleMethods.GetStringAction是有效的?对于M与委托类型兼容的方法D(第15.2节),“存在从返回类型M到返回类型的标识或隐式引用转换D。”
杰森

@Jason:规范没有说转换有效,而是说存在。实际上,它是无效的,因为它在编译时失败。第6.6节的前两点确定转换是否存在。以下几点确定转换是否成功。从第2点开始:“否则,该算法将产生一个具有与D相同数量的参数的最佳方法M,并认为存在该转换。” §15.2在点3调用
zinglon

1

在remove中使用Func<string>Action<string>(显然与Action和非常不同Func<string>ClassWithDelegateMethods消除了歧义。

Action和之间也会出现歧义Func<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
{
}

如果您注意到,编译器不知道要调用哪个,因为它们仅在返回类型上有所不同。


我不认为这真的很像-因为您不能将a Func<string>转换为Action...,也不能转换仅由将字符串返回为其中一个的方法组成的方法组Action
乔恩·斯基特

2
你可以不投没有参数,并返回一个代表stringAction。我不明白为什么会有歧义。
杰森

3
@dtb:是的,消除重载可以解决问题-但这并不能真正解释为什么存在问题。
乔恩·斯基特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.