为什么要使用yield关键字,而不是普通的IEnumerable?

174

考虑以下代码:

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关键字的作用。它告诉编译器构建一种特定类型的东西(迭代器)。但为什么要使用它?除了代码稍微少些外,它对我有什么作用呢?


29
我知道这只是一个例子,但是实际上,代码应该像这样编写:FullList.Where(IsItemInPartialList) :) - BlueRaja - Danny Pflughoeft
8个回答

246

使用yield可以使集合变得延迟处理

比如说,你只需要前五个项目。如果按照你的方式,我需要循环整个列表才能获取前五个项目。而使用yield,我只需要循环前五个项目即可。


15
请注意,使用FullList.Where(IsItemInPartialList)也同样是惰性求值的。只是,它需要更少的编译器生成的自定义 ---gunk--- 代码。并且需要更少的开发人员时间编写和维护。(当然,这只是一个例子) - sehe
5
那是Linq,不是吗?我想在幕后,Linq也做了类似的事情。 - Robert Harvey
1
是的,Linq 尽可能使用延迟执行(yield return)。 - Chad Schouggins
12
请不要担心空引用异常,即使 yield return 语句从未执行,你仍将获得空集合结果。在巧克力屑的陪伴下,yield return 真是太棒了! - Razor

129
迭代器块的好处在于它们可以懒加载。因此,您可以编写像这样的筛选方法:
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 概念的相当危险的误用,但可能因人而异。 - Sebastian Negraszus
6
一个随机数序列在逻辑上是无限的。比如,你可以轻松地创建一个表示斐波那契数列的IEnumerable<BigInteger>。你可以使用foreach来遍历它,但是IEnumerable<T>本身并不能保证它是有限的。 - Jon Skeet

42
使用"list"代码时,必须先处理完整个列表,然后才能将其传递到下一步。而"yield"版本会立即将处理过的项传递到下一步。如果该“下一步”包含“.Take(10)”则"yield"版本只会处理前10个项目并忘记剩余部分,而"list"代码将处理所有项目。
这意味着当需要处理大量项目和/或具有长列表时,您将看到最大的差异。

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的方法将会yield两次。这是一种懒惰行为。
而使用list的方法将把所有n个对象添加到列表中,并将完整的列表传递给调用方法。
这正是可以突出IEnumerable和IList之间差异的一个用例。

8

我见过使用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扩展并仅查询所需内容。


7
我在斐波那契数列的计算中看不到任何与“现实世界”相关的东西。 - Nek
我同意这不是真正的“实际应用”,但是这个想法很酷。 - Casey

1

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

有时候它很有用,有时候不是。如果必须检查并返回整个数据集,则使用yield将没有任何好处,因为它只会引入开销。

当只返回部分集合时,yield真正发挥作用。我认为最好的例子是排序。假设您有一个包含今年日期和美元金额的对象列表,并且您想查看今年的前几条(5)记录。

为了实现这一点,必须按日期升序对列表进行排序,然后取出前5个。如果没有使用yield,就必须对整个列表进行排序,直到确保最后两个日期的顺序。

但是,使用yield之后,一旦确定了前5项,排序就会停止,结果就可用了。这可以节省大量时间。


0
yield return语句允许您一次只返回一个项目。您正在收集列表中的所有项目,然后再返回该列表,这会导致内存开销。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接