代表方差规则的怪异示例

Zac*_*OIR 2 c# delegates covariance contravariance

在埃里克·利珀特(Eric Lippert)的博客文章中,简短地介绍了协方差和逆方差或方差,以及在《Cut in Nutshell》之类的书中指出:

如果要定义通用委托类型,则好的做法是:

  • 将仅在返回值上使用的类型参数标记为协变(out)。
  • 将仅在参数上使用的所有类型参数标记为反(in)。

这样做可以通过尊重类型之间的继承关系来使转换自然地工作。

因此,我正在对此进行试验,并且找到了一个相当奇怪的示例。

使用此类的层次结构:

class Animal { }

class Mamal : Animal { }
class Reptile : Animal { }

class Dog : Mamal { }
class Hog : Mamal { }

class Snake : Reptile { }
class Turtle : Reptile { }
Run Code Online (Sandbox Code Playgroud)

在尝试使用方法组到委托的转换和委托到委托的转换时,我编写了以下代码片段:

 // Intellisense is complaining here  
 Func<Dog, Reptile> func1 = (Mamal d) => new Reptile();

 // A local method that has the same return type and same parameter type as the lambda expression.
 Reptile GetReptile(Mamal d) => new Reptile();

 // Works here.  
 Func<Dog, Reptile> func2 = GetReptile;
Run Code Online (Sandbox Code Playgroud)

为什么方差规则适用于局部方法而不适用于lambda表达式?

假定lambda表达式是一个未命名的方法,它代替了委托实例,并且编译器立即将lambda表达式转换为以下任意一种:

  • 委托实例。
  • 类型为Expression的表达式树。

我认为与:

 Func<Dog, Reptile> func1 = (Mamal d) => new Reptile();
Run Code Online (Sandbox Code Playgroud)

发生的事情是来自类似的转换:

Func<Mamal, Reptile> => Func<Dog, Reptile>. 
Run Code Online (Sandbox Code Playgroud)

代表之间的差异规则与方法组到代表的差异规则是否不同?

Eri*_*ert 5

让我稍微澄清一下您的问题。

这三件事可以转换为委托类型:(1)lambda(或C#2样式匿名方法),(2)方法组或本地方法,(3)另一个委托。在每种情况下,哪些协变和协变转换的规则在法律上是否不同?

是。

它们有何不同?

您应该阅读规范以获取确切的细节,但是要简短:

  • 仅当将委托类型参数标记为协变或相反时,才能将泛型委托类型转换为另一个泛型委托类型。也就是说,Func<Giraffe>可以转换为,Func<Animal>因为Func<out T>被标记为协变。(此外:如果您需要从一种委托类型到另一种委托类型进行变体转换,并且该委托类型不支持差异,您可以做的是使用Invoke“源”委托方法的方法组,现在我们可以重新使用方法组规则,但失去引用相等性。)

  • 即使未将委托标记为支持方差,也可以使用协方差和反方差规则将方法组或局部方法转换为匹配的委托人类型。也就是说,你可以转换Giraffe G()delegate Animal D();即使D是不通用的,或者是通用的,但没有标记为变量。

  • 转换lambda的规则很复杂。如果lambda没有形式参数类型,则使用目标类型的形式参数类型,分析lambda主体,如果主体没有错误分析并且结果与目标类型的结果类型兼容,则该主体是可转换的。 如果lambda确实具有形式参数类型,则它们必须与目标类型的形式参数类型完全匹配

他们为什么不同?

不同的事物是不同的。我真的不知道该如何回答这样一个模糊,广泛的“为什么”问题。

这些规则是由十几个人坐在一个房间多年后得出的。在C#1中添加了用于委托转换的方法组,在C#2中添加了通用委托,在C#3中添加了lambda,在C#4中添加了通用委托方差。我不知道如何回答关于实际上完成了数百个小时的设计工作,其中一半以上是在我加入设计团队之前的。该设计工作涉及许多争论和折衷。 请不要问关于编程语言设计的模糊“为什么”和“为什么不”的问题

诸如“规范的哪一页定义了此行为?”之类的问题。有一个答案,但是“为什么规范会这样说?” 基本上是要求对15年前从事此设计工作的人员进行心理分析,以及为什么他们发现某些折衷方案令人信服,而其他折衷方案却没有那么多。我不具备或不愿意进行这种分析;这实际上涉及重新散布数百小时的争论。

如果您的问题是“哪些通用语言设计原则会鼓励或不鼓励精确或不精确的匹配?” 这是我可以长时间讨论的话题。例如,昨天我设计了一种新的重载解决算法,重载解决方案只不过确定精确或不精确匹配何时重要以及它们的重要性问一个更具体的问题

告诉你吧,让我们做的工作,而不是我的。这是您的一种情况:

Action<Mammal> ma = (Animal a) => ...
Run Code Online (Sandbox Code Playgroud)

向我描述禁止用户编写该行代码的引人注目的好处。例子:对我来说看起来确实像个虫子。看起来用户开始输入一件事,并在中途改变了主意。这种毫无意义的,怪异的不一致是草率的,错误的代码的高度特征,可以很容易地避免。 C#的设计原则之一是,该语言会在您可能犯错时告诉您。那肯定看起来像个错误。

现在,提出反对意见,即应允许使用代码。示例:就Lambda和本地方法之间的可转换性规则而言,是否也应该保持一致是一个普遍原则?与防止草率错误的规则相比,该规则有多重要?

现在提出更多关于每种选择的优缺点的争论,以及不同的开发人员方案如何影响您对每种选择的分析。给出许多实际代码示例。

请记住,有些用户是类型系统方面的专家,有些则不是。有些是具有二十年经验的建筑师,有些则刚大学毕业。有些Java程序员昨天才开始使用C#,但仍处于一种擦除的心态。有些是F#程序员,他们习惯于对程序进行全程序推理。 对每种情况的优缺点进行大量记录,然后提出一个折衷的建议,该建议在任何重要的情况下都不能妥协。

现在考虑费用。拟议的功能会难以实现吗?是否添加新的错误消息?消息是否清晰,还是会使用户感到困惑?建议的功能可能会阻止将来的任何功能吗?我注意到,要执行此步骤,您必须对语言的未来做出很好的预测。

一旦做出决定,然后用一句话来描述所有工作,回答“为什么要做出决定?”的问题。