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

4

8 回答 8

241

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

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

于 2012-12-27T16:28:09.033 回答
128

迭代器块的好处是它们可以懒惰地工作。所以你可以写一个这样的过滤方法:

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 从根本上讲是懒惰的——把东西放在一个列表中根本行不通。

于 2012-12-27T16:29:59.427 回答
42

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

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

于 2012-12-27T16:29:19.783 回答
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

...ETC。到控制台,直到取消。

于 2012-12-27T16:32:18.280 回答
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 之间差异的用例。

于 2012-12-27T16:30:39.960 回答
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 扩展并仅查询您需要的内容。

于 2013-04-22T22:42:17.940 回答
1

为什么使用[产量]?除了代码稍微少一点之外,它对我有什么用?

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

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

为了做到这一点,列表必须按日期升序排序,然后取前 5 个。如果在没有产量的情况下这样做,则必须对整个列表进行排序,直到确保最后两个日期是有序的。

但是,对于产量,一旦确定了前 5 个项目,排序就会停止并且结果可用。这可以节省大量时间。

于 2014-07-23T23:31:38.793 回答
0

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

于 2013-01-02T11:48:21.327 回答