问题 为什么数组像IEnumerable一样被忽略延迟执行?


我今天遇到了这个问题而且我不明白发生了什么:

enum Foo
{
    Zero,
    One,
    Two
}

void Main()
{
    IEnumerable<Foo> a = new Foo[]{ Foo.Zero, Foo.One, Foo.Two};
    IEnumerable<Foo> b = a.ToList();

    PrintGeneric(a.Cast<int>());
    PrintGeneric(b.Cast<int>());

    Print(a.Cast<int>());
    Print(b.Cast<int>());
}

public static void PrintGeneric<T>(IEnumerable<T> values){
    foreach(T value in values){
        Console.WriteLine(value);
    }
}

public static void Print(IEnumerable values){
    foreach(object value in values){
        Console.WriteLine(value);
    }
}

输出:

0
1
2
0
1
2
Zero
One
Two
0
1
2

我知道Cast()将导致延迟执行,但它看起来像将它转换为IEnumerable导致延迟执行丢失,并且只有当实际的实现集合是一个数组时。

为什么枚举值中的值 Print 方法导致 enum 被投射到 int 为了 List<Foo> 收藏,但不是 Foo[]


1284
2017-11-04 16:22


起源

PrintGeneric中Console.WriteLine(value.GetType()。Name)的输出是什么? - SimSimY
@SimSimY Int32。它是Int32的 Print 当。。。的时候 List<Foo>.Cast<int>() 传入,但是 Foo 当。。。的时候 Foo[].Cast<int>() 过去了。 - Daryl
这与延迟执行有什么关系? - Servy
@Servy我错误地认为延迟执行被忽略了。 - Daryl
@Daryl正在推迟什么操作,以及如何在以后而不是之前执行它是相关的?这些是你应该在延期执行相关问题中问自己的问题。 - Servy


答案:


这是因为在面对意外的CLR转换时,不幸的是有一点优化。

在CLR级别,有一个 参考 转换自 Foo[] 至 int[]  - 实际上你根本不需要投射每个对象。在C#级别上并非如此,但它处于CLR级别。

现在, Cast<> 包含一个优化,说“如果我已经在处理正确类型的集合,我可以返回相同的引用” - 有效地像这样:

if (source is IEnumerable<T>)
{
    return source;
}

所以 a.Cast<int> 回报 a,这是一个 Foo[]。传递给你时没关系 PrintGeneric,因为那时有一个隐含的转换 T 在里面 foreach 循环。编译器知道的类型 IEnumerator<T>.Current 是 T,所以相关的堆栈槽是类型 T。当将值视为一个时,每类型参数JIT编译的代码将“做正确的事” int 而不是作为一个 Foo

但是,当您将数组作为传递时 IEnumerableCurrent 物业 IEnumerator 只是类型 object,因此每个值都将被装箱并传递给 Console.WriteLine(object)  - 盒装对象将是类型 Foo不是 int

这里有一些示例代码来展示这个的第一部分 - 其余的有点简单,我相信,一旦你已经过去了:

using System;
using System.Linq;

enum Foo { }

class Test
{
    static void Main()
    {
        Foo[] x = new Foo[10];
        // False because the C# compiler is cocky, and "optimizes" it out
        Console.WriteLine(x is int[]);

        // True because when we put a blindfold in front of the compiler,
        // the evaluation is left to the CLR
        Console.WriteLine(((object) x) is int[]);

        // Foo[] and True because Cast returns the same reference back
        Console.WriteLine(x.Cast<int>().GetType());
        Console.WriteLine(ReferenceEquals(x, x.Cast<int>()));
    }
}

如果你试图介入,你会看到同样的事情 uint[] 和 int[] 顺便一提。


14
2017-11-04 16:28



如果它更加一致,这实际上不会那么糟糕。在这种情况下, List 和 array 应该满足 is IEnumerable<T> 条件,但只有一个表现出优化。 - Joel Coehoorn
@JoelCoehoorn:这是特别的一部分 排列 CLR中的等价,而非一般 IEnumerable<T> 等价。 - Jon Skeet
在我的实际生产代码中,我有一个接受IEnumerable <T>的方法,其中T是一个枚举。然后我打电话 Cast<int>在IEnumerable <T>上传递给另一个方法时。是解决方案 ToList() 在上面? - Daryl
怎么能 PrintGeneric 呼叫 Console.WriteLine(int)?由于没有类型约束 T 在 PrintGeneric,不会 WriteLine 调用编译为 Console.WriteLine(object)? - Heinzi
@Heinzi:对不起,你是对的 - 当然会的。但隐含的演员 T 将转换个人价值。将编辑我的答案。 - Jon Skeet