为什么不能将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.Object
inheritance。 每个类对象都包含一个32位字(x86)或64位长字(x64),用于标识其types。 然而,值types的存储位置并不包含这样的类对象或对它们的引用,也不具有与它们一起存储的types数据字。 相反,每个原始值types的位置只需保存表示一个值所需的位,而每个struct-value-type存储位置只保存该types的所有公有和私有字段的内容。
将Double
types的variables复制到Object
types的variables时,将创build与Double
关联的类对象types的新实例,并将原始的所有字节复制到该新的类对象中。 尽pipe盒装Double
types与Double
值types具有相同的名称,但这不会导致歧义,因为它们通常不能在相同的上下文中使用。 值types的存储位置保存原始位或字段的组合,没有存储的types信息; 复制一个这样的存储位置到另一个复制所有字节,并因此复制所有的公共和私人领域。 相比之下,从值types派生的types的堆对象是堆对象,并且像堆对象一样。 尽pipeC#将值types存储位置的内容看作是Object
衍生物,但是在这种情况下,这些存储位置的内容只是字节的集合,实际上在types系统之外。 由于只能通过知道字节代表的代码访问,所以不需要将这些信息与存储位置本身一起存储。 尽pipe在结构体上调用GetType
时必须使用装箱方法,但GetType
是一个非阴影的非虚函数,所以真正的必要性源于这样一个事实:值types存储位置的内容(与位置本身)没有types信息。
这种types的差异只支持参考types。 见http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx