16

C# 4.0 中的 Generic Variance 的实现方式使得可以毫无例外地编写以下内容(这在 C# 3.0 中会发生):

 List<int> intList = new List<int>();
 List<object> objectList = intList; 

[示例非功能性:参见 Jon Skeet 的回答]

我最近参加了一个会议,Jon Skeet 对通用方差进行了出色的概述,但我不确定我是否完全理解它 - 我理解inout关键字在反方差和协方差方面的重要性,但我很好奇幕后发生的事情。

执行此代码时,CLR 会看到什么?它是隐式转换List<int>List<object>还是简单地内置,我们现在可以在派生类型之间转换为父类型?

出于兴趣,为什么在以前的版本中没有引入它,主要的好处是什么 - 即现实世界的使用?

有关通用差异的这篇文章的更多信息(但问题非常过时,正在寻找真实的最新信息)

4

3 回答 3

20

不,您的示例不起作用,原因有以下三个:

  • 类(例如List<T>)是不变的;只有委托和接口是变体
  • 为了使方差起作用,接口只能在一个方向上使用类型参数(in 表示逆变,out 表示协变)
  • 不支持值类型作为变量的类型参数 -例如,没有从IEnumerable<int>到的转换IEnumerable<object>

(代码在 C# 3.0 和 4.0 中都无法编译 - 也不例外。)

所以这起作用:

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

CLR 只使用未更改的引用 - 没有创建新对象。所以,如果你打电话objects.GetType(),你仍然会得到List<string>.

我相信它没有更早地引入,因为语言设计者仍然必须制定如何公开它的细节——它从 v2.1 开始就在 CLR 中。

好处与您希望能够将一种类型用作另一种类型的其他时间相同。使用我上周六使用的相同示例,如果您有一些工具IComparer<Shape>可以按区域比较形状,那么您不能使用它来对 a 进行排序真是太疯狂了List<Circle>- 如果它可以比较任何两个形状,它当然可以比较任何两个界。从 C# 4 开始,将有一个从IComparer<Shape>to的逆变转换,IComparer<Circle>因此您可以调用circles.Sort(areaComparer).

于 2010-02-05T15:04:55.213 回答
15

一些额外的想法。

执行此代码时 CLR 看到什么

正如 Jon 和其他人正确指出的那样,我们没有对类进行变体,只有接口和委托。因此,在您的示例中,CLR 什么也看不到;该代码无法编译。如果您通过插入足够的强制转换来强制它编译,它会在运行时崩溃并出现错误的强制转换异常。

现在,询问变异在幕后如何起作用仍然是一个合理的问题。答案是:我们将其限制为参数化接口和委托类型的引用类型参数的原因是为了在幕后没有任何事情发生当你说

object x = "hello";

幕后发生的事情是对字符串的引用被卡在对象类型的变量中而没有修改。构成对字符串的引用的位是对对象的引用的合法位,因此这里不需要发生任何事情。CLR 只是停止将这些位视为引用字符串,并开始将它们视为引用对象。

当你说:

IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = e1;

一样。没发生什么事。引用字符串枚举器的位与引用对象枚举器的位相同。当你进行演员表时,会有更多的魔法发挥作用,比如:

IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = (IEnumerator<object>)(object)e1;

现在 CLR 必须生成一个检查 e1 确实实现了该接口,并且该检查必须聪明地识别差异。

但是我们可以摆脱变体接口只是无操作转换的原因是因为常规分配兼容性就是这样。你打算用 e2 做什么?

object z = e2.Current;

这将返回作为对字符串的引用的位。我们已经确定它们与对象兼容而无需更改。

为什么不早点介绍呢?我们还有其他功能要做,而且预算有限。

主要的好处是什么?从字符串序列到对象序列的转换“正常工作”。

于 2010-02-10T00:42:56.620 回答
8

出于兴趣,为什么在以前的版本中没有引入

.NET 的第一个版本 (1.x) 根本没有泛型,因此泛型差异相去甚远。

应该注意的是,在所有版本的 .NET 中,都存在数组协方差。不幸的是,这是不安全的协方差:

Apple[] apples = new [] { apple1, apple2 };
Fruit[] fruit = apples;
fruit[1] = new Orange(); // Oh snap! Runtime exception! Can't store an orange in an array of apples!

C# 4 中的协变和逆变换是安全的,并且可以防止这个问题。

主要的好处是什么 - 即现实世界的使用?

很多时候,在代码中,您调用的 API 需要放大类型的 Base(例如IEnumerable<Base>),但您得到的只是放大类型的 Derived(例如IEnumerable<Derived>)。

在 C# 2 和 C# 3 中,您需要手动转换为IEnumerable<Base>,即使它应该“正常工作”。协方差和反方差使其“正常工作”。

ps 太糟糕了,Skeet 的回答是吃掉我所有的代表点。该死的,斯基特!:-) 看起来他以前回答过这个问题

于 2010-02-05T15:09:43.367 回答