C# Tips to Avoid Modifying Bound Variables

First look at a piece of code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#region test1 Closure

        public static void test1()
        {
            int index = 0;
            Func<IEnumerable<int>> sequence =()=>GetEnumrableInt(index);

            index = 20;
            foreach(int n in sequence())
                Console.WriteLine(n);

            Console.WriteLine("Done");

            index = 100;
            foreach (int n in sequence())
                Console.WriteLine(n);
        }


        public static IEnumerable<int> GetEnumrableInt(int index)
        {
            List<int> l = new List<int>();
            for(int i=index;i<index+30;i++)
            {
                l.Add(i);
            }
            return l;
        }

        #endregion

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//Our lambda expression
        public static void test2()
        {
            int[] someNum = {0,1,2,3,4,5,6,7,8,9,10 };

            IEnumerable<int> ans = from n in someNum
                                   select n * n;

            foreach (int i in ans)
                Console.WriteLine(i);

        }

The compiler will use static delegates to implement the n*n lambda expression, and the code generated for the above code is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
         //Code generated by the compiler for our lambda
        #region Equivalent to test2()
        private static int HiddenFunc(int n)
        {
            return n * n;
        }
        //Static delegate
        private static Func<int, int> HiddenDelegate;

        public void test2_1()
        {

            int[] someNum = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

            if(HiddenDelegate==null)
            {
                HiddenDelegate = new Func<int, int>(HiddenFunc);
            }
            IEnumerable<int> ans = someNum.Select<int, int>(HiddenDelegate);

          foreach(int i in ans)
              Console.WriteLine(i);

        }
        #endregion

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    public class ModFilter
    {
        private readonly int modules;

        public ModFilter(int mod)
        {
            modules = mod;
        }


        public IEnumerable<int> FindValues(IEnumerable<int> sequence)
        {

            return from n in sequence
                   where n % modules == 0 //Newly added expression
                   select n * n;  //Same as the previous example
        }
    }



/* 

In this case, the compiler will create an instance method to wrap the delegate for the expression.
The basic concept is the same as the previous case, except that an instance method is used here to read and modify the state of the current object.
Like the static delegate example, here the compiler will convert the lambda expression into familiar code. Which includes the definition of the delegate and the method call.
As follows:

*/



    public class ModFilter_Other
    {
        private readonly int modules;


        //Instance method
        private bool WhereClause(int n)
        {
            return ((n%this.modules) ==0);
        }


        private static int SelectClause(int n)
        {
            return n * n;
        }

        private static Func<int, int> SelectDelegate;




        public ModFilter_Other(int mod)
        {
            modules = mod;
        }


        public IEnumerable<int> FindValues(IEnumerable<int> sequence)
        {
            if(SelectDelegate==null)
            {
                SelectDelegate = new Func<int, int>(SelectClause);
            }

            return sequence.Where<int>(
                new Func<int, bool>(this.WhereClause)).
                Select<int, int>(SelectClause);
        }
    }

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
		  public class ModFilter
		  {
		        private readonly int modules;
		
		        public ModFilter(int mod)
		        {
		            modules = mod;
        }
        
        
        public IEnumerable<int> FindValues(IEnumerable<int> sequence)
        {
            int numValues = 0;
            
            return from n in sequence
                   where n % modules == 0 //Newly added expression
                   select n * n / ++ numValues; //Access local variable
        }
      }


Note that the select clause needs to access the local variable numValues. In order to create this closure, the compiler needs to use a nested type to implement the behavior you need. Below is the code generated by the compiler for you.




 	 public class ModFilter
     {
        private sealed class Closure
        {
            public ModFilter outer;

            public int numValues;

            public int SelectClause(int n)
            {
                return ((n * n) / ++this.numValues);
            }
        }



        private readonly int modules;


        //Instance method
        private bool WhereClause(int n)
        {
            return ((n % this.modules) == 0);
        }

        public ModFilter(int mod)
        {
            modules = mod;
        }


        public IEnumerable<int> FindValues(IEnumerable<int> sequence)
        {
            Closure c = new Closure();
            c.outer = this;
            c.numValues = 0;

            return sequence.Where<int>(
                new Func<int, bool>(this.WhereClause)).
                Select<int, int>(c.SelectClause);
        }
    }

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.

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy