First look at a piece of code
| |
The above code demonstrates the situation where external variables are used in closures, and then these variables are modified externally, resulting in outputting numbers from 20-50, and then outputting numbers between 100-130. This behavior is a bit weird, but it does have meaning… (The book says so, I think it’s rarely used.)
To convert query expressions into executable code, the C# compiler does a lot of work. Generally speaking, the C# compiler converts queries and lambda expressions into “static delegates”, “instance delegates” or “closures”. The compiler will choose an implementation method based on the code in the lambda expression. Which method is chosen depends on the body of the lambda expression. This seems to be some implementation details of the language, but it can significantly affect our code. The choice of implementation by the compiler may lead to subtle changes in behavior.
Not all lambda expressions generate code with the same structure.
For the compiler, the simplest behavior is to generate delegates for code in the following form.
| |
The compiler will use static delegates to implement the n*n lambda expression, and the code generated for the above code is as follows:
| |
The body of this lambda expression does not access any instance variables or local variables. The lambda expression only accesses its parameters. In this case, the C# compiler will create a static method as the target of the delegate. This is also the simplest processing performed by the compiler. If the expression can be implemented through a private static method, the compiler will generate the private static method and the corresponding delegate definition. For the code example above and expressions that only access static variables, the compiler will adopt this approach.
Next, introduce another relatively simple situation:
The lambda expression needs to access the instance variables of the type, but does not need to access the local variables in the outer method.
| |
In summary: If the code in the lambda expression accesses member variables in the object instance, then the compiler will generate instance methods to represent the code in the lambda expression. In fact, there is nothing special about this—the compiler saves us some code input work, and the code becomes much cleaner. Essentially, this is still an ordinary method call.
However, if the lambda expression accesses local variables or method parameters in the outer method, then the compiler will do a lot of work for you.
Closures are used here. The compiler will generate a private nested type to implement closures for local variables.
Local variables must be passed into the delegate that implements the body of the lambda expression.
In addition, all modifications made by the lambda expression to these local variables must be accessible externally.
Of course, there may be more than one variable shared between the inner and outer layers in the code, and there may be more than one query expression.
Let’s modify the instance method to access a local variable.
| |
In the above code, the compiler specifically creates a nested class to contain all variables that will be accessed or modified in the lambda expression. In fact, these local variables will be completely replaced by the fields of the nested class. The code inside the lambda expression and the code outside (but still in the current method) access the same field, and the logic in the lambda expression is also compiled into a method of the inner class.
For the parameters of the outer method that will be used in the lambda expression, the compiler will also implement them in the same way as local variables: the compiler will copy these parameters into the nested class representing the closure.
Going back to the initial example, this is something we should be able to understand this seemingly strange behavior. The variable index was passed into the closure, but was modified by external code before the query started executing. That is to say, you modified the internal state of the closure, and then expected it to return to its previous state to start executing, which is obviously impossible.
Considering the interaction in deferred execution and the way the compiler implements closures, modifying variables bound between the query and external code may cause erroneous behavior.
Therefore, we should try to avoid modifying variables in methods that will be passed into closures and used in closures.