到目前为止,我很难理解收益率。但现在我抓住了它。现在,在一个项目中,如果我返回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表达式,但通常情况并非如此,这个例子专门用于理解编码的最佳方法。
答案 0 :(得分:46)
你的第一个例子仍然是急切地并在内存中建立一个列表。实际上,对AsEnumerable()
的调用毫无意义 - 您可以使用:
return outputlist;
你的第二个例子是 lazy - 当客户端从中提取数据时,它只能完成所需的工作量。
显示差异的最简单方法可能是在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);
}
...你会看到你的第一个代码,你首先看到所有“有一个值”的行,然后是所有“已接收的值”行。使用迭代器块,您将看到它们交错。
现在假设您的代码实际上做了一些昂贵的事情,而且您的列表很长,并且客户端只需要前3个值...使用您的第一个代码,您将要做的一堆无关紧要的工作。使用懒惰的方法,您只需要以“及时”的方式完成所需的工作。第二种方法也不需要将所有结果缓冲到内存中 - 再次,如果输入列表非常大,您最终也会得到一个大的输出列表,即使您只想使用单个值一段时间。
答案 1 :(得分: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
只返回原始来源 type 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);
答案 2 :(得分:1)
差异很大:第二个(yield)会产生更少的内存垃圾。第一个基本上是在内存中创建列表的副本。
差别很大:如果调用者操作样本2中的原始列表,它将会中断,在样本1中它不会(由于迭代副本)。
所以,这两个代码并不相同,只有当你不考虑边缘情况并且只看直线情况时才会这样,并忽略所有副作用。
因此,顺便说一下,示例2由于没有分配第二个列表而更快。
答案 3 :(得分:0)
不同之处在于执行时间。
在第一个示例中,函数中的代码在函数退出之前执行。创建整个列表,然后以IEnumerable的形式返回。
在第二个例子中,当函数退出时,函数中的代码实际上并不运行。 相反,当函数退出时,它返回一个IEnumerable,当你稍后迭代IEnumerable时,那就是代码执行时。
特别是如果你只迭代第二个例子中IEnumerable的前3个元素,for循环将只迭代足够的时间来获得三个元素而不是更多。
答案 4 :(得分: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;
}
}
}
}
}