Iterate from 0 to 20 (excluding 20), output every element it traversed to, and put all numbers greater than 2 into an IEnumerable<int> to return
Answer 1: (I used to do this)
static IEnumerable<int> WithNoYield() { IList<int> list = new List<int>(); for (int i = 0; i < 20; i++) { (()); if(i > 2) (i); } return list; }
Answer 2: (Since we have C# 2.0 we can still do this)
static IEnumerable<int> WithYield() { for (int i = 0; i < 20; i++) { (()); if(i > 2) yield return i; } }
If I use the following code to test, what kind of output will I get?
Test 1:
Test WithNoYield()
static void Main()
{
WithNoYield();
();
}
Test WithYield()
static void Main()
{
WithYield();
();
}
Test 2:
Test WithNoYield()
static void Main()
{
foreach (int i in WithNoYield())
{
(());
}
();
}
Test WithYield()
static void Main()
{
foreach (int i in WithYield())
{
(());
}
();
}
Give you 5 minutes to give the answer, don't run it on the machine
************************************************************************************
Test 1's calculation result
Test WithNoYield(): Output numbers from 0-19
Test WithYield(): Nothing output
Test 2's calculation results
Test WithNoYield(): Output 1-19 Then output 3-19
Test WithYield(): Output 12334455....
(To save space, the answer above is not pasted as is, you can run the test yourself)
Do you feel very strange? Why does the program using yield behave so weird?
In the test of WithYield() in Test 1, the method was called, but there was no output in a row. Could it be that the for loop was not executed at all? This is true through breakpoint debugging, and the for loop has not entered at all. What's going on? There is a test output for WithYield() in Test 2, but why is the output so interesting? With the output interspersed, when foreach traversing the result of WithYield(), it seems that without waiting for the last traversal to be completed, WithYield() does not exit. What's going on?
Open the IL code and see what happened
IL code of Main method:
.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init ( [0] int32 i, [1] class [mscorlib]`1<int32> CS$5$0000) L_0000: call class [mscorlib]`1<int32> ::WithYield() L_0005: callvirt instance class [mscorlib]`1<!0> [mscorlib]`1<int32>::GetEnumerator() L_000a: stloc.1 L_000b: L_0020 L_000d: ldloc.1 L_000e: callvirt instance !0 [mscorlib]`1<int32>::get_Current() L_0013: stloc.0 L_0014: i L_0016: call instance string [mscorlib]System.Int32::ToString() L_001b: call void [mscorlib]::WriteLine(string) L_0020: ldloc.1 L_0021: callvirt instance bool [mscorlib]::MoveNext() L_0026: L_000d L_0028: L_0034 L_002a: ldloc.1 L_002b: L_0033 L_002d: ldloc.1 L_002e: callvirt instance void [mscorlib]::Dispose() L_0033: endfinally L_0034: call string [mscorlib]::ReadLine() L_0039: pop L_003a: ret .try L_000b to L_002a finally handler L_002a to L_0034 }
There is nothing strange here. I have analyzed it in the previous article. In the foreach, it is converted to the MoveNext() method that calls the iterator for a while loop. I browse to the WithYield() method:
private static IEnumerable<int> WithYield()
{
return new <WithYield>d__0(-2);
}
Dizzy, what happened? Is this the code I wrote? Where is my for loop? After repeated confirmation, it was indeed generated by the code I wrote. I secretly scolded in my heart, compiler, how can you be so "shameless" and modify my code behind the scenes? Are you not infringing on the rights? I also generated a new class <WithYield>d__0, which implements several interfaces: IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable (Okay, this class implements both the enumeration interface and the iterator interface)
Now we can answer why there is no output in Test 1. Calling WithYield() means calling the constructor method of <WithYield>d__0, and the code of the constructor method of <WithYield>d__0:
public <WithYield>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = ;
}
There is no output here.
In Test 2, first we will call the GetEnumerator() method of <WithYield>d__0. In this method, an integer local variable <>1__state is initialized to 0, and then look at the code of 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) { (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; }
It turns out that the one in our for loop has come here, so the output in the for will not be executed without waiting for the MoveNext() call, because every traversal requires access to the MoveNext() method, so the element in the return result will not be exited without waiting for the return result. Now the weird behavior shown by our test program can be found, that is: the compiler is doing it in the background.
In fact, this implementation is theoretically supported: Lazy evaluation or delayed evaluation can be found on the Wiki: the delay will be calculated until the result of this calculation is needed, so that performance can be improved by avoiding some unnecessary calculations, and some unnecessary conditions can be avoided when synthesising some expressions, because at this time other calculations have been completed, all conditions have been clear, and some unreachable conditions can be ignored. Anyway, there are many benefits.
Delay calculation comes from functional programming. In functional programming, the function is passed as a parameter. Think about it, if this function is calculated as soon as it is passed, then what else can you do? If you use delay calculation, the expression will not be calculated when it is not used. For example, there is an application like this: x=expression, assign this expression to the x variable, but if x is not used elsewhere, the expression will not be calculated. Before this, x contained this expression.
It seems that this delay calculation is really a good thing. Don't worry, the whole Linq is built on it. This delay calculation has helped Linq a lot (did Microsoft start planning for its Linq in 2.0?), look at the following code:
var result = from book in books where (“t”) select book if(state > 0) { foreach(var item in result) { //…. } }
result is a class that implements the IEnumerable<T> interface (in Linq, all classes that implement the IEnumerable<T> interface are called sequences). Access to its foreach or while must be through its corresponding IEnumerator<T> MoveNext() method. If we put some time-consuming or delayed operations in MoveNext(), then those operations will be executed only when MoveNext() is accessed, that is, result is used, and allocate values to the result, passing, etc., and those time-consuming operations are not executed.
If the above code ends up with state less than 0 and there is no requirement for result, the results returned in Linq are all IEnumerable<T>. If there is no delay calculation here, then wouldn't the Linq expression be used in vain? It is a little better if it is Linq to Objects. If it is Linq to SQL, and the database table is large, it is really not worth the effort. So Microsoft thought of this and used delay calculation here. Only when the program uses result elsewhere will the value of the Linq expression be calculated. In this way, Linq's performance is much better than before. In addition, Linq to SQL will eventually generate SQL statements. For the generation of SQL statements, if the generation delay is to be determined first, some conditions will be determined first, and it can be more refined when generating SQL statements. Also, since MoveNext() is executed step by step, and the loop is executed once, so if there is such a situation: we traverse and judge it, and we exit if our conditions are not met. If there are ten thousand elements that need to be traversed, when the second one is traversed, the conditions are not met. At this time, we can exit. So many elements behind are actually not processed, and those elements are not loaded into memory.
Delay calculations also have many vivid characteristics, and maybe you can program in this way in the future. After writing this, I suddenly thought of the Command mode. The Command mode encapsulates the method into a class. The Command object will not execute anything when it is passed. Only when it is called the method inside it will be executed. In this way, we can send commands everywhere, and we can press the stack, etc. without worrying that the Command will be processed during the delivery process. Perhaps this is also a kind of delay calculation.
This article only briefly talks about delay calculations. From here, we can also involve more content such as concurrent programming models and collaborative programs. Since I am not knowledgeable, I can only introduce it to this point. Some of the above statements are my personal understanding. There must be many inappropriate aspects. Everyone is welcome to take a look.
foreach, yield, there are so many wonderful places hidden behind this thing we often use. I only realized today that it seems that the road ahead is still very far.
The road is long and arduous, and I will search up and down.