为什么不能将IEnumerable <struct>转换为IEnumerable <object>?

为什么最后一行不允许?

IEnumerable<double> doubleenumerable = new List<double> { 1, 2 }; IEnumerable<string> stringenumerable = new List<string> { "a", "b" }; IEnumerable<object> objects1 = stringenumerable; // OK IEnumerable<object> objects2 = doubleenumerable; // Not allowed 

这是因为double是不是从对象派生的值types,因此协变不起作用?

这是否意味着没有办法做到这一点:

 public interface IMyInterface<out T> { string Method(); } public class MyClass<U> : IMyInterface<U> { public string Method() { return "test"; } } public class Test { public static object test2() { IMyInterface<double> a = new MyClass<double>(); IMyInterface<object> b = a; // Invalid cast! return b.Method(); } } 

而我需要写我自己的IMyInterface<T>.Cast<U>()来做到这一点?

为什么最后一行不允许?

因为double是一个值types,而object是一个引用types; 协方差只适用于两种types都是参考types。

这是因为double是不是从对象派生的值types,因此协变不起作用?

没有双重从对象派生。 所有值types都从对象派生。

现在你应该问的问题是:

为什么协变不能将IEnumerable<double>转换为IEnumerable<object>

因为谁是拳击 ? 从double到object的转换必须包含double。 假设你有一个对IEnumerator<object>.Current的调用,它实际上是一个对IEnumerator<double>.Current的实现的调用。 调用者期望一个对象被返回。 被调用者返回一个double。 在哪里是做的拳击指令的代码,将IEnumerator<double>.Current返回的IEnumerator<double>.Current变成一个盒装双?

这是不通的 ,那就是这个地方,这就是为什么这个转换是非法的。 对Current的调用将在评估堆栈上放置一个八字节的双精度值,并且消费者将期望在评估堆栈中对四个字节的双精度值进行引用,所以消费者将会崩溃并死亡没有alignment的堆栈和对无效内存的引用。

如果你想要代码框执行,那么它必须在某个时刻,而你是写这个代码的人。 最简单的方法是使用Cast<T>扩展方法:

 IEnumerable<object> objects2 = doubleenumerable.Cast<object>(); 

现在调用一个辅助方法,该方法包含将double从8字节double转换为reference的装箱指令。

更新:一位意见提供者指出,我已经提出这个问题 – 也就是说,我已经回答了一个问题,假设存在一个解决问题的机制,就像解决原始问题一样困难。 Cast<T>的实现如何解决知道是否装箱的问题?

它就像这个草图。 请注意,参数types不是通用的:

 public static IEnumerable<T> Cast<T>(this IEnumerable sequence) { if (sequence == null) throw ... if (sequence is IEnumerable<T>) return sequence as IEnumerable<T>; return ReallyCast<T>(sequence); } private static IEnumerable<T> ReallyCast<T>(IEnumerable sequence) { foreach(object item in sequence) yield return (T)item; } 

确定从对象到T的转换是拆箱转换还是引用转换的责任推迟到运行时。 抖动知道T是引用types还是值types。 99%的时间当然是一个参考types。

为了理解什么是允许的,什么是不允许的,为什么事情的行为和它们一样,理解下面的内容是很有帮助的。 对于每个值types,都存在相应types的类对象,像所有对象一样,将从System.Objectinheritance。 每个类对象都包含一个32位字(x86)或64位长字(x64),用于标识其types。 然而,值types的存储位置并不包含这样的类对象或对它们的引用,也不具有与它们一起存储的types数据字。 相反,每个原始值types的位置只需保存表示一个值所需的位,而每个struct-value-type存储位置只保存该types的所有公有和私有字段的内容。

Doubletypes的variables复制到Objecttypes的variables时,将创build与Double关联的类对象types的新实例,并将原始的所有字节复制到该新的类对象中。 尽pipe盒装Doubletypes与Double值types具有相同的名称,但这不会导致歧义,因为它们通常不能在相同的上下文中使用。 值types的存储位置保存原始位或字段的组合,没有存储的types信息; 复制一个这样的存储位置到另一个复制所有字节,并因此复制所有的公共和私人领域。 相比之下,从值types派生的types的堆对象是堆对象,并且像堆对象一样。 尽pipeC#将值types存储位置的内容看作是Object衍生物,但是在这种情况下,这些存储位置的内容只是字节的集合,实际上在types系统之外。 由于只能通过知道字节代表的代码访问,所以不需要将这些信息与存储位置本身一起存储。 尽pipe在结构体上调用GetType时必须使用装箱方法,但GetType是一个非阴影的非虚函数,所以真正的必要性源于这样一个事实:值types存储位置的内容(与位置本身)没有types信息。