返回调用Yield Return的结果



我经常发现这样的场景:我使用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>的函数实际上是在返回一个对象,该对象不是完整列表,但知道在调用其EnumeratorMoveNext方法时如何迭代它。上述GetNumbersWrapped方法也是如此。它不需要等待完整的集合。它返回一个内部有Enumerator的对象。每当foreach循环(或其他循环操作)调用此枚举器的MoveNext方法时,它就会开始读取值。所以GetNumbersWrappedGetNumbersWrappedYield是相同的,只是GetNumbersWrappedYield有一个冗余的循环层。

如果我正确理解你的问题,函数GetNumbersWrapped在返回之前不会等待完整列表,因为它的返回类型是IEnumerable,并且GetNumbers内部内部循环的执行是延迟的。

正如您可能知道的,IEnumerable<T>的求值是惰性的。这意味着所有三种方法都将有效地执行完全相同的操作,但GetNumbersWrappedYield将只封装在一个冗余枚举器中。这样做不会带来任何性能优势,事实上,额外的枚举器会带来一些开销。我会直接使用GetNumbers