IEnumerable<T>
是 co-variant ,但它不支持值类型,只支持引用类型。以下简单代码编译成功:
IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;
但是从string
更改为int
会收到编译错误:
IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;
原因在MSDN中解释:
差异仅适用于参考类型;如果为变量类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。
我搜索过并发现有些问题提到原因是值类型和引用类型之间的装箱。但它仍然没有清楚我的想法为什么拳击是什么原因?
有人可以提供一个简单而详细的解释,为什么协方差和逆变不支持值类型以及拳击如何影响这个?
答案 0 :(得分:116)
基本上,当CLR可以确保它不需要对值进行任何代表性更改时,方差适用。引用看起来都一样 - 所以你可以使用IEnumerable<string>
作为IEnumerable<object>
,而不会改变表示形式;本机代码本身不需要知道你对这些值做了什么,只要基础设施保证它肯定是有效的。
对于不起作用的值类型 - 将IEnumerable<int>
视为IEnumerable<object>
,使用序列的代码必须知道是否执行装箱转换。
您可能希望阅读Eric Lippert的blog post on representation and identity以获取有关此主题的更多信息。
编辑:自己重读了Eric的博客文章,它至少与身份一样多,尽管这两者是相互关联的。特别是:这就是为什么接口和委托类型的协变和逆变转换要求所有不同类型的参数都是引用类型的原因。为确保变体引用转换始终保持身份,所有涉及类型参数的转换也必须是保持身份的。确保类型参数的所有非平凡转换都是保持身份的最简单方法是将它们限制为引用转换。
答案 1 :(得分:9)
如果你考虑底层表示,也许更容易理解(即使这确实是一个实现细节)。这是一个字符串集合:
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()
。将此与一组整数进行比较:
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
此转换需要的不仅仅是演员。
答案 2 :(得分:8)
我认为一切都始于LSP
(Liskov替代原则)的定义,其中包括:
如果q(x)是关于类型T的对象x可证明的属性,那么对于类型S的对象y,q(y)应该为真,其中S是T的子类型。
但是值类型(例如int
)不能替代object
中的C#
。
证明非常简单:
int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);
即使我们将相同的“引用”分配给对象,也会返回false
。
答案 3 :(得分:3)
它确实归结为一个实现细节:值类型的实现方式与引用类型不同。
如果强制将值类型视为引用类型(例如,通过接口引用它们,则可以获得差异)。
查看差异的最简单方法是简单地考虑Array
:将值类型数组连续(直接)放在内存中,其中Reference类型数组只有引用(指针)在记忆中连续;指向的对象是单独分配的。
另一个(相关)问题(*)是(几乎)所有参考类型具有相同的表示以用于方差目的,并且许多代码不需要知道类型之间的差异,因此可以进行共方差和反方差。 (并且很容易实现 - 通常只是省略额外的类型检查)。
(*)可能会出现同样的问题......