当我只能使用普通的IEnumerable时,为什么要使用yield关键字?


171

给出以下代码:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            yield return item;
    }
}

为什么我不应该这样编码呢?:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

我有点理解yield关键字的作用。它告诉编译器构建某种事物(迭代器)。但是为什么要使用它呢?除了代码少一点,它对我有什么作用?


28
我意识到这只是一个示例,但实际上,代码应该这样写:FullList.Where(IsItemInPartialList):)
BlueRaja-Danny Pflughoeft 2012年

Answers:


241

使用yield会使集合变得懒惰。

假设您只需要前五个项目。用您的方式,我必须遍历整个列表才能获得前五个项目。使用yield,我仅循环浏览前五个项目。


14
请注意,使用FullList.Where(IsItemInPartialList)将变得很懒。只是,它所需的编译器生成的自定义--- gunk ---代码少得多。减少了开发人员编写和维护的时间。(当然,这只是这个示例)
sehe 2012年

4
是Linq,不是吗?我想Linq在幕后做了非常相似的事情。
罗伯特·哈维

1
是的,Linq yield return尽可能使用延迟执行()。
乍得Schouggins

11
不要忘记,如果yield return语句从不执行,您仍然会得到一个空的收集结果,因此无需担心null引用异常。撒上巧克力可以使产量大增。
文斯Panuccio

127

迭代器块的好处是它们懒惰地工作。因此,您可以编写这样的过滤方法:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

这样一来,您就可以根据需要过滤流,而一次最多只能缓存一个项目。例如,如果仅需要返回序列中的第一个值,为什么要将所有内容复制到新列表中?

再举一个例子,您可以使用迭代器块轻松创建无限流。例如,这是一个随机数序列:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

您将如何在列表中存储无限序列?

我的Edulinq博客系列提供了LINQ to Objects的示例实现,该实现大量使用了迭代器块。LINQ从根本上是懒惰的-将东西放在列表中根本行不通。


1
我不确定是否喜欢你RandomSequence。对我来说,IEnumerable意味着-首先也是最重要的-我可以使用foreach进行迭代,但这显然会导致无限循环。我认为这是对IEnumerable概念的非常危险的滥用,但是YMMV。
塞巴斯蒂安·内格拉苏斯

5
@SebastianNegraszus:随机数序列在逻辑上是无限的。例如,您可以轻松创建IEnumerable<BigInteger>代表斐波那契序列的代表。您可以使用foreach它,但是不能IEnumerable<T>保证它是有限的。
乔恩·斯基特

42

使用“列表”代码,您必须先处理完整列表,然后才能将其传递到下一步。“ yield”版本将已处理的项目立即传递到下一步。如果“下一步”包含“ .Take(10)”,则“ yield”版本将仅处理前10个项目,而忽略其余项目。“列表”代码将处理所有内容。

这意味着,当您需要进行大量处理和/或要处理的项目清单较长时,您会看到最大的不同。


23

您可以yield用来返回不在列表中的项目。这是一个可以无限迭代列表直到被取消的小示例。

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

这写

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...等等。到控制台,直到被取消。


10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

当以上代码用于遍历FilteredList()并假定item.Name ==“ James”将在列表中的第二个项目上满足时,使用的方法yield将产生两次。这是一个懒惰的行为。

其中,使用list的方法会将所有n个对象添加到列表中,并将完整的列表传递给调用方法。

这正是一个用例,其中IEnumerable和IList之间的差异可以突出显示。


7

我所见过的最好的真实示例yield是计算斐波那契数列。

考虑以下代码:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

这将返回:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

这很好,因为它使您可以快速轻松地计算出无限级数,从而使您能够使用Linq扩展名并仅查询所需内容。


5
我在斐波那契数列计算中看不到任何“真实世界”。
2013年

我同意这并不是真正的“现实世界”,而是一个很棒的主意。
Casey 2014年

1

为什么要使用[yield]?除了代码少一点,它对我有什么作用?

有时有用,有时没有用。如果必须检查并返回整个数据集,那么使用yield不会有任何好处,因为它所做的只是引入了开销。

当yield真正发光时,仅返回部分集合。我认为最好的例子是排序。假设您有一个对象列表,其中包含今年的日期和美元金额,并且希望查看该年度的前几笔(5)记录。

为了做到这一点,该列表必须按日期升序排序,然后采用前5个。如果这样做没有结果,则必须对整个列表进行排序,直到确保最后两个日期都正确为止。

但是,就产量而言,一旦确定了前5个项目,就停止分类并获得结果。这样可以节省大量时间。


0

yield return语句使您一次只能返回一项。您正在收集列表中的所有项目,然后再次返回该列表,这是内存开销。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.