关于C#:为什么协方差和逆变不支持值类型

Why covariance and contravariance do not support value type

IEnumerable是同变量,但不支持值类型,只支持引用类型。已成功编译以下简单代码:

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

但从string改为int会产生编译错误:

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

原因在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.

我已经搜索并发现,提到的一些问题的原因是值类型和引用类型之间的装箱。但我还是不太清楚为什么拳击是原因?

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


基本上,当clr可以确保它不需要对值进行任何表示性更改时,方差就适用了。所有引用看起来都是一样的,所以您可以使用IEnumerable作为IEnumerable,而不需要更改表示形式;只要基础结构保证它绝对有效,本地代码本身就不需要知道您对值做了什么。

对于值类型,如果将IEnumerable视为IEnumerable,则使用序列的代码必须知道是否执行装箱转换。

你可能想阅读埃里克·利珀特关于代表性和身份的博客文章,了解更多关于这个主题的内容。

编辑:我自己重读了埃里克的博客文章,它至少和代表性一样多,尽管两者都有联系。特别地:

This is why covariant and contravariant conversions of interface and delegate types require that all varying type arguments be of reference types. To ensure that a variant reference conversion is always identity-preserving, all of the conversions involving type arguments must also be identity-preserving. The easiest way to ensure that all the non-trivial conversions on type arguments are identity-preserving is to restrict them to be reference conversions.


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

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

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

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

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

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

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

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

表示是相同的。引用的处理方式不同;您不能再访问string.Length属性,但仍可以调用object.GetHashCode()。将其与一组整数进行比较:

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

要将其转换为IEnumerable,必须通过装箱将数据转换为ints:

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

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


我认为一切都是从LSP的定义(liskov替代原则)开始的,该定义是:

if q(x) is a property provable about objects x of type T then q(y) should be true for objects y of type S where S is a subtype of T.

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

1
2
3
4
int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

这将返回false,即使我们为对象指定相同的"引用"。


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

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

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

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

(*)这可能是同一个问题……