代表的方差规则的奇怪示例

问题描述 投票:2回答:1

在Eric Lippert关于协方差和逆变或差异的博客文章中,以及在诸如C#in a Nutshell等书籍中,它表明:

如果您要定义通用委托类型,那么最好:

  • 将仅在返回值上使用的类型参数标记为协变(out)。
  • 将仅用于参数的任何类型参数标记为逆变量(in)。

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

所以我正在试验这个,我发现了一个相当奇怪的例子。

使用此类层次结构:

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>. 

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

c# delegates covariance contravariance
1个回答
5
投票

让我稍微澄清你的问题。

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

是。

他们有什么不同?

您应该阅读规范以获取确切的详细信息,但请简要说明:

  • 仅当委托类型参数标记为协变或逆变时,通用委托类型才可以转换为另一种通用委托类型。也就是说,Func<Giraffe>可以转换为Func<Animal>,因为Func<out T>被标记为协变。 (旁白:如果你需要从一个委托类型转换到另一个委托类型并且委托类型不支持方差,那么你可以做的是使用“source”委托的Invoke方法的方法组,现在我们'使用方法组规则,但丢失了引用相等性。)
  • 即使委托未标记为支持方差,也可以使用协方差和逆变规则将方法组或本地方法转换为匹配的委托类型。也就是说,即使Giraffe G()不是通用的,也可以将delegate Animal D();转换为D,或者是通用的但未标记为变体。
  • 转换lambda的规则很复杂。如果lambda没有形式参数类型,则使用目标类型的形式参数类型,分析lambda体,如果body分析没有错误并且结果与目标类型的结果类型兼容,则它是可转换的。如果lambda具有形式参数类型,则它们必须与目标类型的形式参数类型完全匹配。

他们为什么不同?

不同的事情是不同的。我真的不知道如何回答这种模糊,宽泛的“为什么”的问题。

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

诸如“规范的哪个页面定义了这种行为?”之类的问题。有一个答案,但“为什么规范会这么说?”基本上是要求对15年前从事这项设计工作的人进行心理分析,以及为什么他们发现某些妥协令人信服而其他人则没有那么多。我没有能力或不愿意做那个分析;它将涉及重复数百小时的争论。

如果您的问题是“什么是鼓励或阻止精确或不精确匹配的一般语言设计原则?”这个话题我可以讨论几个小时。例如,我昨天设计了一个新的重载决策算法,除了决定精确匹配或不精确匹配的重要性以及它们的重要性之外,重载决策只不过是其中之一。问一个更具体的问题。

告诉你什么,让我们做你的工作,而不是我。这是你的一个场景:

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

向我描述禁止用户编写该行代码的令人信服的好处。示例:对我来说,这肯定是个错误。看起来用户开始输入一个东西并改变了他们的想法。这种毫无意义的,奇怪的不一致性是草率,错误代码的高度特征,并且可以很容易地防止它。 C#的设计原则之一是语言告诉你什么时候你可能犯了错误。这肯定是一个错误。

现在提出应该允许代码的反驳论据。示例:就可兑换规则而言,lambdas和本地方法之间是否应该保持一致是一般原则吗?与防止草率漏洞的规则相比,这条规则有多重要?

现在提出十几个关于每个选择的微妙优缺点的论据,以及不同的开发者场景如何影响您对每个选择的分析。提供许多现实代码的例子。

请记住,有些用户是类型系统的专家,有些则不是。有些是具有二十年经验的建筑师,有些是大学毕业的。有些是Java程序员,他们昨天刚拿起C#并且仍处于擦除的心态;有些人是习惯于全程推理的F#程序员。对每个场景的优缺点进行广泛记录,然后提出一个妥协的提议,在任何重要场景下都不会妥协。

现在考虑成本。拟议的功能难以实施吗?它是否添加了新的错误消息?消息是否清晰,还是会使用户感到困惑?建议的功能是否可能阻止任何未来的功能?我注意到你必须很好地预测语言的未来才能完成这一步。

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

© www.soinside.com 2019 - 2024. All rights reserved.