28

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

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

4

9 回答 9

32

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

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


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

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

(是的,第 6 章确实下载了;我验证了...)

于 2009-08-25T19:42:39.187 回答
30

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

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

鉴于上述情况,编译器将生成一个实现IEnumerator<int>, IEnumerable<int>and的类(实际上它也将实现andIDisposable的非泛型版本)。IEnumerableIEnumerator

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

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

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

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

于 2009-08-25T19:50:07.220 回答
18

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

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)");

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

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.");
于 2009-08-25T20:42:56.943 回答
10

关键字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;
        }
    }
}
于 2009-08-25T19:46:10.270 回答
6

查看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);
        }
    }
}
于 2009-08-25T19:44:08.510 回答
4

Eric White关于函数式编程的系列文章非常值得一读,但Yield上的条目与我所见的解释一样清晰。

于 2009-08-25T19:44:06.500 回答
3

yield与 LINQ 没有直接关系,而是与迭代器块相关。链接的 MSDN文章提供了有关此语言功能的详细信息。尤其参见使用迭代器部分。有关迭代器块的详细信息,请参阅 Eric Lippert 最近关于该功能的博客文章。有关一般概念,请参阅有关迭代器的 Wikipedia文章。

于 2009-08-25T19:44:21.303 回答
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();
        }
    }
}
于 2009-09-30T09:52:49.780 回答
0

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

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

于 2009-08-25T19:55:09.567 回答