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.
我已经搜索并发现,提到的一些问题的原因是值类型和引用类型之间的装箱。但我还是不太清楚为什么拳击是原因?
有人能简单而详细地解释一下协变和逆变为什么不支持值类型以及装箱是如何影响这一点的吗?
另请参见Eric对我类似问题的回答:stackoverflow.com/question s/4096299/&hellip;
无法将值类型数组转换为参数对象的可能副本
基本上,当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.
谢谢你的回答,埃里克·利珀特的链接非常有价值,我还是有点困惑,当clr可以确保它不需要对值进行任何代表性的更改时,你的语句差异是否适用?这是应用差异的第一个标准?
@库恩格尔:从某种意义上说,这是一个实现细节,但我相信这是限制的根本原因。
我不认为这与表示有任何关系:int 不是object 的子类型,因此没有协变转换。相反,IEnumerable 将是协变返回类型。
@Andr&233;Caron:Eric的博客帖子在这里很重要-它不仅是表示,而且是身份保护。但是表示保留意味着生成的代码根本不需要关心这一点。
准确地说,由于int 不是object 的子类型,因此无法保留身份。事实上,一个代表性的变化是必需的,这只是一个后果。
它有助于从可快速复制的布局的角度进行思考,也就是说,我相信在这个讨论中,"表示"的用法指的是什么。由于值类型显示任意大小的内存映像(与每个引用类型的intptr.size不同),因此它们在通用代码实例化中用作类型参数(这必然包含基础实体的大小)将导致特定于实例的运行时代码无法合理地共享。
为什么int不是object的子类型?Int32继承自System.ValueType,后者继承自System.Object。
如果您考虑底层表示(即使这实际上是一个实现细节),可能更容易理解。以下是字符串集合:
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
此转换需要的不是强制转换。
装箱不仅仅是一个"实现细节"。装箱值类型的存储方式与类对象的存储方式相同,并且与类对象的行为(就外部世界而言)相同。唯一的区别是,在装箱值类型的定义中,this 指的是一个结构,它的字段覆盖存储它的堆对象的字段,而不是引用保存它们的对象。装箱的值类型实例无法获取对封闭堆对象的引用。
我认为一切都是从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 ,即使我们为对象指定相同的"引用"。
我认为你使用的是正确的原则,但没有证据可以证明:int 不是object 的一个子类型,因此该原则不适用。您的"证据"依赖于中间代表Integer ,它是object 的一个子类型,并且语言有一个隐式转换(object obj1=myInt; 实际上扩展到object obj1=new Integer(myInt) )。
语言处理类型之间的正确转换,但是ints行为与我们从对象的子类型期望的行为不一致。
我的观点是,int 不是object 的一个亚型。此外,LSP不适用,因为myInt 、obj1 和obj2 指的是三个不同的对象:一个int 和两个(隐藏的)Integer 。
@ ANDR&Y 233;C是不是Java。c的int 关键字是bcl的System.Int32 的别名,实际上是object 的一个子类型(System.Object 的别名)。实际上,int 的基类是System.ValueType ,而System.Object 的基类是System.Object 。尝试评估以下表达式并参见:typeof(int).BaseType.BaseType 。这里,ReferenceEquals 返回错误的原因是int 被装箱成两个单独的盒子,每个盒子的标识对于任何其他盒子都是不同的。因此,无论装箱值如何,两个装箱操作始终生成两个永远不相同的对象。
@allonguralnek:每个值类型(例如System.Int32 或List.Enumerator 实际上代表两种类型:存储位置类型和堆对象类型(有时称为"装箱值类型")。其类型派生自System.ValueType 的存储位置将保存前者;其类型同样保存后者的堆对象。在大多数语言中,前一种语言有一个扩大的转换,后一种语言有一个缩小的转换。请注意,虽然装箱的值类型与值类型存储位置具有相同的类型描述符,…
…它们在语义上更像可变的引用类型(即使假设为"不可变"的值类型在装箱时也是可变的)。例如,将List.Enumerator 类型的一个变量复制到另一个变量将复制其状态;将其强制转换到IEnumerator 将转换为装箱的等效变量。但是,将其复制到IEnumerator 类型的另一个变量将存储对原始装箱对象的引用,而不是复制其状态。
+一个很好的解释。
它可以归结为实现细节:值类型的实现方式与引用类型不同。
如果强制将值类型视为引用类型(即将它们框起来,例如通过接口引用它们),则可以获得差异。
最简单的方法就是考虑一个Array :值类型数组连续(直接)地放在内存中,其中作为引用类型数组的引用(指针)在内存中只有连续的;被指向的对象是单独分配的。
另一个(相关的)问题(*)是(几乎)所有引用类型都具有相同的表示,用于差异的目的,并且许多代码不需要知道类型之间的差异,因此co-和contra-差异是可能的(并且很容易实现——通常只是由于省略了额外的类型检查)。
(*)这可能是同一个问题……