【问题标题】:What is the yield keyword used for in C#?C# 中使用的 yield 关键字是什么?
【发布时间】:2014-09-27 11:47:37
【问题描述】:

How Can I Expose Only a Fragment of IList<> 问题中,其中一个答案的代码如下:sn-p:

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

yield 关键字在那里有什么作用?我已经在几个地方看到了它的引用,还有另一个问题,但我还没有完全弄清楚它的实际作用。我习惯于从一个线程屈服于另一个线程的意义上考虑屈服,但这在这里似乎无关紧要。

【问题讨论】:

标签: c# yield


【解决方案1】:

yield 上下文关键字实际上在这里做了很多。

该函数返回一个实现IEnumerable&lt;object&gt; 接口的对象。如果调用函数在此对象上开始foreaching,则再次调用该函数,直到它“屈服”。这是 C# 2.0 中引入的语法糖。在早期版本中,您必须创建自己的 IEnumerableIEnumerator 对象才能执行此类操作。

理解这样的代码最简单的方法是输入一个示例,设置一些断点,然后看看会发生什么。尝试逐步执行此示例:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

当您逐步完成该示例时,您会发现对Integers() 的第一次调用返回1。第二次调用返回2 并且yield return 1 行不再执行。

这是一个真实的例子:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

【讨论】:

  • 在这种情况下会更容易,我只是在这里使用整数来显示收益率返回的工作原理。使用 yield return 的好处是它是实现迭代器模式的一种非常快速的方法,因此会延迟评估。
  • 另外值得注意的是,当您不想退货时,可以使用yield break;
  • yield 不是关键字。如果是这样,我就不能像 int yield = 500; 那样使用 yield 作为标识符
  • @Brandin 这是因为所有编程语言都支持两种类型的关键字,即保留关键字和上下文关键字。 yield 属于后面的类别,这就是 C# 编译器不禁止您的代码的原因。更多细节在这里:ericlippert.com/2009/05/11/reserved-and-contextual-keywords 你会很高兴知道还有保留字不被语言识别为关键字。例如在java中转到。更多细节在这里:stackoverflow.com/questions/2545103/…
  • 'If a calling function starts foreach-ing over this object the function is called again until it "yields"'。对我来说听起来不对。我一直在“庄稼丰收”的上下文中想到 c# yield 关键字,而不是“汽车让行人让步”。
【解决方案2】:

迭代。它会在“幕后”创建一个状态机,该状态机会记住您在函数的每个附加循环中所处的位置,并从那里获取信息。

【讨论】:

    【解决方案3】:

    产量有两大用途,

    1. 它有助于在不创建临时集合的情况下提供自定义迭代。

    2. 它有助于进行有状态的迭代。

    为了更直观地解释以上两点,我制作了一个简单的视频,大家可以观看here

    【讨论】:

    • 该视频帮助我清楚地了解yield。 @ShivprasadKoirala 的代码项目文章What is the use of C# Yield ?的同解也是不错的来源
    • 我还要补充一点,yield 是创建自定义 IEnumerator 的“快速”方式(而不是让类实现 IEnumerator 接口)。
    • 我看了你的视频 Shivprasad,它清楚地解释了 yield 关键字的用法。
    • 很棒的视频,但想知道...使用 yield 的实现显然更干净,但它本质上必须在内部创建自己的临时内存或/和 List 以跟踪状态(或者更确切地说创建状态机)。那么,“Yield”除了让实现更简单、让事情看起来更好之外,还有什么别的作用吗?或者还有别的什么吗?效率如何,使用 Yield 运行代码的效率/速度是否比不使用更高或更低?
    【解决方案4】:

    最近 Raymond Chen 还发表了一系列关于 yield 关键字的有趣文章。

    虽然它名义上用于轻松实现迭代器模式,但可以推广到状态机。引用 Raymond 没有意义,最后一部分还链接到其他用途(但 Entin 博客中的示例特别好,展示了如何编写异步安全代码)。

    【讨论】:

    • 这需要投票。他如何解释运营商的目的和内部结构。
    • 第 1 部分解释了“收益回报”的语法糖。很好的解释!
    【解决方案5】:

    乍一看,yield return 是一个 .NET 糖返回一个 IEnumerable

    没有yield,集合的所有项都会一次性创建:

    class SomeData
    {
        public SomeData() { }
    
        static public IEnumerable<SomeData> CreateSomeDatas()
        {
            return new List<SomeData> {
                new SomeData(), 
                new SomeData(), 
                new SomeData()
            };
        }
    }
    

    使用yield的相同代码,它逐项返回:

    class SomeData
    {
        public SomeData() { }
    
        static public IEnumerable<SomeData> CreateSomeDatas()
        {
            yield return new SomeData();
            yield return new SomeData();
            yield return new SomeData();
        }
    }
    

    使用 yield 的好处是,如果使用数据的函数只需要集合的第一项,则不会创建其余的项。

    yield 运算符允许根据需要创建项目。这是使用它的好理由。

    【讨论】:

      【解决方案6】:

      列表或数组实现立即加载所有项目,而 yield 实现提供延迟执行解决方案。

      在实践中,通常希望根据需要执行最少的工作,以减少应用程序的资源消耗。

      例如,我们可能有一个应用程序处理来自数据库的数百万条记录。当我们在延迟执行拉式模型中使用 IEnumerable 时,可以获得以下好处:

      • 可扩展性、可靠性和可预测性可能会提高,因为记录的数量不会显着影响应用程序的资源需求。
      • 性能和响应能力可能会提高,因为处理可以立即开始,而不是等待首先加载整个集合。
      • 可恢复性和利用率可能会提高,因为应用程序可以停止、启动、中断或失败。与仅使用实际使用部分结果的预取所有数据相比,只会丢失正在进行的项目。
      • 在添加恒定工作负载流的环境中,可以进行连续处理

      这是先构建集合(例如列表)与使用 yield 的比较。

      列表示例

          public class ContactListStore : IStore<ContactModel>
          {
              public IEnumerable<ContactModel> GetEnumerator()
              {
                  var contacts = new List<ContactModel>();
                  Console.WriteLine("ContactListStore: Creating contact 1");
                  contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
                  Console.WriteLine("ContactListStore: Creating contact 2");
                  contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
                  Console.WriteLine("ContactListStore: Creating contact 3");
                  contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
                  return contacts;
              }
          }
      
          static void Main(string[] args)
          {
              var store = new ContactListStore();
              var contacts = store.GetEnumerator();
      
              Console.WriteLine("Ready to iterate through the collection.");
              Console.ReadLine();
          }
      

      控制台输出
      ContactListStore:创建联系人 1
      ContactListStore:创建联系人 2
      ContactListStore:创建联系人 3
      准备遍历集合。

      注意:整个集合被加载到内存中,甚至没有请求列表中的单个项目

      收益示例

      public class ContactYieldStore : IStore<ContactModel>
      {
          public IEnumerable<ContactModel> GetEnumerator()
          {
              Console.WriteLine("ContactYieldStore: Creating contact 1");
              yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
              Console.WriteLine("ContactYieldStore: Creating contact 2");
              yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
              Console.WriteLine("ContactYieldStore: Creating contact 3");
              yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
          }
      }
      
      static void Main(string[] args)
      {
          var store = new ContactYieldStore();
          var contacts = store.GetEnumerator();
      
          Console.WriteLine("Ready to iterate through the collection.");
          Console.ReadLine();
      }
      

      控制台输出
      准备遍历集合。

      注意:该集合根本没有执行。这是由于 IEnumerable 的“延迟执行”性质。只有在真正需要时才会构建项目。

      让我们再次调用集合并观察我们在获取集合中的第一个联系人时的行为。

      static void Main(string[] args)
      {
          var store = new ContactYieldStore();
          var contacts = store.GetEnumerator();
          Console.WriteLine("Ready to iterate through the collection");
          Console.WriteLine("Hello {0}", contacts.First().FirstName);
          Console.ReadLine();
      }
      

      控制台输出
      准备遍历集合
      ContactYieldStore:创建联系人 1
      你好鲍勃

      不错!当客户端从集合中“拉出”项目时,仅构造了第一个联系人。

      【讨论】:

      • 这个答案需要多加注意!谢谢
      • @leon22 绝对+2
      【解决方案7】:

      yield return 与枚举器一起使用。在每次调用 yield 语句时,控制权都返回给调用者,但它确保保持被调用者的状态。因此,当调用者枚举下一个元素时,它会在 yield 语句之后立即在被调用者方法 from 语句中继续执行。

      让我们试着通过一个例子来理解这一点。在这个例子中,对应于每一行,我已经提到了执行流程的顺序。

      static void Main(string[] args)
      {
          foreach (int fib in Fibs(6))//1, 5
          {
              Console.WriteLine(fib + " ");//4, 10
          }            
      }
      
      static IEnumerable<int> Fibs(int fibCount)
      {
          for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
          {
              yield return prevFib;//3, 9
              int newFib = prevFib + currFib;//6
              prevFib = currFib;//7
              currFib = newFib;//8
          }
      }
      

      此外,为每个枚举维护状态。假设,我再次调用Fibs() 方法,那么状态将被重置。

      【讨论】:

      • 设置 prevFib = 1 - 第一个斐波那契数是“1”,而不是“0”
      【解决方案8】:

      直观地说,关键字从函数返回一个值而不离开它,即在您的代码示例中,它返回当前的item 值,然后继续循环。更正式地说,它被编译器用来为 iterator 生成代码。迭代器是返回IEnumerable 对象的函数。 MSDN 有几个关于它们的articles

      【讨论】:

      • 好吧,确切地说,它不会恢复循环,它会暂停循环,直到父调用“iterator.next()”。
      • @jitbit 这就是我使用“直观”和“更正式”的原因。
      【解决方案9】:

      如果我理解正确,下面是我从实现 IEnumerable 和 yield 的函数的角度来表达这一点的方式。

      • 这是一个。
      • 如果您需要另一个,请再次致电。
      • 我会记住我已经给你的。
      • 只有当你再次打电话时,我才知道能否再给你一个。

      【讨论】:

      • 简单而精彩
      【解决方案10】:

      以下是理解该概念的简单方法: 基本思想是,如果您想要一个可以使用“foreach”的集合,但是出于某种原因(例如从数据库中查询它们),将项目收集到集合中的成本很高,而且您通常不需要整个集合,然后您创建一个函数,该函数一次构建一个集合并将其返回给消费者(然后消费者可以提前终止收集工作)。

      这样想:你去肉类柜台想买一磅火腿片。屠夫将一根 10 磅重的火腿放在背后,放在切片机上,将整块火腿切片,然后将一堆切片拿回给你,量出一磅。 (旧方式)。 使用yield,屠夫将切片机带到柜台,开始切片并在秤上“生产”每一片,直到它达到 1 磅,然后为您包装好,您就完成了。 旧方式可能更适合屠夫(让他按照自己喜欢的方式组织机器),但在大多数情况下,新方式显然对消费者更有效。

      【讨论】:

        【解决方案11】:

        yield 关键字允许您在iterator block 的表单中创建IEnumerable&lt;T&gt;。这个迭代器块支持延迟执行,如果你不熟悉这个概念,它可能看起来很神奇。然而,归根结底,它只是执行的代码,没有任何奇怪的技巧。

        迭代器块可以描述为语法糖,其中编译器生成一个状态机,用于跟踪可枚举的枚举进展的程度。要枚举一个可枚举的对象,您经常使用foreach 循环。然而,foreach 循环也是语法糖。所以你是从真实代码中删除的两个抽象,这就是为什么最初可能很难理解它们是如何一起工作的。

        假设您有一个非常简单的迭代器块:

        IEnumerable<int> IteratorBlock()
        {
            Console.WriteLine("Begin");
            yield return 1;
            Console.WriteLine("After 1");
            yield return 2;
            Console.WriteLine("After 2");
            yield return 42;
            Console.WriteLine("End");
        }
        

        真正的迭代器块通常有条件和循环,但是当您检查条件并展开循环时,它们仍然会以yield 语句与其他代码交错结束。

        要枚举迭代器块,使用foreach 循环:

        foreach (var i in IteratorBlock())
            Console.WriteLine(i);
        

        这是输出(这里没有惊喜):

        开始 1 1 之后 2 2 之后 42 结尾

        如上所述foreach是语法糖:

        IEnumerator<int> enumerator = null;
        try
        {
            enumerator = IteratorBlock().GetEnumerator();
            while (enumerator.MoveNext())
            {
                var i = enumerator.Current;
                Console.WriteLine(i);
            }
        }
        finally
        {
            enumerator?.Dispose();
        }
        

        为了解决这个问题,我制作了一个删除抽象的序列图:

        编译器生成的状态机也实现了枚举器,但为了使图表更清晰,我将它们显示为单独的实例。 (当从另一个线程枚举状态机时,您实际上会得到单独的实例,但这个细节在这里并不重要。)

        每次调用迭代器块时,都会创建一个新的状态机实例。但是,在第一次执行 enumerator.MoveNext() 之前,不会执行迭代器块中的任何代码。这就是延迟执行的工作原理。这是一个(相当愚蠢的)示例:

        var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
        

        此时迭代器还没有执行。 Where 子句创建了一个新的IEnumerable&lt;T&gt;,它包装了IteratorBlock 返回的IEnumerable&lt;T&gt;,但这个可枚举对象尚未被枚举。当您执行 foreach 循环时会发生这种情况:

        foreach (var evenNumber in evenNumbers)
            Console.WriteLine(eventNumber);
        

        如果您对可枚举对象进行两次枚举,则每次都会创建一个新的状态机实例,并且您的迭代器块将执行相同的代码两次。

        请注意,ToList()ToArray()First()Count() 等 LINQ 方法将使用 foreach 循环来枚举可枚举对象。例如ToList() 将枚举可枚举的所有元素并将它们存储在列表中。您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块。在使用诸如ToList() 之类的方法时,在使用 CPU 多次生成可枚举元素和使用内存存储枚举元素以多次访问它们之间存在权衡。

        【讨论】:

          【解决方案12】:

          关于 Yield 关键字的一个重点是延迟执行。现在我所说的延迟执行是在需要时执行。更好的表达方式是举个例子

          示例:不使用 Yield,即不延迟执行。

          public static IEnumerable<int> CreateCollectionWithList()
          {
              var list =  new List<int>();
              list.Add(10);
              list.Add(0);
              list.Add(1);
              list.Add(2);
              list.Add(20);
          
              return list;
          }
          

          示例:使用 Yield,即延迟执行。

          public static IEnumerable<int> CreateCollectionWithYield()
          {
              yield return 10;
              for (int i = 0; i < 3; i++) 
              {
                  yield return i;
              }
          
              yield return 20;
          }
          

          现在当我调用这两种方法时。

          var listItems = CreateCollectionWithList();
          var yieldedItems = CreateCollectionWithYield();
          

          您会注意到 listItems 里面有 5 个项目(在调试时将鼠标悬停在 listItems 上)。 而 yieldItems 只会引用方法而不是项目。 这意味着它没有执行在方法中获取项目的过程。仅在需要时获取数据的一种非常有效的方法。 yield 的实际实现可以在 Entity Framework 和 NHibernate 等 ORM 中看到。

          【讨论】:

            【解决方案13】:

            简单地说,C# 的 yield 关键字允许多次调用代码体,称为迭代器,它知道如何在完成之前返回,并且当再次调用时,从中断处继续 - 即它帮助迭代器按照迭代器在连续调用中返回的序列中的每个项目变得透明地有状态。

            在 JavaScript 中,同样的概念称为生成器。

            【讨论】:

            • 最好的解释。这些也是python中的相同生成器吗?
            【解决方案14】:

            这是为您的对象创建可枚举的一种非常简单易行的方法。编译器会创建一个类来包装您的方法并在本例中实现 IEnumerable。如果没有 yield 关键字,您必须创建一个实现 IEnumerable 的对象。

            【讨论】:

              【解决方案15】:

              它正在产生可枚举的序列。它实际上是创建本地 IEnumerable 序列并将其作为方法结果返回

              【讨论】:

                【解决方案16】:

                这个link有一个简单的例子

                这里有更简单的例子

                public static IEnumerable<int> testYieldb()
                {
                    for(int i=0;i<3;i++) yield return 4;
                }
                

                请注意,yield return 不会从该方法返回。您甚至可以在yield return 之后添加WriteLine

                上面产生了 4 个整数 4,4,4,4 的 IEnumerable

                这里是WriteLine。将 4 添加到列表中,打印 abc,然后将 4 添加到列表中,然后完成方法,然后真正从方法返回(一旦方法完成,就像没有返回的过程一样)。但这会有一个值,一个ints 的IEnumerable 列表,它会在完成时返回。

                public static IEnumerable<int> testYieldb()
                {
                    yield return 4;
                    console.WriteLine("abc");
                    yield return 4;
                }
                

                还请注意,当您使用 yield 时,您返回的内容与函数的类型不同。它属于IEnumerable 列表中的元素类型。

                您使用yield,方法的返回类型为IEnumerable。如果方法的返回类型是intList&lt;int&gt; 并且您使用yield,那么它将无法编译。您可以使用不带yield 的IEnumerable 方法返回类型,但似乎您不能使用不带IEnumerable 方法返回类型的yield。

                要让它执行,你必须以一种特殊的方式调用它。

                static void Main(string[] args)
                {
                    testA();
                    Console.Write("try again. the above won't execute any of the function!\n");
                
                    foreach (var x in testA()) { }
                
                
                    Console.ReadLine();
                }
                
                
                
                // static List<int> testA()
                static IEnumerable<int> testA()
                {
                    Console.WriteLine("asdfa");
                    yield return 1;
                    Console.WriteLine("asdf");
                }
                

                【讨论】:

                • 注意——如果试图理解 SelectMany,它会使用 yield 和泛型。这个例子可能对 public static IEnumerable&lt;TResult&gt; testYieldc&lt;TResult&gt;(TResult t) { yield return t; }public static IEnumerable&lt;TResult&gt; testYieldc&lt;TResult&gt;(TResult t) { return new List&lt;TResult&gt;(); } 有帮助
                • 看起来很好的解释!这可能是公认的答案。
                • @pongapundit 谢谢,我的回答当然清晰而简单,但我自己并没有使用太多 yield,其他回答者比我有更多的经验和使用知识。我在这里写的关于产量的内容可能是我挠头试图在这里和那个 dotnetperls 链接上找出一些答案!但是由于我不太了解yield return(除了我提到的简单的事情),并且没有太多使用它并且不太了解它的用途,我认为这不应该是公认的.
                【解决方案17】:

                现在您可以将 yield 关键字用于异步流。

                C# 8.0 引入了异步流,它对流数据源进行建模。数据流经常异步检索或生成元素。异步流依赖于 .NET Standard 2.1 中引入的新接口。 .NET Core 3.0 及更高版本支持这些接口。它们为异步流数据源提供了一种自然的编程模型。

                来源:Microsoft docs

                以下示例

                using System;
                using System.Collections.Generic;               
                using System.Threading.Tasks;
                
                public class Program
                {
                    public static async Task Main()
                    {
                        List<int> numbers = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
                        
                        await foreach(int number in YieldReturnNumbers(numbers))
                        {
                            Console.WriteLine(number);
                        }
                    }
                    
                    public static async IAsyncEnumerable<int> YieldReturnNumbers(List<int> numbers) 
                    {
                        foreach (int number in numbers)
                        {
                            await Task.Delay(1000);
                            yield return number;
                        }
                    }
                }
                

                【讨论】:

                  【解决方案18】:

                  了解产量的简单演示

                  using System;
                  using System.Collections.Generic;
                  using System.Linq;
                  
                  namespace ConsoleApp_demo_yield {
                      class Program
                      {
                          static void Main(string[] args)
                          {
                              var letters = new List<string>() { "a1", "b1", "c2", "d2" };
                  
                              // Not yield
                              var test1 = GetNotYield(letters);
                  
                              foreach (var t in test1)
                              {
                                  Console.WriteLine(t);
                              }
                  
                              // yield
                              var test2 = GetWithYield(letters).ToList();
                  
                              foreach (var t in test2)
                              {
                                  Console.WriteLine(t);
                              }
                  
                              Console.ReadKey();
                          }
                  
                          private static IList<string> GetNotYield(IList<string> list)
                          {
                              var temp = new List<string>();
                              foreach(var x in list)
                              {
                                  
                                  if (x.Contains("2")) { 
                                  temp.Add(x);
                                  }
                              }
                  
                              return temp;
                          }
                  
                          private static IEnumerable<string> GetWithYield(IList<string> list)
                          {
                              foreach (var x in list)
                              {
                                  if (x.Contains("2"))
                                  {
                                      yield return x;
                                  }
                              }
                          }
                      } 
                  }
                  

                  【讨论】:

                  • 我认为需要在这个答案中添加一些解释来澄清你的意思。
                  【解决方案19】:

                  它试图引入一些 Ruby Goodness :)
                  概念:这是一些示例 Ruby 代码,可以打印出数组的每个元素

                   rubyArray = [1,2,3,4,5,6,7,8,9,10]
                      rubyArray.each{|x| 
                          puts x   # do whatever with x
                      }
                  

                  数组的每个方法实现控制权交给调用者('puts x'),数组的每个元素整齐地呈现为x。然后调用者可以对 x 做任何需要做的事情。

                  但是 .Net 并没有一路走下去.. C# 似乎将 yield 与 IEnumerable 结合在一起,以某种方式迫使您在调用者中编写一个 foreach 循环,如 Mendelt 的响应所示.不那么优雅。

                  //calling code
                  foreach(int i in obCustomClass.Each())
                  {
                      Console.WriteLine(i.ToString());
                  }
                  
                  // CustomClass implementation
                  private int[] data = {1,2,3,4,5,6,7,8,9,10};
                  public IEnumerable<int> Each()
                  {
                     for(int iLooper=0; iLooper<data.Length; ++iLooper)
                          yield return data[iLooper]; 
                  }
                  

                  【讨论】:

                  • -1 这个答案对我来说听起来不对。是的,C# yieldIEnumerable 结合在一起,而 C# 缺少 Ruby 的“块”概念。但是 C# 有 lambda,它可以允许实现 ForEach 方法,很像 Ruby 的 eachThis that does not mean it would be a good idea to do so,不过。
                  • 更好:public IEnumerable Each() { int index = 0;产生返回数据[索引++]; }
                  猜你喜欢
                  • 2022-11-29
                  • 2011-01-17
                  • 2012-07-01
                  • 2011-05-18
                  • 2020-01-22
                  • 2023-03-12
                  • 2021-09-20
                  相关资源
                  最近更新 更多