【问题标题】:Yield keyword value added?产生关键字附加值?
【发布时间】:2010-09-27 22:00:07
【问题描述】:

仍在尝试找到在实际情况下我会在哪里使用“yield”关键字。

我在这个主题上看到了这个帖子

What is the yield keyword used for in C#?

但在接受的答案中,他们以此为例,有人在 Integers() 周围进行迭代

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

但为什么不直接使用

list<int>

改为这里。似乎更直接..

【问题讨论】:

标签: c# iterator ienumerable yield


【解决方案1】:

如果您构建并返回一个 List(假设它有 100 万个元素),那将是一大块内存,而且创建它也需要大量工作。

有时调用者可能只想知道第一个元素是什么。或者他们可能希望在获得它们时将它们写入文件,而不是在内存中构建整个列表然后将其写入文件。

这就是为什么使用收益回报更有意义的原因。它看起来与构建整个列表并返回它并没有什么不同,但它非常不同,因为在调用者可以查看其中的第一项之前,不必在内存中创建整个列表。

当来电者说:

foreach (int i in Integers())
{
   // do something with i
}

每次循环需要一个新的 i 时,它都会运行更多 Integers() 中的代码。该函数中的代码在遇到yield return 语句时“暂停”。

【讨论】:

  • 我很难理解产量。但是你的回答很好!我认为yield的使用或多或少就像使用DataReader和DataSets的区别。使用 DataSets,我们获得了所有数据,然后我们对其进行处理,而 DataReaders 您可以在数据从源到达时对其进行处理。 :-)
【解决方案2】:

Yield 允许您构建生成数据的方法,而无需在返回之前收集所有内容。将其视为一路返回多个值。

这里有几个方法可以说明这一点

public IEnumerable<String> LinesFromFile(String fileName)
{
    using (StreamReader reader = new StreamReader(fileName))
    {
        String line;
        while ((line = reader.ReadLine()) != null)
            yield return line;
    }
}

public IEnumerable<String> LinesWithEmails(IEnumerable<String> lines)
{
    foreach (String line in lines)
    {
        if (line.Contains("@"))
            yield return line;
    }
}

这两种方法都不会将文件的全部内容读入内存,但您可以像这样使用它们:

foreach (String lineWithEmail in LinesWithEmails(LinesFromFile("test.txt")))
    Console.Out.WriteLine(lineWithEmail);

【讨论】:

    【解决方案3】:

    您可以使用yield 构建任何迭代器。这可能是一个懒惰的评估系列(例如,从文件或数据库中读取行,而不是一次读取所有内容,这可能太多而无法保存在内存中),或者可能正在迭代现有数据,例如 List&lt;T&gt;

    C# in Depth 有一个免费的章节 (6)所有关于迭代器块。

    我最近还 blogged 谈论将 yield 用于智能蛮力算法。

    以惰性文件阅读器为例:

        static IEnumerable<string> ReadLines(string path) {
            using (StreamReader reader = File.OpenText(path)) {
                string line;
                while ((line = reader.ReadLine()) != null) {
                    yield return line;
                }
            }
        }
    

    这完全是“懒惰”; 什么都没有在你开始枚举之前被读取,并且只有一行被保存在内存中。

    请注意,LINQ-to-Objects 广泛使用了迭代器块 (yield)。例如,Where 扩展名本质上是:

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

    再一次,完全惰性 - 允许您将多个操作链接在一起,而无需强制将所有内容加载到内存中。

    【讨论】:

    • 关于惰性蛮力的好文章,如果您期望一个包含一个单一对象的列表,您会使用 Single() 来确保这一点吗?这是一个好习惯吗?
    • @CloudyMarble 当然,这是确保它得到验证的一种完全合理的方式。请注意,First() 可能会更便宜,但是 - 避免必须找到第二个项目 - 所以这取决于您是要断言“至少一个”还是“完全一个”
    【解决方案4】:

    yield 允许您处理可能无限大小的集合,因为与基于列表的方法不同,整个集合永远不会一次性加载到内存中。例如,所有素数的 IEnumerable 可以通过适当的算法来查找素数,而 List 方法的大小总是有限的,因此是不完整的。在此示例中,使用 yield 还允许将下一个元素的处理推迟到需要它为止。

    【讨论】:

      【解决方案5】:

      对我来说一个真实的情况是,当我想要处理一个需要一段时间才能更顺利地填充的集合时。

      想象一些类似的东西(伪代码):

      public IEnumberable<VerboseUserInfo> GetAllUsers()
      {
          foreach(UserId in userLookupList)
          {
              VerboseUserInfo info = new VerboseUserInfo();
      
              info.Load(ActiveDirectory.GetLotsOfUserData(UserId));
              info.Load(WebSerice.GetSomeMoreInfo(UserId));
      
              yield return info;
          }
      }
      

      我不必等待一分钟来填充集合,然后我才能开始处理其中的项目。我将能够立即开始,然后在发生时向用户界面报告。

      【讨论】:

        【解决方案6】:

        您可能并不总是想使用 yield 而不是返回一个列表,在您的示例中,您使用 yield 实际上返回一个整数列表。根据您想要一个可变列表还是不可变序列,您可以使用列表或迭代器(或其他一些可变/不可变的集合)。

        但是使用 yield 也有好处。

        • Yield 提供了一种构建惰性求值迭代器的简单方法。 (这意味着当调用 MoveNext() 方法时,只执行获取序列中下一个元素的代码,然后迭代器返回不做更多计算,直到再次调用该方法)

        • Yield 在后台构建了一个状态机,这样您就不必编写通用生成器的状态代码,从而节省了您分配的工作量 => 更简洁/简单的代码。

        • Yield 自动构建优化且线程安全的迭代器,让您无需了解如何构建它们的细节。

        • Yield 比第一眼看上去要强大得多,而且不仅可以用于构建简单的迭代器,请观看此视频以查看 Jeffrey Richter and his AsyncEnumerator 以及如何使用 yield 来使用异步模式进行编码容易。

        【讨论】:

          【解决方案7】:

          您可能想要遍历各种集合:

          public IEnumerable<ICustomer> Customers()
          {
                  foreach( ICustomer customer in m_maleCustomers )
                  {
                      yield return customer;
                  }
          
                  foreach( ICustomer customer in m_femaleCustomers )
                  {
                      yield return customer;
                  }
          
                  // or add some constraints...
                  foreach( ICustomer customer in m_customers )
                  {
                      if( customer.Age < 16 )
                      {
                          yield return customer;
                      }
                  }
          
                  // Or....            
                  if( Date.Today == 1 )
                  {
                      yield return m_superCustomer;
                  }
          
          }
          

          【讨论】:

          • 如果你有兴趣(并且不知道 Linq),你可以把整个事情写成: return m_maleCustomers.Concat(m_femaleCustomers).Concat(m_customers.Where(c => c.Age
          【解决方案8】:

          我同意每个人在这里所说的关于惰性求值和内存使用的一切,并想添加另一个场景,我发现使用 yield 关键字的迭代器很有用。我遇到过一些情况,我必须对一些数据进行一系列可能代价高昂的处理,而使用迭代器非常有用。与其立即处理整个文件,或者滚动我自己的处理管道,我可以简单地使用迭代器,如下所示:

          IEnumerable<double> GetListFromFile(int idxItem)
          {
              // read data from file
              return dataReadFromFile;
          }
          
          IEnumerable<double> ConvertUnits(IEnumerable<double> items)
          {
              foreach(double item in items)
                  yield return convertUnits(item);
          }
          
          IEnumerable<double> DoExpensiveProcessing(IEnumerable<double> items)
          {
              foreach(double item in items)
                  yield return expensiveProcessing(item);
          }
          
          IEnumerable<double> GetNextList()
          {
              return DoExpensiveProcessing(ConvertUnits(GetListFromFile(curIdx++)));
          }
          

          这里的优点是,通过保持所有函数IEnumerable&lt;double&gt; 的输入和输出,我的处理管道是完全可组合的、易于阅读和惰性评估的,因此我只需要进行我真正需要做的处理.这让我可以将几乎所有的处理都放在 GUI 线程中,而不会影响响应能力,因此我不必担心任何线程问题。

          【讨论】:

            【解决方案9】:

            我想出这个来克服 .net 必须手动深拷贝列表的缺点。

            我用这个:

            static public IEnumerable<SpotPlacement> CloneList(List<SpotPlacement> spotPlacements)
            {
                foreach (SpotPlacement sp in spotPlacements)
                {
                    yield return (SpotPlacement)sp.Clone();
                }
            }
            

            在另一个地方:

            public object Clone()
            {
                OrderItem newOrderItem = new OrderItem();
                ...
                newOrderItem._exactPlacements.AddRange(SpotPlacement.CloneList(_exactPlacements));
                ...
                return newOrderItem;
            }
            

            我试图想出一个可以做到这一点的 oneliner,但这是不可能的,因为 yield 在匿名方法块中不起作用。

            编辑:

            更好的是,使用通用列表克隆器:

            class Utility<T> where T : ICloneable
            {
                static public IEnumerable<T> CloneList(List<T> tl)
                {
                    foreach (T t in tl)
                    {
                        yield return (T)t.Clone();
                    }
                }
            }
            

            【讨论】:

              【解决方案10】:

              yield 使用的通过动态处理项目来节省内存的方法很好,但实际上它只是语法糖。它已经存在了很长时间。在任何具有函数或接口指针的语言(甚至 C 和汇编)中,您都可以使用回调函数/接口获得相同的效果。

              这些花哨的东西:

              static IEnumerable<string> GetItems()
              {
                  yield return "apple";
                  yield return "orange";
                  yield return "pear";
              }
              
              foreach(string item in GetItems())
              {
                  Console.WriteLine(item);
              }
              

              基本上相当于老式的:

              interface ItemProcessor
              {
                  void ProcessItem(string s);
              };
              
              class MyItemProcessor : ItemProcessor
              {
                  public void ProcessItem(string s)
                  {
                      Console.WriteLine(s);
                  }
              };
              
              static void ProcessItems(ItemProcessor processor)
              {
                  processor.ProcessItem("apple");
                  processor.ProcessItem("orange");
                  processor.ProcessItem("pear");
              }
              
              ProcessItems(new MyItemProcessor());
              

              【讨论】:

              • 这并不完全等同,因为每种方法都允许其他方法无法完成的事情。例如,可以“并行”迭代两个 IEnumerable 以进行合并操作。使用委托传递方法是不可能的。另一方面,像 ProcessItems 这样的“DoForEach”风格的方法可以通过引用接受参数并将其通过引用传递给嵌套委托;此类方法还将嵌套项调用包装在“try”块中,而不必担心在没有被 Disposed 的情况下被放弃。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-07-30
              • 1970-01-01
              • 2014-11-15
              • 2021-03-04
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多