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

时间:2012-03-13 16:41:34

标签: c# generics covariance

为什么最后一行不被允许?

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是一个不是从对象派生的值类型,因此协方差不起作用吗?

这是否意味着没有办法让这项工作:

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>()才能做到这一点?

3 个答案:

答案 0 :(得分:46)

  

为什么最后一行不被允许?

因为double是值类型而object是引用类型;协方差仅在两种类型都是引用类型时才有效。

  

这是因为double是一个不是从对象派生的值类型,因此协方差不起作用吗?

没有。 Double确实来自于对象。所有值类型都来自object。

现在你应该问的问题是:

  

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

因为谁做拳击?从double到object的转换必须 box double。假设您呼叫IEnumerator<object>.Current,这实际上是对IEnumerator<double>.Current实施的调用。调用者期望返回一个对象。被调用者返回一个双精度数。 执行装箱指令的代码在哪里,将IEnumerator<double>.Current返回的双精度转换为盒装双精度?

无处,就在那里,这就是为什么这种转换是非法的。对Current的调用将在评估堆栈上放置一个8字节的双精度数,并且消费者期望对评估堆栈上的盒装双精度进行四字节引用,因此消费者将会崩溃并死于错位堆栈和无效内存的引用。

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

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

现在,您调用一个辅助方法,该方法包含将双精度字符串从八字节双精度转换为引用的装箱指令。

更新:一位评论者指出,我已经提出了这个问题 - 也就是说,我已经回答了一个问题,假设存在一种解决问题的机制,就像解决原始问题所需要的那样难。 Cast<T>的实施如何解决了解是否包装的问题?

它就像这个草图一样。请注意,参数类型泛型:

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是引用类型还是值类型。 99%的时间当然是参考类型。

答案 1 :(得分:5)

要了解允许和不允许的内容,以及为什么事情会像他们那样行事,了解内幕发生的情况会很有帮助。对于每种值类型,都存在相应类型的类对象,它与所有对象一样,将继承自System.Object。每个类对象的数据包括一个32位字(x86)或64位长字(x64),用于标识其类型。但是,值类型存储位置不包含此类对象或对它们的引用,也不存在与它们一起存储的类型数据的单词。相反,每个原始值类型位置只保存表示值所需的位,每个结构值类型存储位置只保存该类型的所有公共和私有字段的内容。

当一个类型Double的变量复制到Object类型的变量时,会创建一个与Double关联的类对象类型的新实例,并复制所有字节。原始到那个新的类对象。虽然盒装 - Double类类型与Double值类型具有相同的名称,但这不会导致歧义,因为它们通常不能在相同的上下文中使用。值类型的存储位置包含原始位或字段组合,没有存储的类型信息;将一个这样的存储位置复制到另一个存储位置复制所有字节,从而复制所有公共和私有字段相比之下,从值类型派生的类型的堆对象是堆对象,并且行为类似于堆对象。虽然C#认为值类型存储位置的内容好像它们是Object的衍生物一样,但是这些存储位置的内容只是字节的集合,有效地在类型系统之外。由于它们只能通过知道字节代表的代码来访问,因此不需要将这些信息与存储位置本身一起存储。虽然在结构上调用GetType时拳击的必要性通常用GetType作为非阴影,非虚函数来描述,但真正的必要性源于值的内容这一事实类型存储位置(与位置本身不同)没有类型信息。

答案 2 :(得分:0)