有人可以揭开yield关键字的神秘面纱吗?

时间:2009-08-25 19:40:30

标签: c# language-features yield

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

我知道存在类似的问题。 但是没有一个能用简单的语言解释它的用法。

9 个答案:

答案 0 :(得分:32)

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

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


问:为什么我不在这里写一个更长的答案(从评论中解释);简单。正如Eric Lippert观察到的那样(here),yield构造(及其背后的魔力)是C#编译器中单一最复杂的代码,并且试着在一个简短的回复中描述它在这里是天真的。 yield有很多细微差别,IMO最好引用预先存在的(并且完全合格的)资源。

Eric的博客现在有7个条目(这只是最近的条目)讨论yield。我对Eric有很强烈的尊重,但他的博客可能更适合作为 对这个主题感到满意的人的“进一步信息”({{1}在这种情况下),因为它通常描述了许多背景设计注意事项。最好在合理的基础上完成。

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

答案 1 :(得分:30)

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

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

鉴于上述情况,编译器将生成一个实现IEnumerator<int>IEnumerable<int>IDisposable的类(实际上它还将实现IEnumerable的非泛型版本和{ {1}})。

这允许您在IEnumerator循环中调用方法SequenceOfOneToThree,如下所示

foreach

迭代器是状态机,因此每次调用foreach(var number in SequenceOfOneToThree) { Console.WriteLine(number); } 时,都会记录方法中的位置。如果迭代器移动到下一个元素,则该方法在此位置后立即恢复。所以第一次迭代返回1并标记该位置。下一个迭代器在一个之后重新开始,因此返回2,依此类推。

毋庸置疑,您可以以任何您喜欢的方式生成序列,因此您不必像我那样对数字进行硬编码。此外,如果您想要打破循环,可以使用yield

答案 2 :(得分:18)

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

收益率收益率和收益率中断报表通常用于提供收集的“延期评估”。

这意味着当你获得使用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.");

答案 3 :(得分: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;
        }
    }
}

答案 4 :(得分: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);
        }
    }
}

答案 5 :(得分:4)

Eric White的series on functional programming完全值得阅读,但entry on Yield就像我看到的一样清楚。

答案 6 :(得分:3)

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

答案 7 :(得分:2)

我提出这个来克服.NET必须手动深度复制List的缺点。

我用这个:

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,但由于无法在匿名方法块中工作,因此无法实现。

编辑:

更好的是,使用通用的List cloner:

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

答案 8 :(得分:0)

让我补充一切。收益率不是关键字。 它只有在你使用“yield return”时才有效,除了它将像普通变量一样工作。

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