Delay calculation of yield and streams under of property iterator C

  • 2020-12-13 19:03:23
  • OfStack

Traversal from 0 to 20(not including 20), outputs each element traversed to, and places all numbers greater than 2 into one IEnumerable < int > In return

Solution 1 :(I used to do this a lot)


static IEnumerable<int> WithNoYield()
    {
      IList<int> list = new List<int>();
      for (int i = 0; i < 20; i++)
      {
        Console.WriteLine(i.ToString());
        if(i > 2)
          list.Add(i);
      }
      return list;
    }

Solution 2 :(we can still do this since C# 2.0)


static IEnumerable<int> WithYield()
    {
      for (int i = 0; i < 20; i++)
      {
        Console.WriteLine(i.ToString());
        if(i > 2)
          yield return i;
      }
    }

If I test with code like this, what output do I get?
Test 1:

Test WithNoYield ()


static void Main()
        {
            WithNoYield();
            Console.ReadLine();
        }

Test WithYield ()


static void Main()
        {
            WithYield();
            Console.ReadLine();
        }

Test 2:
Test WithNoYield ()


static void Main()
        {
            foreach (int i in WithNoYield())
            {
                Console.WriteLine(i.ToString());
            }
            Console.ReadLine();
        }

Test WithYield ()


static void Main()
        {
            foreach (int i in WithYield())
            {
                Console.WriteLine(i.ToString());
            }
            Console.ReadLine();
        }

Give you five minutes to answer the question. Don't run it on the computer

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 5 minutes * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

Test the results of 1
Test WithNoYield() : Outputs numbers from 0 to 19
Test WithYield() : Output nothing
Test 2 results
Test WithNoYield(): Output 1-19 followed by output 3-19
Test WithYield() : Output 12334455... .
(To save space, the above answer is not pasted as is, so you can run the test by yourself.)

Isn't it strange that the program using yield behaves so strangely?

The WithYield() test in Test 1 did not execute an for loop at all, even though the method was called and there was not one line of output? Debug with breakpoint and sure enough, the for loop doesn't go in at all. What's wrong with that? The test output for WithYield() in Test 2 is available, but why is the output so interesting? Interspersed with the output, while foreach traverses the results of WithYield(), it seems that WithYield() does not exit until the last traversal is completed. What is this?

So let's open up the IL code and look at 1 and see what's going on

IL code of Main method:


.method private hidebysig static void Main() cil managed
{
  .entrypoint
  .maxstack 1
  .locals init (
    [0] int32 i,
    [1] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000)
  L_0000: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> TestLambda.Program::WithYield()
  L_0005: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
  L_000a: stloc.1 
  L_000b: br.s L_0020
  L_000d: ldloc.1 
  L_000e: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
  L_0013: stloc.0 
  L_0014: ldloca.s i
  L_0016: call instance string [mscorlib]System.Int32::ToString()
  L_001b: call void [mscorlib]System.Console::WriteLine(string)
  L_0020: ldloc.1 
  L_0021: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
  L_0026: brtrue.s L_000d
  L_0028: leave.s L_0034
  L_002a: ldloc.1 
  L_002b: brfalse.s L_0033
  L_002d: ldloc.1 
  L_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
  L_0033: endfinally 
  L_0034: call string [mscorlib]System.Console::ReadLine()
  L_0039: pop 
  L_003a: ret 
  .try L_000b to L_002a finally handler L_002a to L_0034
}

Nothing new here, as I've already analyzed in the previous article, is that inside foreach is the MoveNext() method that is converted to call the iterator for the while loop. I browse to WithYield() method:


private static IEnumerable<int> WithYield()
{
    return new <WithYield>d__0(-2);
}

What's going on? Is this the code I wrote? What about my for loop? After I confirm again 3, it is indeed generated by the code I wrote. I secretly shouted, compiler, how can you be so "shameless", behind the modification of my code, you do not infringe. And gave me a freshman class < WithYield > d___, the class implements these interfaces: IEnumerable < int > , IEnumerable, IEnumerator < int > , IEnumerator, IDisposable (ok, this class implements both the enumerator interface and the iterator interface)
Now you can answer the question why there is no output from test 1. When I call WithYield(), I call 1 < WithYield > The construction method of d___, < WithYield > The constructor code for d___ :


public <WithYield>d__0(int <>1__state)
    {
        this.<>1__state = <>1__state;
        this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
    }

There's no output here.
In Test 2, first we will call < WithYield > The d___ () method with 1 integer local variable < > Start with 0 per ___, and look at the code with the MoveNext() method:


private bool MoveNext()
  {
    switch (this.<>1__state)
    {
      case 0:
        this.<>1__state = -1;
        this.<i>5__1 = 0;
        goto Label_006A;

      case 1:
        this.<>1__state = -1;
        goto Label_005C;

      default:
        goto Label_0074;
    }
  Label_005C:
    this.<i>5__1++;
  Label_006A:
    if (this.<i>5__1 < 20)
    {
      Console.WriteLine(this.<i>5__1.ToString());
      if (this.<i>5__1 > 2)
      {
        this.<>2__current = this.<i>5__1;
        this.<>1__state = 1;
        return true;
      }
      goto Label_005C;
    }
  Label_0074:
    return false;
  }

Console. WriteLine in our for loop is here, so the output in for will not be executed until MoveNext() is called, because MoveNext() is accessed every time, so WithYield() will not exit until the element in the return result has been traversed. Now we can find evidence for the weird behavior of our test program, which is that the compiler is doing something in the background.

Actually this implementation is supported in theory: delay calculation (Lazy evaluation or delayed evaluation) can find it on Wiki explanation: the computation delay until needed calculation result of the calculation, because so that you can avoid some unnecessary calculation and improve the performance, in the synthesis of 1 some expressions can also avoid some unnecessary conditions, because other computing has been completed at this time, all the conditions have been identified, some fundamental conditions can be inaccessible. Anyway, there are a lot of benefits.

Delay calculation from the functional programming, in functional programming, the function as a parameter to pass, what do you want to ah, if the function 1 pass was calculated, that's what make, if you use the delay calculation, the expression in the absence of use is not be calculated, such as there is one application: x = expression, to assign the expression x variables, but if the x do not use in other places this expression is not be calculated, before it is filled x this expression.

Don't worry, the whole Linq is built on top of this, and the delay is a huge help to Linq. (Was Microsoft planning for Linq in 2.0?) , look at the following code:


var result = from book in books
  where book.Title.StartWiths( " t " )
  select book
if(state > 0)
{
  foreach(var item in result)
  {
    // ... .
}
}

result is one implementation of IEnumerable < T > Interface class (in Linq, all implementations of IEnumerable < T > The class of the interface is called sequence), and access to its foreach or while must go through its corresponding IEnumerator < T > The MoveNext() method, if we put 1 time-consuming or delayed operation in MoveNext(), those operations will only be performed when MoveNext() is accessed, that is, result is used, while assignments, passes, and so on to result are not performed.

If the above code ends up with no requirement for result since state is less than 0, the result returned in Linq is IEnumerable < T > If there is no delay computation, then the Linq expression is white. If it is Linq to Objects also slightly better, if it is Linq to SQL, and the database table and is very big, it's not worth the cost, so Microsoft came up with this, here using the delay calculation, only wait for application elsewhere use result computing Linq here the value of the expression, such Linq performance also improved than before, In addition, Linq to SQL finally generates SQL statement. For SQL statement generation, if the generation is delayed, some conditions are determined first, so that the generation of SQL statement can be more concise. And, as a result of MoveNext () is a step by step, cycle time perform 1 time, so if you have this kind of circumstance, we traverse the judge one at a time, do not meet our conditions we can quit, if there are 10000 elements need to traverse, when traversing to the second did not meet the conditions, at this time we can rejoin the, behind so many elements are in fact not processing, the element has not been loaded into memory.

There are many other nice things about delayed computing that you might be able to program in this way in the future. Write here I suddenly thought of Command mode, Command mode method encapsulated into class, Command objects such as delivery time will not perform any thing, just call it inside the method he can perform, so that we can put the command hair everywhere, still can stack pressure and so on and don't worry about Command is processed in the process of transfer, maybe this is one kind of delay calculation.

This paper is only a very shallow talk about 1 delay computing things, from here can also involve in the concurrent programming model and collaborative programs and other content, because I have little knowledge, so can only be introduced to this point, some of the above is my personal understanding, there must be a lot of improper, welcome to clap your hands.

foreach,yield, this thing that we often use actually hides so many wonderful places behind, I also just know today, it seems that the future is far far away.

The way ahead is so long without ending, yet high and low I'll search with my will unbending.


Related articles: