在他的书 C#in Depth 中,Jon Skeet试图回答以下问题:
为什么我无法将
List<string>
转换为List<object>
?
为了解释它,他开始使用代码片段,其中包括以下两行:
Animal[] animals = new Cat[5]; //Ok. Compiles fine!
List<Animal> animals = new List<Cat>(); //Compilation error!
正如评论所述,第一个compiles fine,但第二个给出了compilation error。我真的不明白原因。 Jon Skeet解释了这一点,只说第一个编译,因为在.NET中,数组是协变的,第二个不编译,因为泛型不是协变的(相反,它们是不变的)。此外,数组在.NET中是协变的,因为数组在Java和.NET中是协变的,因此它与Java类似。
我对这个简短的答案并不完全满意。我想更详细地了解它,并深入了解编译器如何处理差异,以及它如何生成IL和所有。
另外,如果我写(摘自书本身):
Animal[] animals = new Cat[5]; //Ok. Compiles fine!
animals.Add(new Turtle()); //this too compiles fine!
编译很好,但在运行时失败。如果它必须在运行时失败(这意味着我写的东西应该没有意义),那为什么它首先编译?我可以在我的代码中使用实例animals
,并且运行时没有运行时错误吗?
答案 0 :(得分:10)
阵列有一个奇怪的历史,在.NET中存在差异。在2.0版本的CLR中添加了对方差的适当支持 - 以及C#4.0中的语言。然而,数组总是具有协变行为。
Eric Lippert在blog post中详细介绍了这一点。
有趣的一点:
自C#1.0以来,元素类型为引用类型的数组是协变的。这完全合法:
动物[]动物=新长颈鹿[10];
由于Giraffe小于Animal,而“make a array of”是对类型的协变操作,Giraffe []小于Animal [],因此一个实例适合该变量。
不幸的是,这种特殊的协方差被打破了。 它被添加到CLR中,因为Java需要它,而CLR设计者希望能够支持类似Java的语言。然后我们将它添加到C#,因为它在CLR中。这个决定在当时颇具争议,我对此并不十分满意,但现在我们无能为力。
我自己强调的重点。
答案 1 :(得分:7)
如果它必须在运行时失败,那么为什么它首先编译?
这正是阵列协方差已破坏的原因。我们允许它的事实意味着我们允许忽略在编译时被捕获的错误,而不是在运行时被捕获。
我对这个简短的答案并不完全满意。我想更详细地了解它,并深入了解编译器如何处理差异......
编译器可以轻松处理差异。编译器有一大堆代码,用于确定一种类型何时与另一种类型兼容。部分代码处理数组到数组的转换。该代码的一部分涉及泛型到泛型的转换。代码是规范相关行的直接翻译。
......以及它如何产生IL和所有。
无需为协变阵列转换生成任何IL。 为什么我们需要为两个参考兼容类型之间的转换生成IL?这就像询问我们生成的用于将字符串转换为对象的IL。 字符串已经是对象,因此没有生成代码。
答案 2 :(得分:0)
我认为Jon Skeet解释得相当不错,但如果您需要“Ahah”时刻,请考虑 泛型如何工作。
像List&lt;&gt;这样的通用类对于大多数目的而言,是对外治疗 作为一个普通的班级。例如当你说编译器说
List<string>()
时ListString()
(包含字符串)并且编译器不能智能 足以通过强制转换项目将ListString转换为ListObject 它的内部集合。
从阅读MSDN blog post开始,当 使用委托和接口时, .NET 4.0 支持协方差和逆变。它在第二句中也提到了埃里克的文章。