Why does the lambda expression save the values ​​of the variable scope after the method finishes?

I got the impression that the lambda expression contexts in C # contain references to the scope variables of the parent function that are used in them. Consider:

public class Test { private static System.Action<int> del; public static void test(){ int i = 100500; del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i); del(1); i = 10; del(1); } public static void Main() { test(); } } 

exits

 param = 1, i = 100500 param = 1, i = 10 

However, if that were true, the following would be illegal, since in the context of lambda, a reference to a local variable went beyond the scope:

 public class Test { private static System.Action<int> del; public static void test(){ int i = 100500; del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i); } public static void Main() { test(); del(1); } } 

However, this compiles, runs, and outputs

 param = 1, i = 100500 

This means that something strange is happening or the context saves the values ​​of local variables, rather than referencing them. But if that were true, it would have to update them with every lambda call, and I don’t see how this will work when the original variables go beyond. Also, it looks like this could lead to overhead when dealing with large value types.

I know that, for example, in C ++ it is UB (confirmed in response to this question ).

The question is, is this behavior good in C #? (I think C # has some UB, or at least some IB, right?)

If it is clearly defined, how and why does it really work? (implementation logic will be interesting)

+6
source share
1 answer

The concept of closures related to lambda syntax in C # is a very big topic and too big for me to cover everything in this answer only, but at least try to answer a specific question. The actual answer is below, the rest between the background is necessary for understanding the answer.


What happens when the compiler tries to compile a method using anonymous methods is that it somehow rewrites the method.

Basically, a new class is created, and the anonymous method rises to this class. This gave a name, albeit an internal one, so for the compiler it is a kind of transition from an anonymous method to a named method. You, however, should not know or process this name.

Any variables that this method requires, variables that were declared in addition to the anonymous method, but in the same method that used / declared the anonymous method, will also be discarded, and then all changes to these variables will be overwritten.

There are several methods here, so it will be difficult for you to read the above text, so instead, give an example:

 public Func<int, int> Test1() { int a = 42; return value => a + value; } 

This method is rewritten something like this:

 public Func<int, int> Test1() { var dummy = new <>c__DisplayClass1(); dummy.a = 42; return dummy.<Test1>b__0; } internal class <>c__DisplayClass1 { public int a; public int <Test1>b__0(int value) { return a + value; } } 

The compiler can handle all these funky names (and yes, they are really named with all these brackets), because this applies to things with id identifiers and objects, the names are no longer a problem for the compiler. However, you can never declare a class or method with these names so that there is no risk that the compiler generates a class that just exists already exists.

Here is an LINQPad example that shows that the declared class, although with smaller brackets in its names, looks identical to the one generated by the compiler:

 void Main() { var f1 = Test1(); f1(10).Dump(); f1.Dump(); var f2 = Test2(); f2(10).Dump(); f2.Dump(); } public Func<int, int> Test1() { int a = 42; return value => a + value; } public Func<int, int> Test2() { var dummy = new __c__DisplayClass1(); dummy.a = 42; return dummy._Test2_b__0; } public class __c__DisplayClass1 { public int a; public int _Test2_b__0(int value) { return a + value; } } 

output:

LINQPad output

If you look at the screenshot above, you will notice two things for each delegate variable, the Method property and the Target property.

When a method is called, it is called using the this link, which refers to the Target object. Thus, the delegate captures two things: which method to call and the object to which it can be called.

Basically, this object of this generated class is saved as part of the delegate because it is a method object.


With all of this in mind, let's look at your question:

Why does the lambda expression save the values ​​of the variable scope after the method finishes?

A: If the lambda survives, all captured variables survive because they are no longer local variables of the method in which they were declared. Instead, they were raised to a new object, which also has a lambda method and thus “follows” the lambda wherever it goes.

+10
source

Source: https://habr.com/ru/post/984037/


All Articles