为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?

Posted

技术标签:

【中文标题】为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?【英文标题】:Why does C# (4.0) not allow co- and contravariance in generic class types?为什么 C# (4.0) 不允许泛型类类型中的协变和逆变? 【发布时间】:2021-12-03 01:18:42 【问题描述】:

造成这种限制的真正原因是什么?只是必须完成的工作吗?概念上很难吗?不可能吗?

当然,不能在字段中使用类型参数,因为它们始终是可读写的。但这不可能是答案,不是吗?

这个问题的原因是我正在写一篇关于 C# 4 中的方差支持的文章,我觉得我应该解释一下为什么它仅限于委托和接口。只是为了颠倒举证责任。

更新: 埃里克问了一个例子。

这个怎么样(不知道这是否有意义,但是:-))

public class Lookup<out T> where T : Animal 
  public T Find(string name) 
    Animal a = _cache.FindAnimalByName(name);
    return a as T;
  


var findReptiles = new Lookup<Reptile>();
Lookup<Animal> findAnimals = findReptiles;

将它放在一个类中的原因可能是类本身中保存的缓存。并且请不要将您不同类型的宠物命名相同!

顺便说一句,这将我带到optional type parameters in C# 5.0 :-)

更新 2: 我没有声称 CLR 和 C# 应该允许这样做。只是试图了解导致它没有的原因。

【问题讨论】:

当然,这是一个合理的例子,但你没有展示任何不能用接口完成的东西。只需制作接口 ILookup 并让 Lookup 实现它。您的类差异方案添加了哪些与接口差异相比引人注目的额外好处? 实际上没有。除此之外,它的代码更少。让我把举证责任倒过来。我们如何解释为什么不支持它。实际上,我并不是要求实施它。 “一直都是这样”不算数! :-) @Eric Lippert:我当然可以想象协变结构的用例。 KeyValuePair 怎么样?可以定义一个 IKeyValPair,并有一个结构 KeyValPair 来实现它,但这在很多使用场景中都需要非常可怕的装箱。 @supercat:这是可以使协变的类型的一个很好的例子。这里的关键是数据类型是逻辑上不可变的,因此您不必担心在构造函数中设置字段后会更改其值。如果我们有 struct 或 class 差异,那就是我想要开始的地方。 @Eric Lippert:我知道你讨厌可变结构(顺便说一句,我刚刚打开了一个关于这个主题的聊天室)但是对于结构的协变正确性来说,不变性是必要的吗?传递给期望 IEnumerable 的例程的 List 仍然是 List,但传递给期望 KeyValuePair 的例程的 KeyValuePair成为 KeyValuePair。假设 Key 是可变的,那么将 Key 设置为 Dog 会造成什么危害?一种是将狗存放在动物中,而不是猫中。 【参考方案1】:

首先,正如 Tomas 所说,CLR 不支持它。

其次,这将如何运作?假设你有

class C<out T>
 ... how are you planning on using T in here? ... 

T 只能用于输出位置。正如您所注意到的,该类不能有任何类型为 T 的字段,因为可以写入该字段。该类不能有任何采用 T 的方法,因为这些是逻辑写入的。假设您有这个功能——您将如何利用它?

这对于不可变类很有用,例如,如果我们可以合法地拥有一个类型为 T 的只读字段;这样我们就大大减少了它被不正确写入的可能性。但是很难想出其他允许以类型安全方式变化的场景。

如果你有这样的场景,我很乐意看到它。这将有助于有朝一日在 CLR 中实现这一点。

更新:见

Why isn't there generic variance for classes in C# 4.0?

关于这个问题的更多信息。

【讨论】:

好问题 :) 我记得去年在 Lang.NET 上 Anders 和一些 Java 编译器极客(抱歉)之间的讨论,Java 专家要求这个特性。他似乎知道他为什么要问。但我不记得了。我想到了没有状态的类。我会尝试在问题中弥补一些东西。 这对于不可变数据类型(例如Tuple&lt;T1, T2&gt;)来说绝对是一个不错的功能,但我同意这对于更改 CLR 来说不够令人信服:-)。 另一个用例是幻像类型,其中 T 仅用于类型安全,而不用于类的实现。 Lazy 和 Task 类不适合在 T 上协变吗?他们仅在其公共 API 的外部位置使用 T。 很多例子。建设者呢?当然,这些都是协变和逆变泛型的候选者。一个类的状态阻止它从通用方差中受益的概念似乎非常站不住脚。它仅在 state 对应于类型 var 时才成立,该类型完全依赖于手头的类。换句话说,不难想象一个类会产生 Ts 或消费 Ts,但不能两者兼而有之。而且因为您可以分解出一个接口,所以在我看来,这并不意味着不应该允许类有差异。 耸耸肩【参考方案2】:

据我所知,CLR 不支持此功能,因此添加此功能也需要 CLR 方面的大量工作。我相信 CLR 在 4.0 版之前实际上支持接口和委托的协变和逆变,因此这是一个相对简单的实现扩展。

(不过,为类支持此功能肯定很有用!)

【讨论】:

你是对的。接口和委托的泛型类型参数的差异来自 CLR 2.0 - 只是在 C# 中没有【参考方案3】:

如果它们被允许,则可以定义有用的 100% 类型安全(无内部类型转换)类或结构,如果它们的构造函数接受一个或多个 T 或 T 供应商,则它们的类型 T 是协变的。如果它们的构造函数接受一个或多个 T 消费者,则可以定义有用的 100% 类型安全的类或结构,这些类或结构相对于 T 是逆变的。除了使用“新”而不是使用静态工厂方法(很可能来自名称与接口名称相似的类)之外,我不确定类比接口有多大优势,但我可以当然可以看到使用不可变结构支持协方差的用例。

【讨论】:

以上是关于为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?的主要内容,如果未能解决你的问题,请参考以下文章

c#泛型的协变和逆变

Java泛型中的协变和逆变

Typescript 中的协变和逆变

了解 C# 中的协变和逆变接口

.Net中委托的协变和逆变详解

scala中泛型,协变和逆变