我经常发现这样的场景:我使用yield return
语句返回一个IEnumerable,然后使用其他方法用不同的参数调用该函数并直接返回结果。
与简单地返回集合相比,在yield return
结束时迭代结果是否有任何性能优势?即,如果我在生成的IEnumberable上使用return
,而不是再次循环这些结果,并使用yield return
,编译器是否知道只生成所需的结果,还是等待返回整个集合后再返回所有结果?
public class Demo
{
private IEnumerable<int> GetNumbers(int x)
{
//imagine this operation were more expensive than this demo version
//e.g. calling a web service on each iteration.
for (int i = 0; i < x; i++)
{
yield return i;
}
}
//does this wait for the full list before returning
public IEnumerable<int> GetNumbersWrapped()
{
return GetNumbers(10);
}
//whilst this allows the benefits of Yield Return to persist?
public IEnumerable<int> GetNumbersWrappedYield()
{
foreach (int i in GetNumbers(10))
yield return i;
}
}
GetNumbersWrapped
只是简单地通过原始可枚举对象——它当时甚至还没有调用迭代器。最终结果仍然是完全延迟/延迟/假脱机/其他任何操作。
GetNumbersWrappedYield
添加了一个额外的抽象层,所以现在,对MoveNext
的每个调用都必须做更多的工作;不足以引起疼痛。它也将被完全推迟/延迟/假脱机/其他任何操作,但会增加一些小的开销。通常,这些较小的开销是合理的,因为您正在添加一些值,如过滤或额外处理;但在本例中并非如此。
注意:GetNumbersWrappedYield
所做的的一件事是防止呼叫者滥用。例如,如果GetNumbers
是:
private IEnumerable<int> GetNumbers(int x)
{
return myPrivateSecretList;
}
则GetNumbersWrapped
的呼叫者可以滥用这个:
IList list = (IList)GetNumbers(4);
list.Clear();
list.Add(123); // mwahahaahahahah
但是,GetNumbersWrappedYield
的呼叫者不能执行此操作。。。至少,不是那么容易。当然,他们仍然可以使用反射来分离迭代器,获得封装的内部引用,并强制转换。
然而,这通常不是一个真正的问题。
返回IEnumberable<T>
的函数实际上是在返回一个对象,该对象不是完整列表,但知道在调用其Enumerator
的MoveNext
方法时如何迭代它。上述GetNumbersWrapped
方法也是如此。它不需要等待完整的集合。它返回一个内部有Enumerator
的对象。每当foreach
循环(或其他循环操作)调用此枚举器的MoveNext
方法时,它就会开始读取值。所以GetNumbersWrapped
和GetNumbersWrappedYield
是相同的,只是GetNumbersWrappedYield
有一个冗余的循环层。
如果我正确理解你的问题,函数GetNumbersWrapped
在返回之前不会等待完整列表,因为它的返回类型是IEnumerable,并且GetNumbers
内部内部循环的执行是延迟的。
正如您可能知道的,IEnumerable<T>
的求值是惰性的。这意味着所有三种方法都将有效地执行完全相同的操作,但GetNumbersWrappedYield
将只封装在一个冗余枚举器中。这样做不会带来任何性能优势,事实上,额外的枚举器会带来一些开销。我会直接使用GetNumbers
。