C#中“ yield”关键字的实际使用


76

经过将近4年的经验,我还没有看到使用yield关键字的代码。有人可以告诉我该关键字的实际用法(以及解释)吗?如果可以,是否还有其他方法可以更轻松地实现它的功能?


9
LINQ的全部(或至少大多数)都是使用yield来实现的。Unity3D框架也找到了很好的用途-用来暂停函数(在yield语句上),以后再使用IEnumerable中的状态恢复它。
达尼(Dani)

2
这不应该移到StackOverflow吗?
Danny Varod 2011年

4
@Danny-它不适用于Stack Overflow,因为问题不是要解决特定问题,而是要问yield总体上可以使用什么。
克里斯·

9
真的?我无法想到一个我从未使用过的应用程序。
亚伦诺特,2011年

Answers:


107

效率

yield关键字有效地创建了藏品一个懒惰的枚举,可以更高效。例如,如果您的foreach循环仅对100万个项目的前5个项目进行迭代,那么这都是yield返回值,而您并没有首先在内部建立100万个项目的集合。同样,你将要使用yield带有IEnumerable<T>返回值在自己的编程场景来达到同样的效率。

在某些情况下获得效率的示例

不是迭代器方法,可能无法有效利用大集合
(建立的中间集合包含很多项目)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

迭代器版本,高效
(不构建中间集合)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

简化一些编程方案

在另一种情况下,它使列表的某些排序和合并更容易编程,因为您只需yield按所需顺序返回项目,而不是将它们排序到中间集合中并在其中交换它们。有许多这样的方案。

仅一个示例是两个列表的合并:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

此方法返回一个连续的项目列表,有效地合并而无需中间集合。

更多信息

yield关键字只能在一个迭代方法的上下文中使用(具有一个返回类型IEnumerableIEnumeratorIEnumerable<T>,或IEnumerator<T>。),并有一个与一特殊的关系foreach。迭代器是特殊的方法。在MSDN文档产量迭代器文档包含很多有趣的信息和概念的解释。请务必将其与相关foreach关键字念叨太,来补充你的迭代器的理解。

要了解迭代器如何实现其效率,秘密在于C#编译器生成的IL代码。为迭代器方法生成的IL与为常规(非迭代器)方法生成的IL完全不同。本文(收益关键字真正产生了什么?)提供了这种见解。


2
它们对于采用(可能很长)序列并生成另一个不是一对一映射的序列的算法特别有用。一个例子是多边形裁剪。一旦修剪,任何特定的边缘可能会产生很多甚至没有边缘。迭代器使这种表达非常容易,并且让出是编写它们的最佳方法之一。
Donal Fellows,

+1我写下的答案更好。现在,我还了解到良率对提高性能有好处。
2011年1

3
曾几何时,我使用yield为二进制网络协议构建数据包。这似乎是C#中最自然的选择。
捷尔吉Andrasek

4
难道不是要database.Customers.Count()枚举整个客户的枚举,从而需要更高效的代码来遍历每个项目?
斯蒂芬

5
叫我肛门,但这是串联的,不是合并的。(并且linq已经具有Concat方法。)
OldFart 2015年

4

前段时间我有一个实际的例子,让我们假设您遇到这样的情况:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

按钮对象不知道自己在集合中的位置。相同的限制适用于Dictionary<T>或其他集合类型。

这是我使用yield关键字的解决方案:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

这就是我的使用方式:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

我不必for遍历我的收藏集即可找到索引。我的按钮有一个ID,它也用作in的索引IndexerList<T>,因此您可以避免任何多余的ID或索引 -这就是我喜欢的!索引/ Id可以是任意数字。


2

一个实际的例子可以在这里找到:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

与标准代码相比,使用yield有许多优点:

  • 如果使用迭代器来构建列表,则可以产生返回值,而调用方可以决定是否要在列表中获得该结果。
  • 调用者还可能出于超出迭代范围的原因而决定取消迭代。
  • 代码要短一些。

但是,正如Jan_V所说的(将我击败它几秒钟:-),您可以没有它,因为在内部编译器将在两种情况下产生几乎相同的代码。


1

这是一个例子:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

该班级基于一个工作周执行日期计算。我可以告诉班上一个实例,鲍勃每周工作9:30到17:30,在12:30休息一小时。有了这些知识,AscendingShifts()函数将在提供的日期之间产生工作班次对象。要列出鲍勃今年1月1日至2月1日之间的所有工作班次,您可以这样使用:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

该类并没有真正遍历一个集合。但是,可以将两个日期之间的转换视为一个集合。该yield运营商能够遍历这个想象集合,而无需创建集合本身。


1

我有一个小的db数据层,该层具有一个command类,您可以在其中设置SQL命令文本,命令类型并返回IEnumerable的“命令参数”。

基本上,这种想法是键入CLR命令,而不是一直手动填充SqlCommand属性和参数。

所以有一个看起来像这样的函数:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

继承此类的command类具有属性AgeName

然后,您可以新建一个command填充其属性的对象,并将其传递给db实际执行命令调用的接口。

总而言之,使用SQL命令并保持其键入非常简单。


1

尽管合并的情况已经包含在可接受的答案中,但是让我向您展示yield-merge params扩展方法™:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

我用它来构建网络协议的数据包:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

MarkChecksum例如,该方法看起来像这样。它也有一个yield

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

但是在枚举方法中使用聚合方法(例如Sum())时要小心,因为它们会触发单独的枚举过程。


1

Elastic Search .NET示例存储库提供了一个很好的示例,该示例yield return用于将一个集合划分为多个具有指定大小的集合:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

扩展Jan_V的答案,我刚刚找到了一个与之相关的真实案例:

我需要使用FindFirstFile / FindNextFile的Kernel32版本。您可以从第一个呼叫中获得一个句柄,并将其提供给所有后续呼叫。将其包装在枚举器中,您可以获得可以直接与foreach一起使用的功能。

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.