Why covariance and contravariance do not support value type

IEnumerable<T> is co-variant but it does not support value type, just only reference type. The below simple code is compiled successfully:

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

But changing from string to int will get compiled error:

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

The reason is explained in MSDN:

Variance applies only to reference types; if you specify a value type for a variant type parameter, that type parameter is invariant for the resulting constructed type.

I have searched and found that some questions mentioned the reason is boxing between value type and reference type. But it does not still clear up my mind much why boxing is the reason?

Could someone please give a simple and detailed explanation why covariance and contravariance do not support value type and how boxing affects this?

10771 次浏览

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

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

你可能想阅读 Eric Lippert 的 关于代表性和身份的博客文章来获得更多关于这个主题的一般信息。

编辑: 我自己重读了埃里克的博客文章,尽管 身份和表示法是相关联的,但它至少和表示法一样重要。特别是:

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

我认为一切都是从 Liskov代换原则的定义开始的,这个定义是:

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

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

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

即使我们将 一样“引用”分配给对象,它也会返回 false

如果你考虑一下基底形式,也许会更容易理解(尽管这确实是一个实现细节)。下面是一组字符串:

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

你可以认为 strings具有以下代表性:

[0] : string reference -> "A"
[1] : string reference -> "B"
[2] : string reference -> "C"

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

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

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

[0] : object reference -> "A"
[1] : object reference -> "B"
[2] : object reference -> "C"

表现形式是一样的。只是对引用进行了不同的处理; 您不能再访问 string.Length属性,但仍然可以调用 object.GetHashCode()。将其与 int 集合进行比较:

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

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

[0] : object reference -> 1
[1] : object reference -> 2
[2] : object reference -> 3

此转换需要的不仅仅是强制转换。

它确实可以归结为一个实现细节: 值类型的实现与引用类型的实现是不同的。

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

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

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

(*)这可能被视为同样的问题... ..。