Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CSharpNotesForProfessionals.pdf
Скачиваний:
57
Добавлен:
20.05.2023
Размер:
6.12 Mб
Скачать

Chapter 120: Yield Keyword

When you use the yield keyword in a statement, you indicate that the method, operator, or get accessor in which it appears is an iterator. Using yield to define an iterator removes the need for an explicit extra class (the class that holds the state for an enumeration) when you implement the IEnumerable and IEnumerator pattern for a custom collection type.

Section 120.1: Simple Usage

The yield keyword is used to define a function which returns an IEnumerable or IEnumerator (as well as their derived generic variants) whose values are generated lazily as a caller iterates over the returned collection. Read more about the purpose in the remarks section.

The following example has a yield return statement that's inside a for loop.

public static IEnumerable<int> Count(int start, int count)

{

for (int i = 0; i <= count; i++)

{

yield return start + i;

}

}

Then you can call it:

foreach (int value in Count(start: 4, count: 10))

{

Console.WriteLine(value);

}

Console Output

4

5

6

...

14

Live Demo on .NET Fiddle

Each iteration of the foreach statement body creates a call to the Count iterator function. Each call to the iterator function proceeds to the next execution of the yield return statement, which occurs during the next iteration of the for loop.

Section 120.2: Correctly checking arguments

An iterator method is not executed until the return value is enumerated. It's therefore advantageous to assert preconditions outside of the iterator.

public static IEnumerable<int> Count(int start, int count)

{

// The exception will throw when the method is called, not when the result is iterated if (count < 0)

throw new ArgumentOutOfRangeException(nameof(count));

GoalKicker.com – C# Notes for Professionals

605

return CountCore(start, count);

}

private static IEnumerable<int> CountCore(int start, int count)

{

//If the exception was thrown here it would be raised during the first MoveNext()

//call on the IEnumerator, potentially at a point in the code far away from where

//an incorrect value was passed.

for (int i = 0; i < count; i++)

{

yield return start + i;

}

}

Calling Side Code (Usage):

// Get the count

var count = Count(1,10);

// Iterate the results foreach(var x in count)

{

Console.WriteLine(x);

}

Output:

1

2

3

4

5

6

7

8

9

10

Live Demo on .NET Fiddle

When a method uses yield to generate an enumerable the compiler creates a state machine that when iterated over will run code up to a yield. It then returns the yielded item, and saves its state.

This means you won't find out about invalid arguments (passing null etc.) when you first call the method (because that creates the state machine), only when you try and access the first element (because only then does the code within the method get ran by the state machine). By wrapping it in a normal method that first checks arguments you can check them when the method is called. This is an example of failing fast.

When using C# 7+, the CountCore function can be conveniently hidden into the Count function as a local function. See example here.

Section 120.3: Early Termination

You can extend the functionality of existing yield methods by passing in one or more values or elements that could define a terminating condition within the function by calling a yield break to stop the inner loop from executing.

GoalKicker.com – C# Notes for Professionals

606

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)

{

int curr = start;

while (true)

{

if (earlyTerminationSet.Contains(curr))

{

// we've hit one of the ending values yield break;

}

yield return curr;

if (curr == Int32.MaxValue)

{

// don't overflow if we get all the way to the end; just stop yield break;

}

curr++;

}

}

The above method would iterate from a given start position until one of the values within the earlyTerminationSet was encountered.

//Iterate from a starting point until you encounter any elements defined as

//terminating elements

var terminatingElements = new HashSet<int>{ 7, 9, 11 };

// This will iterate from 1 until one of the terminating elements is encountered (7) foreach(var x in CountUntilAny(1,terminatingElements))

{

// This will write out the results from 1 until 7 (which will trigger terminating)

Console.WriteLine(x);

}

Output:

1

2

3

4

5

6

Live Demo on .NET Fiddle

Section 120.4: More Pertinent Usage

public IEnumerable<User> SelectUsers()

{

// Execute an SQL query on a database.

using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))

{

GoalKicker.com – C# Notes for Professionals

607

while (reader.Read())

{

int id = reader.GetInt32(0); string name = reader.GetString(1); yield return new User(id, name);

}

}

}

There are other ways of getting an IEnumerable<User> from an SQL database, of course -- this just demonstrates that you can use yield to turn anything that has "sequence of elements" semantics into an IEnumerable<T> that someone can iterate over.

Section 120.5: Lazy Evaluation

Only when the foreach statement moves to the next item does the iterator block evaluate up to the next yield statement.

Consider the following example:

private IEnumerable<int> Integers()

{

var i = 0; while(true)

{

Console.WriteLine("Inside iterator: " + i); yield return i;

i++;

}

}

private void PrintNumbers()

{

var numbers = Integers().Take(3); Console.WriteLine("Starting iteration");

foreach(var number in numbers)

{

Console.WriteLine("Inside foreach: " + number);

}

}

This will output:

Starting iteration

Inside iterator: 0

Inside foreach: 0

Inside iterator: 1

Inside foreach: 1

Inside iterator: 2

Inside foreach: 2

View Demo

As a consequence:

GoalKicker.com – C# Notes for Professionals

608