C# 4.0 中的泛型变异

16

在C# 4.0中,泛型的协变性被实现得这么好,以至于可以写出下面的代码而不会抛出异常(相对于C# 3.0来说):

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

[示例不可用:请参考Jon Skeet的答案]

最近我参加了一场会议,Jon Skeet给出了一个Generic Variance的很好的概述,但我不确定我是否完全理解它——当涉及到contra和co-variance时,我理解inout关键字的重要性,但我很好奇背后发生了什么。

执行此代码时,CLR会看到什么? 它是否将List<int>隐式转换为List<object>,还是说我们现在可以在派生类型和父类型之间进行转换只是List<T>实现中的内置功能?

出于兴趣,为什么以前的版本没有引入这个特性,主要优点是什么——即实际使用中的好处?

有关Generic Variance的更多信息,请参见此帖子(但问题已过时,需要寻找真正的最新信息)

3个回答

20
不,你的示例不能工作,原因如下:
  • 类(例如List<T>)是不变的;只有委托和接口是可变的
  • 为了使协变或逆变生效,接口必须只在一个方向使用类型参数(协变用于返回值,逆变用于参数)
  • 值类型不支持作为协变或逆变的类型参数 - 因此,例如从IEnumerable<int>IEnumerable<object>的转换是不存在的

(该代码在C# 3.0和4.0中都无法编译 - 没有异常。)

所以这个例子会起作用:

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
CLR只是使用引用,没有创建新的对象。因此,如果您调用objects.GetType(),仍然会得到List<string>
我认为它之前没有被引入是因为语言设计人员仍然需要解决如何公开它的细节问题 - 它自从CLR v2以来就一直存在。
好处与其他需要将一个类型用作另一个类型的情况相同。举个例子,如果你有一个实现IComparer<Shape>的东西,用于按面积比较形状,那么你不能将其用于对List<Circle>进行排序,这是荒谬的 - 如果它可以比较任意两个形状,那么它肯定可以比较任意两个圆。自C# 4以来,从IComparer<Shape>IComparer<Circle>会有逆变转换,因此您可以调用circles.Sort(areaComparer)

好的,我需要下载上周六的示例代码并自己试着玩一下。这个概念本身是有意义的,但我还需要理解如何在实际情况中应用它。非常感谢您的回复。 - Daniel May
@Daniel:没问题 - 很抱歉上周六我没有解释清楚 :) (不可否认,有很多内容需要涵盖...) - Jon Skeet
哦,完全不是那样的,Jon - 一切都发生得太快了,而我还没有接触过C# 4的任何新功能 - 我像个疯子一样在匆忙地记笔记。看来我得订购《深入理解C#》第二版 :) - Daniel May
@Daniel:好吧,我不会拒绝这个...但希望会很快上传会议的视频,这可能会有所帮助。 - Jon Skeet

15
一些额外的想法。
正如Jon和其他人正确指出的那样,我们没有对类进行差异处理,只有接口和委托。因此,在您的示例中,CLR什么也看不到;该代码无法编译。如果通过插入足够的强制转换使其编译,它会在运行时崩溃并显示错误的转换异常。
现在,询问差异处理背后的工作原理仍然是一个合理的问题。答案是:我们将此限制为参数化接口和委托类型的参考类型参数,以便在幕后无事发生。当您说
object x = "hello";

在幕后发生的事情是将字符串的引用不经修改地粘贴到类型为object的变量中。构成对字符串的引用的位是合法的位以引用对象,因此此处不需要发生任何事情。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;

这将返回一个字符串的引用的位。我们已经确定它们与对象兼容且不会改变。

为什么这个功能没有早些时候引入?因为我们有其他功能要做,而且预算有限。

最主要的好处是,从字符串序列到对象序列的转换可以“正常工作”。


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>,即使它应该 "just work"。协变和逆变使其 "just work"。

p.s. 真烦Skeet的回答正在吃掉我的声望分数。该死的你,Skeet! :-) 看起来他之前已经回答过这个问题了。


1
请明确一点:协变性和逆变性在CLI中一直得到支持(其中“一直”指的是“至少从v2开始”)。只是在C# 4.0之前,它没有在C#中被*暴露出来。但是,例如Eiffel.NET一直支持它,尽管据我所知,这些库没有得到正确注释。(实际上不知道为什么。即使过去无法在C#中表达这一点,BCL也可以写成使用一个协变和逆变接口列表的IL重写工具,以便翻转元数据中的正确位) - Jörg W Mittag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接