22

到现在为止,收益率是我觉得很难理解的东西。但现在我掌握了它。现在,在一个项目中,如果我返回 List,Microsoft 代码分析会给出警告。所以,通常我会做所有必要的逻辑部分并将列表作为 IEnumerable 返回。我想知道两者的区别。表示我是否在进行收益回报或其他方式。

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

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

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

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

4

5 回答 5

47

您的第一个示例仍在热切地完成所有工作并在内存中建立一个列表。事实上,调用AsEnumerable()是没有意义的——你不妨使用:

return outputlist;

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

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

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 个值……使用您的第一个代码,您将做大量不相关的工作。使用惰性方法,您只需以“及时”的方式完成所需的工作。第二种方法也不需要将所有结果缓存在内存中 - 同样,如果输入列表非常大,您最终也会得到一个很大的输出列表,即使您只想使用单个值一次。

于 2013-01-21T07:26:57.367 回答
17

关键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可能会破坏迭代器。如果您认为“那很好”,那么坦率地说,您不妨:

return inputList;

如果这不好,在这种情况下,迭代器块有点矫枉过正,并且:

return new List<int>(inputList);

应该足够了。

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

return inputList;

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

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

那么这将不起作用;一个邪恶的来电者仍然可以这样做:

var list = (IList<int>) theAbove;
int mwahaahahaha = 42;
list.Add(mwahaahahaha);
于 2013-01-21T07:29:18.403 回答
1

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

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

因此,这两个代码并不相同,它们只是在您不考虑边缘情况而只看直接情况并忽略所有副作用时才如此。

结果,顺便说一句,示例 2 由于不分配第二个列表而更快。

于 2013-01-21T07:27:30.150 回答
0

区别在于执行的时间。

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

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

特别是如果您仅在第二个示例中迭代 IEnumerable 的前 3 个元素,则 for 循环将仅迭代足够的次数来获得三个元素,而不是更多。

于 2013-01-21T07:27:36.347 回答
0

当您使用 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;
            }
        }
    }
}

}

于 2013-01-21T07:33:00.940 回答