Forgot password?

What is yield return?

Dmitriy Fedyashov

The yield return operator is one of the least known among programmers using C#. At least among beginners. And even those who know something about it, are not completely sure that they understand the principle of its work correctly. This annoying gap must be corrected. And, I hope this article will help you with this.

The yield return operator returns the collection item in the iterator and moves the current position to the next element. The presence of the yield return operator turns the method into an iterator. Each time an iterator encounters a yield return, it returns a value.

This operator signals to us and the compiler that this expression is the iterator. The task of the iterator is to move between the elements of the collection and return the value of the current one. Many people are used to call the counter in the loop as an iterator, but this is not true, because the counter does not return a value.

The iterator is converted by the compiler into a "finite state machine", which tracks the current position and knows how to "move" to the next position. In this case, the value of the element of the sequence is calculated at the time of access to it.

Here is the simplest example of an iterator:

1
2
3
4
5
6
public static IEnumerable<int> GetItems()
{
 foreach (var i in List)
 {
 yield return i;
 }
}

 Iterators can return only the type IEnumerable <>.

Iterators are syntactic shortcuts for a more complex enumerator pattern. When the C # compiler encounters an iterator, it extends its contents to the CIL code that implements the enumerator pattern. This encapsulation significantly saves the time for the programmer.

The first question that will arise in an inexperienced programmer: "Why should I use an iterator? I can perfectly display the sequence without it. "

Sure you can. Difference in approaches. The iterator allows you to do so-called "lazy computing". This means that the value of the element is evaluated only when it is requested.

To better understand how yield return works, we compare it with traditional cycles. By the examples everything becomes clear.

1)      Note that with yield return, we do not need to create an additional list to fill it with values. So we get memory savings, because we only need memory for the current element of the collection. Element-wise processing does not allocate memory, just a cache.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static IEnumerable<int> GetSequence()
 {
 Random rand = new Random();
 List<int> list = new List<int>();
 for (int i = 0; i < 3; i++)
 list.Add(rand.Next());
 return list;
 }
 
 static IEnumerable<int> GetSequence()
 {
 Random rand = new Random();
 for (int i = 0; i < 3; i++)
 yield return rand.Next();
 }

 2)      The ability not to calculate the result for the entire enumeration. This is the main advantage. Do you remember that yield returns a value at the time of its processing? In this example, we infinitely generate numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IEnumerable<int> GetInfinityWithIterator()
{
var i = 0;
while (true)
yield return ++i;
}
 
IEnumerable<int> GetInfinityWithLoop()
{
 var i = 0;
 var list = new List<int>();
 while (true)
 list.Add(++i);
 return list;
}

 Do you see the difference? Now you will understand everything:

1
2
3
4
foreach(var item in GetInfinityWithIterator().Take(5))
{
 Console.WriteLine(item);
}

 We use LINQ  operator Take, to limit the amount of sample. In the case of yield return, the loop stops at the fifth element.

1
2
3
4
foreach(var item in GetInfinityWithLoop().Take(5))
{
 Console.WriteLine(item);
}

 You can not interrupt the list filling. As a result, we get the error Out of memory.

3)      The ability to adjust the collection values after the iterator is executed. Since yield returns a collection element at the time of actual processing (when displaying the value of an element in the console, for example), we can change the elements of the collection even after the iterator is executed. An iterator does not actually return real values when you invoke it. The iterator knows where to get the values. And he will return them only when they are really needed. This is the so-called Lazy load.

1
2
3
4
5
6
7
8
9
10
11
12
13
IEnumerable<int> MultipleYieldReturn(IEnumerable<int> mass)
{
 foreach (var item in mass)
 yield return item * item;
} 
 
IEnumerable<int> MultipleLoop(IEnumerable<int> mass)
{
 var list = new List<int>();
 foreach (var item in mass)
 list.Add(item * item);
 return list;
}

 And now we will call these methods:

1
2
3
4
5
6
7
 var mass = new List<int>() { 1, 2, 3 }; 
 var MultipleYieldReturn = Helper.MultipleYieldReturn(mass);
 var MultipleLoop = Helper.MultipleLoop(mass);
 
 mass.Add(4);
 Console.WriteLine(string.Join(",",MultipleYieldReturn));
 Console.WriteLine(string.Join(",", MultipleLoop));

The result is expected:

 

After initializing the MultipleYieldReturn and MultipleLoop variables, we add one more element to the collection:            mass.Add(4);

1
2
 Console.WriteLine(string.Join(",",MultipleYieldReturn));
 Console.WriteLine(string.Join(",", MultipleLoop));

Have a look at the result:

 

At the time the results were output to the console, the collection contained a value of 4. Since yield return produces values at the time of their query, the iterator processed all the current values. The traditional loop was run when the MultipleLoop variable was initialized, and at that time the collection contained only 3 values.

 4)  Exception handling with yield return has nuances. The yield return statement can not be used in the try-catch section, just try-finally.

For example, how would we write without knowing the constraint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IEnumerable TransformData(List<string> data)
{
 foreach (string item in data)
 {
 try
 {
 yield return PrepareDataRow(item);
 }
 catch (Exception ex)
 {
 Console.Error.WriteLine(ex.Message);
 }
 }
}

 In this case, the catch block will never catch an error. It's all about delayed execution of yield return. We learn about the error only at the moment of real work with the data from the iterator. For example, when we output data from the iterator to the console. Until then, the iterator does not work with real data.

If you still need to "catch" the error in this iterator, then you can do this:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public IEnumerable TransformData(List<string> data)
{
 string text;
 foreach (string item in data)
 {
 try
 {
 text = PrepareDataRow(item);
 }
 catch (Exception ex)
 {
 Console.Error.WriteLine(ex.Message);
 continue;
 }
 yield return text;
 }
}

 Speaking of yield return operator, you cannot fail to mention the second operator with yield. This is a yield break. In its purpose, it is similar to the break operator, it's just used only in iterators. Here is a small example:

1
2
3
4
5
6
7
8
9
10
IEnumerable<int> GetNumbers()
{
int i = 0;
while (true)
{
if (i = 5)
yield break;
yield return i++;
}
}

 It is clearly seen from the example that when the value of 5 is reached, the iterator will end, but until then it will correctly return values.

 Let's sum up. When should you use yield return?

  • When listing objects. The iterator will work faster than the returned collection. And the memory overhead is lower;
  • In infinite cycles. Using the Take () method, you can always restrict the selection.
Similar articles:

back