【问题标题】:Why covariance and contravariance do not support value type为什么协变和逆变不支持值类型
【发布时间】:2018-12-24 14:16:05
【问题描述】:

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中有解释:

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

我搜了一下发现有些问题提到的原因是值类型和引用类型之间装箱。但是我仍然不太清楚为什么拳击是原因?

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

【问题讨论】:

标签: c# .net c#-4.0 covariance contravariance


【解决方案1】:

基本上,当 CLR 可以确保它不需要对值进行任何表示性更改时,差异就会应用。引用看起来都一样 - 因此您可以使用 IEnumerable&lt;string&gt; 作为 IEnumerable&lt;object&gt; 而不会更改表示形式;原生代码本身根本不需要知道你对这些值做了什么,只要基础设施保证它肯定是有效的。

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

您可能想阅读 Eric Lippert 的 blog post on representation and identity,了解有关此主题的更多信息。

编辑:我自己重读了 Eric 的博客文章后,它至少与 身份 一样多,尽管两者是相互关联的。特别是:

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

【讨论】:

  • @CuongLe:从某种意义上说,这是一个实现细节,但我相信这是限制的根本原因。
  • @AndréCaron:Eric 的博文在这里很重要——它不仅仅是代表,也是身份保护。但是表示保留意味着生成的代码根本不需要关心这个。
  • 准确地说,无法保留身份,因为int 不是object 的子类型。需要进行代表性更改的事实只是其结果。
  • int 怎么不是对象的子类型? Int32 继承自 System.ValueType,后者继承自 System.Object。
  • @DavidKlempfner 我认为@AndréCaron 的评论措辞不当。任何值类型,例如Int32,都有两种表示形式,“装箱”和“未装箱”。编译器必须插入代码才能从一种形式转换为另一种形式,即使这在源代码级别通常是不可见的。实际上,底层系统仅将“盒装”形式视为object 的子类型,但只要将值类型分配给兼容接口或object 类型的东西,编译器就会自动处理此问题。
【解决方案2】:

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

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&lt;object&gt;,必须通过装箱整数来转换数据:

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

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

【讨论】:

  • 拳击不仅仅是一个“实施细节”。盒装值类型的存储方式与类对象相同,并且就外界所知,其行为类似于类对象。唯一的区别是在装箱值类型的定义中,this 指的是一个结构,其字段覆盖存储它的堆对象的字段,而不是指包含它们的对象。装箱值类型实例没有干净的方法来获取对封闭堆对象的引用。
【解决方案3】:

我认为一切都始于LSP(Liskov Substitution Principle)的定义,它适用于:

如果 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);

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

【讨论】:

  • 我认为您使用了正确的原则,但没有证据可以证明:int 不是object 的子类型,因此该原则不适用。您的“证明”依赖于中间表示 Integer,它是 object 的子类型,语言对其进行了隐式转换(object obj1=myInt; 实际上扩展为 object obj1=new Integer(myInt);)。
  • 该语言负责类型之间的正确转换,但整数行为与我们期望的对象子类型的行为不对应。
  • 我的重点是int 不是object 的子类型。此外,LSP 不适用,因为 myIntobj1obj2 指的是三个不同的对象:一个 int 和两个(隐藏的)Integers。
  • @André:C# 不是 Java。 C# 的 int 关键字是 BCL 的 System.Int32 的别名,它实际上是 object 的子类型(System.Object 的别名)。实际上,int 的基类是 System.ValueType,而其基类是 System.Object。尝试评估以下表达式并查看:typeof(int).BaseType.BaseTypeReferenceEquals 在这里返回 false 的原因是 int 被装箱成两个单独的盒子,每个盒子的身份对于任何其他盒子都是不同的。因此,无论装箱值如何,两次装箱操作总是产生两个永远不会相同的对象。
  • @AllonGuralnek:每个值类型(例如System.Int32List&lt;String&gt;.Enumerator)实际上代表两种类型:存储位置类型和堆对象类型(有时称为“装箱值”类型”)。类型派生自System.ValueType 的存储位置将保存前者;类型相同的堆对象将持有后者。在大多数语言中,存在从前者到后者的扩大转换,以及从后者到前者的缩小转换。请注意,虽然装箱值类型与值类型存储位置具有相同的类型描述符,但 ...
【解决方案4】:

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

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

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

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

(*) 可以看出是同一个问题...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-24
    • 2011-02-02
    • 2013-11-29
    • 1970-01-01
    • 2010-10-22
    相关资源
    最近更新 更多