Can someone demystify the yield keyword?

2019-01-21 10:51发布

问题:

I have seen the yield keyword being used quite a lot on Stack Overflow and blogs. I don't use LINQ. Can someone explain the yield keyword?

I know that similar questions exist. But none really explain what is its use in plain simple language.

回答1:

By far the best explanation of this (that I've seen) is Jon Skeet's book - and that chapter is free! Chapter 6, C# in Depth. There is nothing I can add here that isn't covered.

Then buy the book; you will be a better C# programmer for it.


Q: Why didn't I write a longer answer here (paraphrased from comments); simple. As Eric Lippert observes (here), the yield construct (and the magic that goes behind it) is the single most complex bit of code in the C# compiler, and to try and describe it in a brief reply here is naïve at best. There are so many nuances to yield that IMO it is better to refer to a pre-existing (and fully qualified) resource.

Eric's blog now has 7 entries (and that is just the recent ones) discussing yield. I have a vast amount of respect for Eric, but his blog is probably more appropriate as a "further information" for people who are comfortable with the subject (yield in this case), as it typically describes a lot of the background design considerations. Best done in the context of a reasonable foundation.

(and yes, chapter 6 does download; I verified...)



回答2:

The yield keyword is used with methods that return IEnumerable<T> or IEnumerator<T> and it makes the compiler generate a class that implements the necessary plumbing for using the iterator. E.g.

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

Given the above the compiler will generate a class that implements IEnumerator<int>, IEnumerable<int> and IDisposable (actually it will also implement the non-generic versions of IEnumerable and IEnumerator).

This allows you to call the method SequenceOfOneToThree in a foreach loop like this

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

An iterator is a state machine, so each time yield is called the position in the method is recorded. If the iterator is moved to the next element, the method resumes right after this position. So the first iteration returns 1 and marks that position. The next iterator resumes right after one and thus returns 2 and so forth.

Needless to say you can generate the sequence in any way you like, so you don't have to hard code the numbers like I did. Also, if you want to break the loop you can use yield break.



回答3:

In an effort to demystify I'll avoid talking about iterators, since they could be part of the mystery themselves.

the yield return and yield break statements are most often used to provide "deferred evaluation" of the collection.

What this means is that when you get the value of a method that uses yield return, the collection of things you are trying to get don't exist together yet (it's essentially empty). As you loop through them (using foreach) it will execute the method at that time and get the next element in the enumeration.

Certain properties and methods will cause the entire enumeration to be evaluated at once (such as "Count").

Here's a quick example of the difference between returning a collection and returning yield:

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

This can also be used if you need to get a reference to the Enumeration before the source data has values. For example if the names collection wasn't complete to start with:

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


回答4:

The yield keyword is a convenient way to write an IEnumerator. For example:

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

is transformed by the C# compiler to something similiar to:

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;
        }
    }
}


回答5:

Take a look at the MSDN documentation and the example. It is essentially an easy way to create an iterator in 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);
        }
    }
}


回答6:

Eric White's series on functional programming it well worth the read in it's entirety, but the entry on Yield is as clear an explanation as I've seen.



回答7:

yield is not directly related to LINQ, but rather to iterator blocks. The linked MSDN article gives great detail on this language feature. See especially the Using Iterators section. For deep details of iterator blocks, see Eric Lippert's recent blog posts on the feature. For the general concept, see the Wikipedia article on iterators.



回答8:

I came up with this to overcome a .NET shortcoming having to manually deep copy List.

I use this:

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

And at another place:

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

I tried to come up with oneliner that does this, but it's not possible, due to yield not working inside anonymous method blocks.

EDIT:

Better still, use a generic 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();
        }
    }
}


回答9:

Let me add to all of this. Yield is not a keyword. It will only work if you use "yield return" other than that it will work like a normal variable.

It's uses to return iterator from a function. You can search further on that. I recommend searching for "Returning Array vs Iterator"