Home c# What is the benefit of Yield?

What is the benefit of Yield?

Author

Date

Category

Example on C #.

Return a collection using Yield .

public static class foo
{
  Public Static Ienumerable Test ()
  {
    var Rand = New Random (). Next (1, 3);
    if (RAND == 1)
      Yield Return 1;
    if (RAND == 2)
      Yield Return 2;
    Yield Return 3;
    Yield Return "Foo";
    Yield Return True;
  }
}

Example 2. Return a collection using a regular sheet.

public static class foo1
{
  Public Static Ienumerable Test ()
  {
    var list = new list & lt; object & gt; ();
    var Rand = New Random (). Next (1, 3);
    if (RAND == 1)
      List.Add (1);
    if (RAND == 2)
      List.Add (2);
    List.add (3);
    List.add ("foo");
    List.Add (True);
    RETURN LIST;
  }
}

The result is equivalent, the question is why then do you need Yield if you can do this code? Or Yield is used where code with new list () for some reason is impossible?


Answer 1, Authority 100%

Well, the difference is actually cardinal.

The fact is that in the first case you have lazy , and in the second – energetic response calculation. This means that the elements of the output sequence in the energetic case are calculated by everything immediately, and in the lazy case – only when they are requested and only those requested.

Let’s see where there is a difference from the practical side.

For the case of lazy calculation, the entire sequence is not fully present in memory. This means that with elementary processing, we do not stand out, and Cache locality is saved:

ienumerable & lt; int & gt; GenerateHugeseQuenelazy ()
{
  for (int i = 0; i & lt; 1000000; i ++)
    Yield Return 13 * i;
}
Ienumerable & lt; int & gt; GenerateHugeseQuenceeager ()
{
  Var Result = New List & LT; int & gt; ();
  for (int i = 0; i & lt; 1000000; i ++)
    result.add (13 * i);
  RETURN RESULT;
}

Calculate the function on the entire sequence, compare the memory consumption:

var seqlazy = generatehugesequenzy ();
// Calculate the maximum manually
var max = 0;
Foreach (VAR V in Seqlazy)
  If (V & GT; MAX)
    Max = V;
var Memlazy = GC.GettotalMemory (ForceFullCollection: False);
var seqeager = generateHugeseQuenceager ();
// Calculate the maximum manually
max = 0;
Foreach (VAR V in Seqeager)
  If (V & GT; MAX)
    Max = V;
var Memeager = GC.GetTotalMemory (ForceFullCollection: False);
Console.Writeline ($ "Memory Footprint Lazy: {Memlazy}, Eager: {Memeager}");

Result:

Memory Footprint Lazy: 29868, Eager: 6323088

Then, we have quite large differences in sense of operations. Energetic calculations are produced at the moment of calling the function, while lazy calculations occur at the time when you use the result. So, for the actual calculation of the lazy sequence, the state of the arguments will be taken at the time of the listing. Here is an example:

ienumerable & lt; int & gt; DoubleEager (IEnumerable & lt; int & gt; SEQ)
{
  Var Result = New List & LT; int & gt; ();
  Foreach (VAR E In SEQ)
    result.add (E * 2);
  RETURN RESULT;
}
Ienumerable & lt; int & gt; DoubleLazy (IEnumerable & lt; int & gt; SEQ)
{
  Foreach (VAR E In SEQ)
    Yield Return E * 2;
}

We look at the differences:

var seq = new list & lt; int & gt; () {1};
var eagerlydoubled = douleeager (SEQ);
var LazilyDoubled = DoubleLazy (SEQ);
Console.WriteLine ("Eager:" + String.Join (", EagerlyDoubled));
Console.WriteLine ("Lazy:" + String.join ("", LazilyDouble));
// displays both times 2, there is no difference 
seq.add (2); // modify * source * sequence
Console.WriteLine ("Eager:" + String.Join (", EagerlyDoubled)); // 2.
Console.WriteLine ("Lazy:" + String.join ("", LazilyDouble)); // 2 4.

Since the lazy calculation occurs when the list is , we see that when the sequence changes, the lazy version picks up changes.


Another example. Let’s see what will happen if we do not calculate the entire sequence. We calculate the same sequence vigorously and lazily:

ienumerable & lt; int & gt; Eager10 ()
{
  Console.Writeline ("Eager");
  INT Counter = 0;
  Try.
  {
    Var Result = New List & LT; int & gt; ();
    for (int i = 0; i & lt; 10; i ++)
    {
      Console.WriteLine ($ "adding: {i}");
      Counter ++;
      result.add (i);
    }
    RETURN RESULT;
  }
  Finally
  {
    Console.Writeline ($ Eagerly Computed: {Counter} ");
  }
}
Ienumerable & lt; int & gt; Lazy10 ()
{
  Console.WriteLine ("Lazy");
  INT Counter = 0;
  Try.
  {
    for (int i = 0; i & lt; 10; i ++)
    {
      Console.WriteLine ($ "adding: {i}");
      Counter ++;
      Yield Return I;
    }
  }
  Finally
  {
    Console.WriteLine ($ Lazily Computed: {Counter} ");
  }
}

We take only 2 elements from the result:

foreach (var e in eager10 (). Take (2))
  Console.WriteLine ($ Obtained: {E} ");
Foreach (VAR E IN Lazy10 (). Take (2))
  Console.WriteLine ($ Obtained: {E} ");
Foreach (VAR E In Lazy10 ())
{
  Console.WriteLine ($ Obtained: {E} ");
  IF (E == 1)
    Break;
}

We get this conclusion on the console:

eager
Adding: 0.
Adding: 1.
Adding: 2.
Adding: 3.
Adding: 4.
Adding: 5.
Adding: 6.
Adding: 7.
Adding: 8.
Adding: 9.
Eagerly Computed: 10
Obtained: 0.
Obtained: 1.
Lazy.
Adding: 0.
Obtained: 0.
Adding: 1.
Obtained: 1.
Lazily Computed: 2
Lazy.
Adding: 0.
Obtained: 0.
Adding: 1.
Obtained: 1.
Lazily Computed: 2

See the difference? The lazy version of the cycle was running only twice, and did not calculate the “tail” of the sequence.


Another difference between cases – when errors are reported. In the case of energetic calculation, they communicate immediately. In case of lazy – only when listed the result. Example:

ienumerable & lt; int & gt; Checkeagerly (Int Value)
{
  if (value == 0)
    Throw New ArgumentException ("Value Cannot Be 0");
  Return New List & LT; int & gt; {Value};
}
Ienumerable & lt; int & gt; Checklazily (Int Value)
{
  if (value == 0)
    Throw New ArgumentException ("Value Cannot Be 0");
  Yield Return Value;
}

Apply Try / Catch:

console.writeline ("Eager:");
Ienumerable & lt; int & gt; seqeager = null;
Try.
{
  seqeager = ChecKeagerly (0);
}
Catch (ArgumentException)
{
  Console.WriteLine ("Exception Caught");
}
if (Seqeager! = NULL)
  Foreach (VAR E In Seqeager)
    Console.Writeline (E);
Console.Writeline ("Lazy:");
Ienumerable & lt; int & gt; seqlazy = null;
Try.
{
  seqlazy = checklazily (0);
}
Catch (ArgumentException)
{
  Console.WriteLine ("Exception Caught");
}
If (Seqlazy! = NULL)
  Foreach (Var E in Seqlazy)
    Console.Writeline (E);

We get the result:

eager:
Exception Caught.
Lazy:
Unhandled Exception: System.argumentException: Value Cannot Be 0
  AT Program. & Lt; Checklazily & GT; D__3.MoveNext () in ... \ Program.cs: line 59
  AT Program.Run () in ... \ Program.cs: line 45
  AT Program.Main (String [] Args) in ... \ Program.cs: line 13

In order to get the “Best of both worlds”, that is, a lazy calculation, but an energetic check of arguments, the easiest way to split the function into two: energetic check and lazy calculation without checking. For modern versions of C #, it is convenient to use nested features:

ienumerable & lt; int & gt; CheckeagerlyEnumerateLazily (Int Value)
{
  if (value == 0)
    Throw New ArgumentException ("Value Cannot Be 0");
  Return impl ();
  Ienumerable & lt; int & gt; Impl ()
  {
    Yield Return Value;
  }
}

Check:

console.writine ("Recommended Way:");
Ienumerable & lt; int & gt; seqlazy = null;
Try.
{
  seqlazy = checkagerlyenumeratelazily (0);
}
Catch (ArgumentException)
{
  Console.WriteLine ("Exception Caught");
}
If (Seqlazy! = NULL)
  Foreach (Var E in Seqlazy)
    Console.Writeline (E);

and get

Recommended Way:
Exception Caught.

Another case of difference is the dependence on external data during the calculation process . The following code is trying to influence the calculations by changing the global state. (This is not a very good code, do not do so in real programs!)

bool evilmutableallowcompute;
Ienumerable & lt; int & gt; Eagerget5WITHEXTERNALDEPENDENCY ()
{
  List & lt; int & gt; Result = New List & lt; int & gt; ();
  for (int i = 0; i & lt; 5; i ++)
  {
    if (evilmutableallowcompute)
      result.add (i);
  }
  RETURN RESULT;
}
Ienumerable & lt; int & gt; Lazyget5WITHEXTERNALDEPENDENCY ()
{
  for (int i = 0; i & lt; 5; i ++)
  {
    if (evilmutableallowcompute)
      Yield Return I;
  }
}

Use:

console.writeline ("Eager:");
evilmutableallowcompute = true;
Foreach (Var E in Eagerget5WITHEXTERNALDEMENCY ())
{
  Console.WriteLine ($ Obtained: {E} ");
  IF (E & GT; 0)
    evilmutableallowcompute = false;
}
Console.Writeline ("Lazy:");
evilmutableallowcompute = true;
Foreach (VAR E IN Lazyget5WITHEXTERNALDEPENCY ())
{
  Console.WriteLine ($ Obtained: {E} ");
  IF (E & GT; 0)
    evilmutableallowcompute = false;
}

Result:

Eager:
Obtained: 0.
Obtained: 1.
Obtained: 2.
Obtained: 3.
Obtained: 4.
Lazy:
Obtained: 0.
Obtained: 1.

We see that the change in global data even after formal development of lazy features can affect calculations.

(this is another argument in favor of the fact that functional programming and mutable state are poorly combined.)


Answer 2, Authority 24%

Yes, in this case, the result is the same, but the “cost” of its production is different. Using Yield You create an optional list , which first fill the objects, additionally spending resources. With Yield , objects are returned “directly”.

In the first case, if foreach (Object Obj in foo.test ()) , for example, I will decide that I need only two elements, and I will make BREAK , on this everything will end. In the second case, if I need only two elements from the iterator, I have to wait until inside Foo1.Test () the internal list will be filled with all the elements from which already I will take only two.

Also with Yield I can return the elements infinitely or almost infinitely. Here, for example, an iterator that returns a sequence of triangular numbers:

ienumerable & lt; long & gt; Triangle ()
{
  Long T = 0;
  Long Next = 0;
  While (True)
  {
    T + = ++ NEXT;
    Yield Return T;
  }
}

Answer 3, Authority 7%

In the book of John Skeit on C #, the principle of operation of the Yield operator is very well disclosed.

If there is a Yield Return inside the method, the compiler builds on the basis of this method the finite machine. To implement the iterator, the final automatic has the following properties:

  • it has a certain initial state
  • When calling MoveNext () , code from the GETENUMERATOR () method for those software will be reached by the Yield Return operator;
  • When the current property is used, it returns the last
    Issued value.
  • he should know when the issuance of the values is completed to the method
    movenext () could return false ;

If briefly, the element value calculates at the moment when it appeals to it.

Programmers, Start Your Engines!

Why spend time searching for the correct question and then entering your answer when you can find it in a second? That's what CompuTicket is all about! Here you'll find thousands of questions and answers from hundreds of computer languages.

Recent questions