协变和逆变在将匿名方法分配给委托时不起作用

3

我有以下代码,源自这篇MSDN文章

public class First { }  
public class Second : First { }  
public delegate First SampleDelegate(Second a);

// Matching signature.  
public static First ASecondRFirst(Second first)  
{ return new First(); }  

// The return type is more derived.  
public static Second ASecondRSecond(Second second)  
{ return new Second(); }  

// The argument type is less derived.  
public static First AFirstRFirst(First first)  
{ return new First(); }  

// The return type is more derived   
// and the argument type is less derived.  
public static Second AFirstRSecond(First first)  
{ return new Second(); }

SampleDelegate test;
test = ASecondRFirst;
test = ASecondRSecond;
test = AFirstRFirst;
test = AFirstRSecond;

这些代码编译没有问题,但我想测试将委托分配给匿名Lambda表达式:

test = (First x) => { return new Second(); };

然而,在那一行,我遇到了错误:
“无法将 lambda 表达式转换为类型'SampleDelegate',因为参数类型与委托参数类型不匹配。”
具体地说:“参数 1 声明为类型 'ConsoleApp1.First',但应该是 ConsoleApp1.Second”(“ConsoleApp1” 是项目的名称)。
我无法理解我的 lambda 到底出了什么问题。
显然,协变、逆变等都工作得很好,似乎只是我的 lambda 出了问题。

“First()”是一个糟糕的类名。它会与Linq函数“First()”冲突(或者至少在我尝试阅读代码时会这样)。 - Neil
@Neil, 哦,抱歉 - 没想到那个。但请记住这只是一个用于测试协变和逆变在委托中的应用程序。 :) - J. Doe
1个回答

10
成为一个无聊的人,给出答案“因为规范是这样规定的”(我的想法在TL;DR结尾处)...基本上,这是因为方法组转换(例如将方法分配给委托)和匿名函数转换(例如将lambda分配给委托)遵循不同的规则,只有前者受益于差异性。请注意,方法组指的是相同方法的1个或多个重载组 - 因此,您的单个方法仍然被视为单独的方法组。C#语言规范第6.5节讨论了匿名函数转换: “匿名方法表达式或lambda表达式被归类为匿名函数(§7.15)。该表达式没有类型,但可以隐式转换为兼容的委托类型或表达式树类型。具体来说,匿名函数F与委托类型D兼容,前提是:... 如果F具有显式类型的参数列表,则D中的每个参数都具有与F中对应参数相同的类型和修饰符。” 然而,第6.6节讨论了方法组转换:
如果一个委托类型D和一个被分类为方法组的表达式E存在至少一个方法可以在其正常形式下应用于由D的参数类型和修饰符构造的参数列表,则从方法组到兼容的委托类型之间存在一种隐式转换。将方法组E转换为委托类型D的编译时应用在以下内容中进行描述。请注意,从E到D的隐式转换的存在并不保证转换的编译时应用会成功而不出现错误。因此,方法组到委托的转换使用了与尝试使用相应参数类型调用方法的更多或更少相同的规则。我们被引导到第7.6.5.1节,该节将我们引导到第7.5.3.1节。这变得复杂,所以我不会在这里逐字粘贴它。有趣的是,我找不到关于委托协变性的章节,只有接口协变性(尽管第6.6节提到了在示例中提到它)。
TL;DR,当你写下以下内容时:
SampleDelegate test = SomeMethodGroup;

编译器会执行整个算法,以选择与委托类型兼容的方法组成员,遵循与调用方法时类似的重载解析规则。当您编写以下内容时:
SampleDelegate test = (First first) => new Second();

编译器遵循一个更简单的规则,即“lambda表达式的签名是否与委托的签名匹配”。

我想这是有道理的。大多数时候你会写:

SampleDelegate test = first => new Second();

编译器可以从委托签名中推断出参数类型。如果您自己添加了显式类型,这并不会完全改变使用的算法:编译器使用相同的算法,但如果它推断出的类型与您的显式类型冲突,则会出现错误。


请注意,几乎所有时间这个{{不重要}}。很少在lambda的参数上放置类型,因此通常只需编写以下内容:
SampleDelegate test = x => new Second();

编译器推断出x实际上是一个Second,这没问题:如果您编写了一个lambda函数,可以在xFirst的情况下工作,那么它也应该在xSecond的情况下工作(尽管有LSP)。请注意,即使SampleDelegate返回一个First,您仍然可以返回一个Second:编译器不介意。

1
@J.Doe 不必无礼。你的SampleDelegate接受一个Second并返回一个First。你的lambda接受一个First并返回一个Second。它们有不同的签名。我的答案解释了为什么你可以用方法组进行赋值,但不能用lambda进行赋值。具体来说,将方法组转换为委托遵循不同的规则,而将lambda分配给委托则遵循不同的规则。 - canton7
1
@J.Doe,因为你在做不同的事情,所以会有不同的行为。规范表明在这两种不同情况下会遵循不同的规则。我的回答说明了这些规则,并提供了我认为这两种情况为什么要遵循不同规则的思考。我不确定还能添加什么内容。 - canton7
2
非常感谢!我为我的粗鲁道歉,看来是我一直有错! - J. Doe
没问题 - 我会在开头添加一个摘要来澄清。 - canton7
1
@J.Doe 事后看来,我本可以在一开始更明确地说出只有方法组受益于变异,这可能有助于避免混淆。我现在已经添加了它。 - canton7
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接