为啥在这种重载解析情况下编译器不能告诉更好的转换目标? (协方差)

Posted

技术标签:

【中文标题】为啥在这种重载解析情况下编译器不能告诉更好的转换目标? (协方差)【英文标题】:Why can't the compiler tell the better conversion target in this overload resolution case? (covariance)为什么在这种重载解析情况下编译器不能告诉更好的转换目标? (协方差) 【发布时间】:2013-12-22 07:36:43 【问题描述】:

理解关于重载解析的 C# 语言规范显然很难,现在我想知道为什么这个简单的案例会失败:

void Method(Func<string> f)


void Method(Func<object> f)


void Call()

    Method(() =>  throw new NotSupportedException(); );

这会产生编译时错误 CS0121,以下方法或属性之间的调用不明确: 后跟我的两个 Method 函数成员(重载)。

我的预期是Func&lt;string&gt; 是一个Func&lt;object&gt; 更好的转换目标,然后应该使用第一个重载。

自 .NET 4 和 C# 4 (2010) 以来,泛型委托类型 Func&lt;out TResult&gt; 一直是 TResult 中的协变,因此隐式转换从Func&lt;string&gt;Func&lt;object&gt; 存在,而从Func&lt;object&gt;Func&lt;string&gt; 显然不存在隐式转换。所以它会让Func&lt;string&gt;成为更好的转换目标,而重载决议应该选择第一个重载?

我的问题很简单:我在这里缺少 C# 规范的哪一部分?


加法:这很好用:

void Call()

    Method(null); // OK!

【问题讨论】:

PS!这工作正常:void Test(Func&lt;string&gt; f1, Func&lt;object&gt; f2, bool b) var x = b ? f1 : f2; 我无法确定,但它可能与这篇文章有关:blogs.msdn.com/b/ericlippert/archive/2011/02/21/… 在那里,Eric 解释说函数永远不会到达它们的端点,或者任何返回实际上根本没有任何返回值.所以我假设编译器会选择第一种方法而不是第二种方法,如果你的 lambda 是 Func 类型。但它的“未知类型”。不知何故.. 呃.. ;) @Imi lambda 本身没有委托(或表达式树)类型。对于我的 lambda,也找不到返回类型。但我的 lambda 肯定可以隐式转换为 Func&lt;string&gt;。我的 lambda 也可以隐式转换为 Func&lt;object&gt;。所以这两个重载都是“适用的”。现在,一个比另一个“更好”吗?是的,因为Func&lt;string&gt; 是比Func&lt;object&gt; 更好的转化目标。另请参阅我对问题的补充(文字 null 本身没有类型,但 null 可隐式转换为 Func&lt;string&gt;Func&lt;object&gt;)。 它为null 正确选择的事实表明,7.5.3.5 更好的转化目标得到了正确遵守。 7.5.3.3 Better conversion from expression 甚至在此之前可能存在问题。我打算通过阅读规范发布答案,但我认为我认为你是对的并且这应该有效就足够了。 感觉有人问过类似的问题,但想不起来.. 【参考方案1】:

我的问题很简单:我在这里缺少 C# 规范的哪一部分?

总结:

您在实现中发现了一个已知的小错误。 出于向后兼容性的原因,该错误将被保留。 C# 3 规范包含有关如何处理“null”情况的错误;它已在 C# 4 规范中得到修复。 您可以使用任何无法推断返回类型的 lambda 来重现错误行为。例如:Method(() =&gt; null);

详情:

C# 5 规范说,更好的规则是:

如果表达式具有类型,则选择从该类型到候选参数类型的更好转换。

如果表达式没有类型且不是 lambda,则选择转换为更好的类型。

如果表达式是 lambda 则首先考虑哪种参数类型更好;如果两者都不是更好并且委托类型具有相同的参数列表,则考虑推断的 lambda 的返回类型和委托的返回类型之间的关系。

所以预期的行为是:首先编译器应该检查一个参数类型是否明显优于另一个参数类型,无论参数是否具有类型。如果这不能解决这种情况并且参数是 lambda,然后检查转换为参数“委托类型”返回类型的推断返回类型中哪个更好。

实现中的错误是实现不这样做。相反,在参数是 lambda 的情况下,它完全跳过类型优化检查并直接进行推断的返回类型优化检查,然后由于没有推断的返回类型而失败。

我的目的是为 Roslyn 解决这个问题。然而,当我去实现它时,我们发现进行修复会导致一些真实世界的代码停止编译。 (我不记得真实世界的代码是什么,并且我不再能够访问存在兼容性问题的数据库。)因此我们决定维护现有的小错误。

我注意到,在我在 C# 4 中添加委托变量之前,该错误基本上是不可能的;在 C# 3 中,两种不同的委托类型不可能或多或少具有特定性,因此唯一可以应用的规则是 lambda 规则。由于 C# 3 中没有测试可以揭示该错误,因此很容易编写。我的错,对不起。

我还注意到,当您开始将表达式树类型混入其中时,分析会变得更加复杂。即使Func&lt;string&gt; 优于Func&lt;object&gt;Expression&lt;Func&lt;string&gt;&gt; 也不能转换为Expression&lt;Func&lt;object&gt;&gt;!如果更好的算法对于 lambda 是去表达式树还是去委托是不可知的,那就太好了,但在某些方面它不是。这些情况变得复杂,我不想在这里费力。

这个小错误是实现规范实际所说的而不是你认为它所说的的重要性的客观教训。如果我在 C# 3 中更加小心以确保代码与规范相匹配,那么代码将在“null”情况下失败,那么之前很明显 C# 3 规范是错误的。并且实现在类型检查之前进行 lambda 检查,这是一个定时炸弹,当 C# 4 滚动并突然变成不正确的代码时等待引爆。无论如何都应该先进行类型检查。

【讨论】:

嗨,埃里克,感谢您的回答!我刚刚尝试使用 Roslyn 进行编译,结果您在诊断中得到了相同的模棱两可的呼叫消息,所以我猜它没有进入。 另一种 lambda 没有返回类型的情况是 Method(() =&gt; null);,它存在相同的错误(正如您的回答所预期的那样)。 我们指的是哪个版本的规范?对于VS2013附带的版本(在我的机器上是C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC#\Specifications\1033),第7.5.3.3更好地从表达式转换部分,第二个项目符号说“•E不是匿名函数并且T1 是比 T2 (§7.5.3.5) 更好的转化目标"null 文字似乎在这里,所以似乎在规范中没有关于 Method(null) 的错误。 @JeppeStigNielsen:很好,它已在规范中修复。我不在我的工作机器上,我在这里有一份较旧的规范副本。以前版本的规范没有这种语言。 请您检查一下您何时可以访问新规范并更新您的答案?我不确定我是否和你一样理解这一点,但在我引用的项目符号中,"• E 不是匿名函数,T1 是比 T2 更好的转换目标",如果一个会发生什么删除了“E不是匿名函数”部分? (由于具体的委托类型总是密封的,我猜只有协变和/或逆变可以导致一种类型比另一种类型成为更好的目标。)【参考方案2】:

嗯,你是对的。导致问题的原因是您作为参数传递的委托。它没有明确的返回类型,你只是抛出一个异常。 Exception 基本上是 object 但它不被视为方法的返回类型。由于异常抛出后没有返回调用,编译器不确定它应该使用什么重载。

试试这个

void Call()

    Method(() => 
     
        throw new NotSupportedException();
        return "";
    );

现在选择重载没有问题,因为明确声明了传递给返回调用的对象类型。由于异常抛出而无法访问返回调用并不重要,但现在编译器知道它应该使用什么重载。

编辑:

至于传递 null 的情况,老实说,我不知道答案。

【讨论】:

匿名方法抛出异常(或者说,返回null)的事实意味着它可以转换为Func的任何一种类型,是的,当然给它一个特定的返回类型限制那。但是根据重载解决方案的规范,它仍然不应该是一个问题。这并不能解释为什么编译器无法区分,只是提供了一种解决方法。 我没有阅读规范。但我知道的是,在翻译过程中,当重载解析存在问题时,编译器会尝试通过检查返回调用来判断方法的返回类型,如果没有,则无法设置解析。 @OndrejJanacek Rawling 是对的,这不是我要问的答案。如果您有两种方法void Method(Func&lt;object&gt; f) void Method(Action a) ,则允许调用Method(() =&gt; throw new NotSupportedException(); );,并使用Func&lt;string&gt; 进行重载。所以仅仅因为 lambda 本身并不清楚返回类型,你不能说一切都必须失败。 要编辑:当说Method(null); 时,一切都按预期工作,调用会转到更好的函数成员,所以我们不会为这种情况寻求答案。添加的目的是为了说明它在相关情况下可以正常工作(即按照规范的预期)。 @JeppeStigNielsen 然后我应该列出规范并尝试弄清楚。

以上是关于为啥在这种重载解析情况下编译器不能告诉更好的转换目标? (协方差)的主要内容,如果未能解决你的问题,请参考以下文章

重载操作符与转换(下)

为啥错误 LNK2001:在这种情况下无法解析外部符号? [复制]

在这种情况下如何重载“+”运算符,

15. 函数运算符重载及转换的知识点小结

15. 函数运算符重载及转换的知识点小结

为啥在这种情况下转换属性不起作用? [复制]