【问题标题】:Weird example of variance rules for delegates代表方差规则的奇怪示例
【发布时间】:2019-07-22 14:25:36
【问题描述】:

在 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 { }

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

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

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

【问题讨论】:

    标签: c# delegates covariance contravariance


    【解决方案1】:

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

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

    是的。

    它们有何不同?

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

    • 一个泛型委托类型可以转换为另一个泛型委托类型只有当委托类型参数被标记为协变或逆变。也就是说,Func&lt;Giraffe&gt; 可以转换为Func&lt;Animal&gt;,因为Func&lt;out T&gt; 被标记为协变。 (另外:如果您需要从一种委托类型到另一种委托类型的变体转换并且委托类型不支持变体,您可以改为使用“源”委托的Invoke 方法的方法组,并且现在我们正在使用方法组规则,但失去了引用相等性。)

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

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

    它们为什么不同?

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

    这些规则是由十几个人坐在一个房间里多年的时间得出的。在 C# 1 中添加了方法组到委托转换,在 C# 2 中添加了泛型委托,在 C# 3 中添加了 lambda,在 C# 4 中添加了泛型委托变体。我不知道如何回答关于“为什么”的问题实际上已经完成了数百小时的设计工作,其中一半以上是在我加入设计团队之前完成的。该设计工作涉及大量争论和妥协。 请不要就编程语言设计提出含糊的“为什么”和“为什么不”的问题

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

    如果您的问题是“鼓励或不鼓励精确匹配或不精确匹配的一般语言设计原则是什么?”这是一个我可以长时间讨论几个小时的话题。例如,我在昨天设计了一个新的重载解析算法,而重载解析只是决定精确匹配或不精确匹配何时重要,以及它们的重要性提出更具体的问题

    告诉你什么,让代替我做这项工作。这是您的一种情况:

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

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

    现在提出应该允许该代码的反论点。示例:就其可转换性规则而言,lambda 和本地方法之间应该保持一致是否也是一般原则?与防止草率错误的规则相比,该规则有多重要?

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

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

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

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

    【讨论】:

    • 很抱歉这个问题花了很长时间才被标记为答案。也许我问这个问题的方式似乎表明我在质疑设计团队的决定,但我只是想了解的情况并非如此,因为我对这个话题非常感兴趣。我理解为使类型系统以正确的方式工作所付出的努力,我也理解讨论和理解决策需要一些抽象数学主题的高级知识。无论如何,非常感谢您为这个主题提供了亮点。
    猜你喜欢
    • 2023-03-22
    • 1970-01-01
    • 2021-12-27
    • 2017-09-05
    • 1970-01-01
    • 2011-03-04
    • 1970-01-01
    • 2021-06-10
    • 2017-07-21
    相关资源
    最近更新 更多