代表方差规则的怪异示例

时间:2019-02-28 15:56:31

标签: c# delegates covariance contravariance

在埃里克·利珀特(Eric Lippert)的博客文章中,有关协方差和协变或方差的简短描述,以及诸如 C#简而言之之类的书中指出:

  

如果您要定义一个通用的委托人类型,那么好的做法是:

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

这样做可以让转换过程在尊重他人的情况下自然发挥作用   类型之间的继承关系。

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

使用此类的层次结构:

class Animal { }

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

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

class Snake : Reptile { }
class Turtle : Reptile { }

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

 // 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;

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

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

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

我认为:

 Func<Dog, Reptile> func1 = (Mamal d) => new Reptile();

正在发生的是类似的转换:

Func<Mamal, Reptile> => Func<Dog, Reptile>. 

从委托到委托的方差规则是否不同于从方法组到委托的方差规则?

1 个答案:

答案 0 :(得分: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年前从事此设计工作的人进行心理分析,以及为什么他们发现某些折衷方案令人信服,而其他折衷方案却没有那么多。我不具备或不愿意进行这种分析;这实际上涉及重新散布数百小时的参数。

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

告诉你什么,让我们让 you 代替我来做。这是您的一种情况:

Action<Mammal> ma = (Animal a) => ...

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

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

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

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

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

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