158

IEnumerable<T>协变的,但它不支持值类型,只支持引用类型。下面的简单代码编译成功:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

但是从stringto更改int会得到编译错误:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

原因在MSDN中有解释:

差异仅适用于引用类型;如果为变体类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。

我搜索了一下,发现有些问题提到的原因是value type 和 reference type 之间的装箱。但是我还是不太清楚为什么拳击是这个原因?

有人可以简单详细地解释为什么协变和逆变不支持值类型以及装箱如何影响这一点?

4

4 回答 4

130

基本上,当 CLR 可以确保它不需要对值进行任何表示性更改时,就会应用差异。引用看起来都一样——所以你可以使用IEnumerable<string>as anIEnumerable<object>而不会改变表示形式;本机代码本身根本不需要知道你对这些值做了什么,只要基础设施保证它肯定是有效的。

对于值类型,这是行不通的——要将 anIEnumerable<int>视为 an IEnumerable<object>,使用序列的代码必须知道是否执行装箱转换。

您可能想阅读 Eric Lippert 的关于表示和身份的博客文章,以了解有关此主题的更多信息。

编辑:我自己重读了 Eric 的博客文章后,它至少与代表一样多的身份,尽管两者是联系在一起的。尤其:

这就是为什么接口和委托类型的协变和逆变转换要求所有不同类型的参数都是引用类型。为确保变体引用转换始终保持身份,所有涉及类型参数的转换也必须保持身份。确保类型参数上的所有重要转换都保持身份的最简单方法是将它们限制为引用转换。

于 2012-09-17T07:37:48.147 回答
10

如果您考虑底层表示,可能更容易理解(即使这确实是一个实现细节)。这是字符串的集合:

IEnumerable<string> strings = new[] { "A", "B", "C" };

您可以将strings视为具有以下表示:

[0] : 字符串引用 -> "A"
[1] : 字符串引用 -> "B"
[2] : 字符串引用 -> "C"

它是三个元素的集合,每个元素都是对字符串的引用。您可以将其转换为对象集合:

IEnumerable<object> objects = (IEnumerable<object>) strings;

基本上它是相同的表示,除了现在引用是对象引用:

[0]:对象引用->“A”
[1]:对象引用->“B”
[2]:对象引用->“C”

表示是一样的。引用的处理方式不同;您无法再访问该string.Length属性,但您仍然可以调用object.GetHashCode(). 将此与整数集合进行比较:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0] : 整数 = 1
[1] : 整数 = 2
[2] : 整数 = 3

要将其转换为IEnumerable<object>数据,必须通过对整数进行装箱来转换:

[0] : 对象引用 -> 1
[1] : 对象引用 -> 2
[2] : 对象引用 -> 3

这种转换需要的不仅仅是演员。

于 2012-09-17T07:51:17.017 回答
8

我认为一切都始于LSP(Liskov Substitution Principle)的定义,其中包括:

如果 q(x) 是关于 T 类型的对象 x 的可证明属性,那么 q(y) 应该对 S 类型的对象 y 为真,其中 S 是 T 的子类型。

但是值类型,例如int不能替代objectin C#。证明很简单:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

false即使我们为对象分配了相同的“引用”,它也会返回。

于 2012-09-17T07:40:25.183 回答
3

它确实归结为实现细节:值类型的实现方式与引用类型不同。

如果您强制将值类型视为引用类型(即将它们装箱,例如通过接口引用它们),您可以获得差异。

查看差异的最简单方法是简单地考虑一个Array:Value 类型数组连续(直接)放在内存中,而 Reference 类型数组只有在内存中连续引用(指针);指向的对象是单独分配的。

另一个(相关)问题(*)是(几乎)所有引用类型出于方差目的都具有相同的表示形式,并且很多代码不需要知道类型之间的差异,因此协方差和反方差是可能的(并且很容易实现——通常只是省略了额外的类型检查)。

(*) 这可能被视为同一个问题...

于 2012-09-17T10:02:42.220 回答