代码之家  ›  专栏  ›  技术社区  ›  Richard Ev

编译器不明确的调用错误-具有func<>或action的匿名方法和方法组

  •  100
  • Richard Ev  · 技术社区  · 15 年前

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

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

    我可以使用匿名方法(或lambda语法)很高兴地调用这两个重载,但得到的编译器错误为 不明确的调用 如果我使用方法组语法。我可以通过明确的演员阵容 行动 函数字符串 但不认为这是必要的。

    有人能解释为什么需要明确的强制转换吗?

    下面是代码示例。

    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() { }
    }
    
    4 回复  |  直到 7 年前
        1
  •  97
  •   user247702    7 年前

    首先,让我说乔恩的回答是正确的。这是规范中最毛茸茸的部分之一,对于乔恩来说,这是一个很好的选择,因为他是第一个潜入IT领域的人。

    第二,让我说这句话:

    存在从方法组到 兼容的委托类型

    (重点补充)是非常误导和不幸的。我要和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包含at,则从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的形式参数列表中被分类为相应参数的变量[…]。

    好啊。所以我们在x上对d1做过载分解。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)尝试选择一个我们知道可以在我们知道不会工作的转换上工作的转换?

    这是一个判断。用 兰姆达斯 我们 在这些类型的转换中考虑返回类型,在第7.4.3.3节中:

    e是一个匿名函数,T1和T2 是委托类型还是表达式树 具有相同参数列表的类型, 存在e的推断返回类型x 在参数列表的上下文中, 以下情况之一适用:

    • T1有一个返回类型y1,而T2有一个返回类型y2,并且转换 从x到y1比 从x到y2的转换

    • T1为返回类型Y,T2为空返回

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

    无论如何,我们没有“betterness”规则来确定哪个转换更好,x到d1或x到d2。因此,我们给出了Y(x)分辨率的模糊误差。

        2
  •  35
  •   Jon Skeet    15 年前

    编辑:我想我明白了。

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

    存在隐式转换(_§6.1) 从方法组(_§7.1)到 兼容的委托类型。给定一个 委托类型d和表达式e 分类为方法组, 从e存在隐式转换 如果e至少包含一种方法,则为d 那就是 以正常形式适用 (_§7.4.3.1)至论点清单 使用参数构造 d的类型和修饰语 ,如所描述的 在下面。

    现在,我被第一句话弄糊涂了——它说的是如何转换为兼容的委托类型。 行动 不是中任何方法的兼容委托 获取字符串 方法组,但 GetString() 方法 以其正常形式适用于通过使用d的参数类型和修饰符构造的参数列表。请注意 谈论D的返回类型。这就是为什么它变得困惑…因为它只检查 获取字符串() 什么时候? 应用 转换,而不是检查它的存在。

    我认为简单地去掉等式中的超负荷是有指导意义的,看看转换的 存在 及其 适用性 可以显示。下面是一个简短但完整的示例:

    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:'string program.getString()'有错误 返回类型

    这一次,它计算出了它想要调用的方法——但随后它无法执行所需的转换。不幸的是,我找不到规范中进行最终检查的地方——看起来是这样的。 可以 在7.5.5.1,但我不知道具体在哪里。


    旧的答案被删除了,除了这一点-因为我希望埃里克能阐明这个问题的“为什么”…

    仍在寻找…同时,如果我们说“埃里克·利珀特”三次,你认为我们会去拜访(这样就有了答案)吗?

        3
  •  1
  •   Matt Ellen Bipin Vayalu    15 年前

    使用 Func<string> Action<string> (显然与 Action 函数字符串 ClassWithDelegateMethods 消除歧义。

    歧义也发生在 行动 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(); }
    } 
    
        4
  •  0
  •   Daniel A. White    15 年前

    超载 Func Action 类似于(因为他们都是代表)

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

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