我有一种情况,我想使用方法组语法而不是匿名方法(或lambda语法)来调用函数。
该函数有两个重载,一个接受一个Action,其他的需要Func<string>。
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() { } }
根据0xcde在2019年3月20日(我发布此问题的九年后!)的评论,由于改进了重载候选,此代码从C#7.3开始编译。
首先,我只想说乔恩的答案是正确的。这是该规范中最毛茸茸的部分之一,因此对于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为空返回
E是一个匿名函数,T1和T2是具有相同参数列表的委托类型或表达式树类型,在该参数列表的上下文中存在一个针对E的推断返回类型X,并且满足以下条件之一:
T1具有返回类型Y1,T2具有返回类型Y2,并且从X到Y1的转换要好于从X到Y2的转换
T1的返回类型为Y,而T2为空返回
不幸的是,方法组转换和lambda转换在这方面不一致。但是,我可以忍受。
无论如何,我们没有“更好”规则来确定从X到D1或X到D2哪个转换更好。因此,我们给出了Y(X)分辨率的歧义误差。