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

d--*_*--b 28 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
Run Code Online (Sandbox Code Playgroud)

这是因为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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我需要自己写这个IMyInterface<T>.Cast<U>()来做吗?

Eri*_*ert 46

为什么不允许最后一行?

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

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

不,Double确实来自于对象.所有值类型都来自对象.

现在你应该问的问题是:

为什么协方差无法转换IEnumerable<double>IEnumerable<object>

因为谁做拳击?从双到对象必须在转换的两倍.假设你有一个调用,IEnumerator<object>.Current那就是"真的"调用一个实现IEnumerator<double>.Current.调用者期望返回一个对象.被调用者返回一个双精度数.执行装箱指令的代码在哪里将返回的double IEnumerator<double>.Current转换为盒装双精度?

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

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

IEnumerable<object> objects2 = doubleenumerable.Cast<object>();
Run Code Online (Sandbox Code Playgroud)

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

更新:一位意见提供者指出,我已经提出了这个问题 - 也就是说,我已经回答了一个问题,假设存在一种解决问题的机制,就像解决原始问题所需要的那样难.如何实现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;
}
Run Code Online (Sandbox Code Playgroud)

确定从对象到T的转换是否为拆箱转换或引用转换的责任推迟到运行时.抖动知道T是引用类型还是值类型.99%的时间当然是参考类型.

  • @JoeTuskan:当我有话要说120个字符并且是普遍感兴趣的时候,我会的.期待漫长的等待. (17认同)
  • 关闭主题,但我希望你发推文. (6认同)
  • @EricLippert,也许是关于如何使用图论来提高洗衣效率的博客链接. (3认同)
  • @FelixK:Cast扩展方法首先很容易实现; 如果对象已经是所需类型的序列,那么我们使用它.否则,它使用非通用IEnumerable将序列视为对象序列,并将每个对象强制转换为T.抖动负责确定从对象到T的强制转换是否要实现为拆箱转换或参考转换. (2认同)

sup*_*cat 5

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

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