关于c#:yield和List.AsEnumerable之间的区别

Difference between yield and List.AsEnumerable

到目前为止,我觉得很难理解收益率。但现在我明白了。现在,在一个项目中,如果我返回列表,Microsoft代码分析将对此发出警告。所以,通常我会做所有必要的逻辑部分,并将列表作为IEnumerable返回。我想知道两者的区别。意思是如果我做了收益回报或其他。

下面是一个非常简单的示例,通常代码有点复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static IEnumerable<int> getIntFromList(List<int> inputList)
{
    var outputlist = new List<int>();
    foreach (var i in inputList)
    {
        if (i %2 ==0)
        {
            outputlist.Add(i);
        }
    }

    return outputlist.AsEnumerable();
}

private static IEnumerable<int> getIntFromYeild(List<int> inputList)
{
    foreach (var i in inputList)
    {
        if (i%2 == 0)
        {
            yield return i;
        }
    }
}

我能看到的一个显著的好处是线条更少。但是还有其他的好处吗?我是否应该更改和更新返回IEnumerable以使用yield而不是list的函数?什么是做事情的最佳方式或更好的方式?

在这里,我可以在列表上使用简单的lambda表达式,但通常情况并非如此,这个示例专门用于理解编码的最佳方法。


你的第一个例子仍然是热切地做所有的工作,并在记忆中建立一个列表。事实上,对AsEnumerable()的调用是毫无意义的—您也可以使用:

1
return outputlist;

第二个例子是懒惰的——它只做客户机从中提取数据所需的工作。

显示差异的最简单方法可能是将Console.WriteLine调用放在if (i % 2 == 0)语句中:

1
Console.WriteLine("Got a value to return:" + i);

然后,如果在客户机代码中还放置了一个Console.WriteLine调用,例如

1
2
3
4
foreach (int value in getIntFromList(list))
{
    Console.WriteLine("Received value:" + value);
}

…您将看到,在第一个代码中,您首先看到所有"有值"行,然后看到所有"收到值"行。使用迭代器块,您将看到它们交错。

现在假设您的代码实际上在做一些昂贵的事情,并且您的列表非常长,并且客户机只需要前3个值…有了第一个代码,您将要做大量无关的工作。用懒惰的方法,你只做你需要做的工作,以"及时"的方式。第二种方法也不需要将所有结果都缓冲在内存中——同样,如果输入列表非常大,最终也会得到一个大的输出列表,即使一次只想使用一个值。


关于yield return的关键点是它没有被缓冲;迭代器块是一个状态机,随着数据的迭代而恢复。这使得它对于非常大的数据源(甚至无限的列表)非常方便,因为您可以避免在内存中有一个巨大的列表。

下面是一个定义良好的迭代器块,可以成功地迭代:

1
2
Random rand = new Random();
while(true) yield return rand.Next();

我们可以做如下的事情:

1
2
for(int i in TheAbove().Take(20))
    Console.WriteLine(i);

尽管很明显,任何迭代到结尾的东西(如Count()等)都将永远运行而不结束——这不是一个好主意。

在您的示例中,代码可能过于复杂。List版本可以是:

1
return new List<int>(inputList);

yield return有点取决于你想做什么:最简单的说,它可能只是:

1
foreach(var item in inputList) yield return item;

尽管很明显,这仍将关注源数据:对inputList的更改可能会破坏迭代器。如果你认为"那很好",那么坦率地说,你也可以:

1
return inputList;

如果这不好,在这种情况下,迭代器块有点多余,并且:

1
return new List<int>(inputList);

应该足够了。

为了完整性:AsEnumerable只返回原始源,类型为cast;它是:

1
return inputList;

版本。这是一个重要的考虑因素,因为它不保护您的列表,如果这是一个问题的话。所以如果你在想:

1
return someList.AsEnumerable(); // so they can only iterate it, not Add

那就不起作用了,一个邪恶的召唤者仍然可以这样做:

1
2
3
var list = (IList<int>) theAbove;
int mwahaahahaha = 42;
list.Add(mwahaahahaha);


区别很大:第二个(yield)产生的内存垃圾更少。第一个基本上是在内存中创建列表的副本。

大区别:如果调用方在示例2中操作原始列表,它将中断,在示例1中则不会中断(由于迭代副本)。

所以,这两个代码是不相同的,它们只是这样,当你不考虑边缘情况,只看直的情况,以及忽略所有的副作用。

因此,顺便说一句,由于没有分配第二个列表,示例2更快。


使用yield时,编译器生成迭代器模式的代码,该代码比预先生成的列表工作得更快。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
namespace Yield
{
    class UserCollection
    {
        public static IEnumerable Power()
        {
            return new ClassPower(-2);
        }

        private sealed class ClassPower : IEnumerable<object>, IEnumerable, IEnumerator<object>, IEnumerator, IDisposable
        {

            private int state;
            private object current;
            private int initialThreadId;

        public ClassPower(int state)
        {
            this.state = state;
            this.initialThreadId = Thread.CurrentThread.ManagedThreadId;
        }

        bool IEnumerator.MoveNext()
        {
            switch (this.state)
            {
                case 0:
                    this.state = -1;
                    this.current ="Hello world!";
                    this.state = 1;
                    return true;

                case 1:
                    this.state = -1;
                    break;
            }
            return false;
        }

        IEnumerator<object> IEnumerable<object>.GetEnumerator()
        {
            if ((Thread.CurrentThread.ManagedThreadId == this.initialThreadId) && (this.state == -2))
            {
                this.state = 0;
                return this;
            }
            return new UserCollection.ClassPower(0);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {      
            return (this as IEnumerable<object>).GetEnumerator();
        }

        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }

        void IDisposable.Dispose()
        {
        }

        object IEnumerator<object>.Current
        {
            get
            {
                return this.current;
            }
        }

        object IEnumerator.Current
        {
            get
            {
                return this.current;
            }
        }
    }
}

}


区别在于执行的时间。

在第一个示例中,函数中的代码在函数退出之前执行。整个列表被创建,然后作为IEnumerable返回。

在第二个示例中,当函数退出时,函数中的代码实际上不会运行。相反,当函数退出时,它返回一个IEnumerable,当您稍后迭代该IEnumerable时,代码就会执行。

特别是,如果在第二个示例中只迭代IEnumerable的前3个元素,那么for循环将只迭代足够的时间,以获得三个元素,而不是更多。