【问题标题】:Can someone demystify the yield keyword?有人可以揭开 yield 关键字的神秘面纱吗?
【发布时间】:2023-03-28 15:45:01
【问题描述】:

我已经看到在 Stack Overflow 和博客上大量使用了 yield 关键字。我不使用LINQ。有人可以解释 yield 关键字吗?

我知道存在类似的问题。 但没有人真正用简单的语言解释它的用途。

【问题讨论】:

  • 好吧,这可能是相似的。但是,这里的答案是不同的,更符合这个问题并且比那个帖子中的答案更好(w.r.t this question)。
  • 我现在可以问一个问题吗?每当一个类型返回 IEnumerable 这意味着我可以迭代它,对吗?因为它有 GetEnumerator() 方法对吗?谢谢。
  • @Aaron:你可以迭代一个实现IEnumerable<T>的类型,它(因为它实现了IEnumerable<T>)有一个GetEnumerator()方法。
  • 如果你“不使用 LINQ”,你应该试一试——你永远不会回头!

标签: c# language-features yield


【解决方案1】:

为了揭开神秘面纱,我将避免谈论迭代器,因为它们本身也可能成为谜团的一部分。

yield return 和yield break 语句最常用于提供集合的“延迟评估”。

这意味着当您获取使用 yield return 的方法的值时,您尝试获取的东西的集合还不存在(它本质上是空的)。当您遍历它们(使用 foreach)时,它将在那时执行该方法并获取枚举中的下一个元素。

某些属性和方法会导致一次计算整个枚举(例如“Count”)。

这里有一个简单的例子来说明返回集合和返回产量之间的区别:

string[] names = { "Joe", "Jim", "Sam", "Ed", "Sally" };

public IEnumerable<string> GetYieldEnumerable()
{
    foreach (var name in names)
        yield return name;
}

public IEnumerable<string> GetList()
{
    var list = new List<string>();
    foreach (var name in names)
        list.Add(name);

    return list;
}

// we're going to execute the GetYieldEnumerable() method
// but the foreach statement inside it isn't going to execute
var yieldNames = GetNamesEnumerable();

// now we're going to execute the GetList() method and
// the foreach method will execute
var listNames = GetList();

// now we want to look for a specific name in yieldNames.
// only the first two iterations of the foreach loop in the 
// GetYieldEnumeration() method will need to be called to find it.
if (yieldNames.Contains("Jim")
    Console.WriteLine("Found Jim and only had to loop twice!");

// now we'll look for a specific name in listNames.
// the entire names collection was already iterated over
// so we've already paid the initial cost of looping through that collection.
// now we're going to have to add two more loops to find it in the listNames
// collection.
if (listNames.Contains("Jim"))
    Console.WriteLine("Found Jim and had to loop 7 times! (5 for names and 2 for listNames)");

如果您需要在源数据有值之前获取对枚举的引用,也可以使用此方法。例如,如果名称集合开始时不完整:

string[] names = { "Joe", "Jim", "Sam", "Ed", "Sally" };

public IEnumerable<string> GetYieldEnumerable()
{
    foreach (var name in names)
        yield return name;
}

public IEnumerable<string> GetList()
{
    var list = new List<string>();
    foreach (var name in names)
        list.Add(name);

    return list;
}

var yieldNames = GetNamesEnumerable();

var listNames = GetList();

// now we'll change the source data by renaming "Jim" to "Jimbo"
names[1] = "Jimbo";

if (yieldNames.Contains("Jimbo")
    Console.WriteLine("Found Jimbo!");

// Because this enumeration was evaluated completely before we changed "Jim"
// to "Jimbo" it isn't going to be found
if (listNames.Contains("Jimbo"))
    // this can't be true
else
   Console.WriteLine("Couldn't find Jimbo, because he wasn't there when I was evaluated.");

【讨论】:

  • 帮我揭开神秘面纱一点点 =)
  • 这个概念初学者的精彩解释
【解决方案2】:

我想出这个来克服 .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();
        }
    }
}

【讨论】:

    【解决方案3】:

    yield 关键字与返回 IEnumerable&lt;T&gt;IEnumerator&lt;T&gt; 的方法一起使用,它使编译器生成一个类,该类实现了使用迭代器所需的管道。例如。

    public IEnumerator<int> SequenceOfOneToThree() {
        yield return 1;
        yield return 2;
        yield return 3;
    }
    

    鉴于上述情况,编译器将生成一个实现IEnumerator&lt;int&gt;IEnumerable&lt;int&gt;IDisposable 的类(实际上它还将实现IEnumerableIEnumerator 的非泛型版本)。

    这允许您像这样在foreach 循环中调用方法SequenceOfOneToThree

    foreach(var number in SequenceOfOneToThree) {
        Console.WriteLine(number);
    }
    

    迭代器是一个状态机,因此每次调用yield 时都会记录方法中的位置。如果迭代器移动到下一个元素,则该方法会在该位置之后立即恢复。所以第一次迭代返回 1 并标记该位置。下一个迭代器在一个之后立即恢复,因此返回 2,依此类推。

    不用说,您可以以任何您喜欢的方式生成序列,因此您不必像我一样对数字进行硬编码。另外,如果你想打破循环,你可以使用yield break

    【讨论】:

    • 在我打开的 12 个 Stackoverflow、2 个 msdn 和 3 个网站选项卡中:这是解释 为什么 我会使用 yield 的一个。这当然是一个棘手的概念,听起来很像过早的优化。
    【解决方案4】:

    到目前为止(我见过的)最好的解释是 Jon Skeet 的书——那一章是免费的!第 6 章,C# in Depth。我可以在这里添加任何未涵盖的内容。

    然后买书;你会成为一个更好的 C# 程序员。


    问:为什么我不在这里写一个更长的答案(转述自cmets);简单的。正如 Eric Lippert 所观察到的 (here),yield 构造(及其背后的魔力)是C# 编译器中最复杂的一段代码,并尝试描述它在这里的简短答复充其量是幼稚的。 yield 有很多细微差别,因此 IMO 最好引用一个预先存在的(且完全合格的)资源。

    Eric 的博客现在有 7 个条目(这只是最近的条目)讨论 yield。我非常尊重 Eric,但他的博客可能更适合作为 熟悉该主题的人的“更多信息”(yield in这种情况),因为它通常描述了许多背景设计考虑因素。最好在合理的基础上完成。

    (是的,第 6 章下载;我已验证...)

    【讨论】:

    • 所以我很困惑:我是在给你投票还是在这里投掷飞碟? :P
    • 没关系 - 我们都在这一天达到极限;-p
    • @Alex - 厚脸皮;-p 否 - 独立推荐。
    • -1。不要忽视这本书,但这并不能回答这个问题。正如我们实际所说的那样,第 6 章甚至没有加载。为什么不回答,然后链接到本书以获取更多信息?
    • 确实,该系列文章明确介绍了将非正交性引入设计的迭代器的奇怪的极端情况,而不是设计“主线”情况。
    【解决方案5】:

    让我补充一下。产量不是关键字。 它只有在你使用“收益回报”时才会起作用,而不是像普通变量一样工作。

    它用于从函数返回迭代器。您可以进一步搜索。 我建议搜索“返回数组与迭代器”

    【讨论】:

    • yield 是 C# 的关键字。它是一个未保留的上下文关键字,但它仍然是一个关键字。
    【解决方案6】:

    yield 关键字是编写IEnumerator 的便捷方式。例如:

    public static IEnumerator<int> Range(int from, int to)
    {
        for (int i = from; i < to; i++)
        {
            yield return i;
        }
    }
    

    由 C# 编译器转换为类似于:

    public static IEnumerator<int> Range(int from, int to)
    {
        return new RangeEnumerator(from, to);
    }
    
    class RangeEnumerator : IEnumerator<int>
    {
        private int from, to, current;
    
        public RangeEnumerator(int from, int to)
        {
            this.from = from;
            this.to = to;
            this.current = from;
        }
    
        public bool MoveNext()
        {
            this.current++;
            return this.current < this.to;
        }
    
        public int Current
        {
            get
            {
                return this.current;
            }
        }
    }
    

    【讨论】:

      【解决方案7】:

      yield 与 LINQ 没有直接关系,而是与 iterator blocks 直接相关。链接的 MSDN article 提供了有关此语言功能的详细信息。请特别参阅Using Iterators 部分。有关迭代器块的详细信息,请参阅 Eric Lippert 最近关于该功能的博客 posts。有关一般概念,请参阅 Wikipedia article on iterators。

      【讨论】:

        【解决方案8】:

        查看MSDN 文档和示例。它本质上是一种在 C# 中创建迭代器的简单方法。

        public class List
        {
            //using System.Collections;
            public static IEnumerable Power(int number, int exponent)
            {
                int counter = 0;
                int result = 1;
                while (counter++ < exponent)
                {
                    result = result * number;
                    yield return result;
                }
            }
        
            static void Main()
            {
                // Display powers of 2 up to the exponent 8:
                foreach (int i in Power(2, 8))
                {
                    Console.Write("{0} ", i);
                }
            }
        }
        

        【讨论】:

        • 那个不错。 yield 的用途很明显。
        【解决方案9】:

        Eric White 的 series on functional programming 值得一读,但 entry on Yield 的解释与我所见的一样清晰。

        【讨论】:

          猜你喜欢
          • 2018-04-17
          • 2020-11-08
          • 2011-04-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-04-29
          • 2012-11-08
          相关资源
          最近更新 更多