代表方差规则的奇怪例子

Weird example of variance rules for delegates

在 Eric Lippert 关于协变和逆变或简称方差的博文中,以及 C# in a Nutshell 等书籍中,指出:

If you’re defining a generic delegate type, it’s good practice to:

  • Mark a type parameter used only on the return value as covariant (out).
  • Mark any type parameters used only on parameters as contravariant (in).

Doing so allows conversions to work naturally by respecting inheritance relationships between types.

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

使用此 class 层次结构:

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 表达式转换为:

我假设 :

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

发生的是从类似 :

的转换
Func<Mamal, Reptile> => Func<Dog, Reptile>. 

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

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

These three things may be converted to a delegate type: (1) a lambda (or C# 2 style anonymous method), (2) a method group or local method, (3) another delegate. Are the rules for what covariant and contravariant conversions are legal different in each case?

是的。

How are they different?

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

  • 泛型委托类型可以转换为另一种泛型委托类型仅当委托类型参数被标记为协变或逆变时。也就是说,Func<Giraffe> 可以转换为 Func<Animal>,因为 Func<out T> 被标记为协变的。 (另外:如果您需要从一种委托类型到另一种委托类型进行变体转换,而该委托类型不支持变体,您可以改为使用 [=95= 的 Invoke 方法的方法组]委托,现在我们正在使用方法组规则,但失去了引用相等性。)

  • 可以使用协变和逆变规则将方法组或本地方法转换为匹配的委托类型,即使委托未标记为支持变体。也就是说,您可以将 Giraffe G() 转换为 delegate Animal D();,即使 D 不是泛型,或者是泛型但未标记为变体。

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

Why are they different?

不同的东西是不同的。我真的不知道如何回答这样一个含糊、广泛的问题 "why"。

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

像 "what page of the spec defines this behaviour?" 这样的问题有答案,但 "why does the spec say that?" 基本上是要求对十五年前从事此设计工作的人进行心理分析,以及为什么他们发现某些妥协令人信服而其他人不信服非常。我没有能力也不愿意做那个分析;这将涉及从字面上重新散列数百小时的争论。

如果您的问题是 "what are general language design principles which encourage or discourage exact or inexact matching?",那么我可以花数小时详细讨论这个话题。例如,我在昨天 设计了一个新的重载解析算法,而重载解析只是 无非就是决定精确匹配或不精确匹配何时重要,以及它们有多重要问一个更具体的问题

告诉你吧,让我们让 代替我来做这项工作。这是您的场景之一:

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

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

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

现在就每个选择的微妙优缺点以及不同的开发人员场景如何影响您对每个选择的分析提出更多论点。给出很多很多实际代码的例子。

请记住,有些用户是类型系统方面的专家,有些则不是。有些是拥有二十年经验的建筑师,有些是刚从大学毕业。有些是 Java 昨天刚接触 C# 的程序员,仍然处于擦除心态;有些是习惯于全程序推理的 F# 程序员。 广泛记录每种情况的优缺点,然后提出一个在任何重要情况下都不会妥协太多的折衷方案。

现在考虑成本。提议的功能是否难以实施?它会添加新的错误消息吗?信息是否清晰,还是会让用户感到困惑?提议的功能是否可能阻止任何未来的功能?我注意到您必须对语言的未来做出良好 预测才能执行此步骤。

一旦你做出决定,然后 用一句话描述所有的工作来回答问题 "why did you decide that?"