yield和List.AsEnumerable的区别

本文关键字:区别 AsEnumerable List yield | 更新日期: 2023-09-27 17:54:39

直到现在我都觉得Yield很难理解。但现在我掌握了它。现在,在一个项目中,如果我返回List,微软代码分析将给出一个警告。所以,通常我会做所有必要的逻辑部分,并返回IEnumerable列表。我想知道两者的区别。意思是如果我在做yield return或其他。

这是我展示的一个非常简单的例子,通常代码有点复杂。

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;
        }
    }
}

我可以看到的一个显著的好处是更少的行。但还有其他好处吗?我应该改变和更新我的函数返回ienumable使用yield而不是列表?什么是最好的或更好的做事方式?

在这里,我可以在List上使用简单的lambda表达式,但通常不是这样,这个例子是专门用来理解编码的最佳方法的。

yield和List.AsEnumerable的区别

您的第一个示例仍然在急切地执行所有工作并在内存中构建列表。实际上,对AsEnumerable()的调用是没有意义的——您还不如使用:

return outputlist;

第二个例子是lazy——当客户端从提取数据时,它只做它需要做的工作。

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

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

然后,如果您在客户端代码中放置Console.WriteLine调用,例如

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

…你会看到,在你的第一个代码中,你首先看到所有的"Got a value"行,然后是所有的"Received value"行。在迭代器块中,你会看到它们交错在一起。

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

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

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

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

我们可以这样做:

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

虽然很明显,任何迭代到结尾的(如Count()等)将永远运行而不结束-这不是一个好主意。

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

return new List<int>(inputList);

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

foreach(var item in inputList) yield return item;

,尽管很明显它仍然会查看源数据:更改inputList可能会破坏迭代器。如果你认为"that's fine",那么坦白地说,你不妨这样做:

return inputList;

如果不适合,在这种情况下,迭代器块有点多余,而:

return new List<int>(inputList);

应该足够了。

为完整起见:AsEnumerable只返回原始源,类型强制转换;它是:

return inputList;

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

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

工作;一个邪恶的调用者仍然可以这样做:

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

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

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

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

结果,顺便说一下。例2由于没有分配第二个列表而更快。

区别在于执行的时间。

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

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

特别是在第二个例子中,如果你只迭代IEnumerable的前3个元素,for循环只迭代足够的次数来获得3个元素,而不是更多。

当你使用yield时,编译器会生成迭代器模式的代码,这将比预生成的列表更快。它是这样的:

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;
            }
        }
    }
}

}