Understanding C# Features (6) Closure

[LINQ via C#] - [C# Features]

Non-local variable

In a C# class, it is perfectly nature normal thing for a method to access a variable defined inside or outside its body, e.g.:

public class DisplayClass
{
    int nonLocalVariable = 0; // Outside the scope of method Add.

    public int Add()
    {
        int localVariable = 1; // Inside the scope of method Add.
        return localVariable + nonLocalVariable; // 1.
    }
}

Here in DisplayClass, the field is defined outside the scope of the method, so that it can be viewed as a non-local variable of method, in contrast of the local variable defined inside method scope. Non-local variable is also called captured variable. This tutorial uses term non-local variable, because it is more specific.

The concept of non-local variable also applies to lambda expression:

public static partial class Closure
{
    public static void Outer()
    {
        int nonLocalVariable = 0; // Outside the scope of function add.
        Func<int> add = () =>
            {
                int localVariable = 1; // Inside the scope of function add.
                return localVariable + nonLocalVariable;
            };

        int result = add(); // 1;
    }
}

nonLocalVariable is defined outside the scope of function add, so it is a non-local variable of add, in contrast of the local variable defined inside add. This capability for a function or method to reference a non-local value, is called closure.

Compilation

In above lambda expression example, nonLocalVariable is created in the scope of outer method Lambda, and it does not exist at all in the scope of inner function add. How does this function access nonLocalVariable? Above DisplayClass example is the answer:

public static class CompiledClosure
{
    [CompilerGenerated]
    private sealed class DisplayClass0
    {
        public int nonLocalVariable;

        internal int Add()
        {
            int localVariable = 1;
            return localVariable + this.nonLocalVariable;
        }
    }

    public static void Outer()
    {
        DisplayClass0 displayClass0 = new DisplayClass0();
        displayClass0.nonLocalVariable = 0;
        Func<int> add = displayClass0.Add;
        int result = add(); // 1.
    }
}

C# compiler generates:

  • A inner class (DisplayClass0) to host the lambda expression; if there are more lambda expressions accessing non-local variables, more inner classes (DisplayClass1, …) will be generated to host these lambda expressions.
  • A method (Add) to represent the function (add)
  • A field to represent the non-local variable (nonLocalVariable). If there are more non-local variables accessed by that lambda expression, more fields will be generated to represent each of these non-local variables.

The generated logic becomes exactly the same case as the initial example. Accessing non-local variable becomes accessing field of the same class, naturally.

In the Outer method, the inner add function creation becomes the instantiation of DisplayClass0. the non-local variable is passed by assigning it to the corresponding field. And, of course, the inner function call becomes a normal method call. C# closure is such a powerful syntactic sugar, which greatly simplifies the code.

Non-local variable can change

In above examples, non-local variables does not change. But if they changes,  of course the referencing functions will be impacted, e.g.:

public static void ChangedNonLocal()
{
    int nonLocalVariable = 1; // Outside the scope of function add.
    Func<int> add = () =>
    {
        int localVariable = 0; // Inside the scope of function add.
        return localVariable + nonLocalVariable;
    };

    nonLocalVariable = 2; // Non-local variable can change.
    int result = add(); // 2 instead of 1.
}

Sometimes, this can be confusing:

public static void MultipleReferences()
{
    List<Func<int>> functions = new List<Func<int>>(3);
    for (int nonLocalVariable = 0; nonLocalVariable < 3; nonLocalVariable++) // Outside the scope of function print.
    {
        Func<int> function = () => nonLocalVariable; // nonLocalVariable: 0, 1, 2.
        functions.Add(function);
    }

    // Now nonLocalVariable is 3.
    foreach (Func<int> function in functions)
    {
        int result = function();
        Trace.WriteLine(result); // 3, 3, 3 instead of 0, 1, 2.
    }
}

In this case, 3 functions are created by the for loop. The nonLocalVariable is 0, 1, 2, when each function is created. However, when the for loop finishes executing, nonLocalVariable becomes 3. So when calling each of these 3 functions, the output will be 3, 3, 3 instead of 0, 1, 2.

This can be resolved by copying the current value of nonLocalVariable:

public static void CopyCurrent()
{
    List<Func<int>> functions = new List<Func<int>>(3);
    for (int nonLocalVariable = 0; nonLocalVariable < 3; nonLocalVariable++) // Outside the scope of function print.
    {
        int copyOfCurrentValue = nonLocalVariable; // nonLocalVariable: 0, 1, 2.
        // When nonLocalVariable changes, copyOfIntermediateState does not change.
        Func<int> function = () => copyOfCurrentValue; // copyOfCurrentValue: 0, 1, 2.
        functions.Add(function);
    }

    // Now nonLocalVariable is 3. Each copyOfCurrentValue does not change.
    foreach (Func<int> function in functions)
    {
        int result = function();
        Trace.WriteLine(result); // 0, 1, 2.
    }
}

Hidden reference

The closure syntactic sugar enables direct access to non-local variable. This convenience has a price. Closure can also be performance pitfall, because a hidden reference is persisted by the generated DisplayClass’s field. As a result, the non-local variable’s lifetime can be extended  by closure. In the last example, copyOfCurrentValue is a temporary variable inside the for loop block, but its value is not gone after each iteration. After 3 iterations, the 3 copyOfCurrentValue values are still persisted by 3 functions, so that later the functions can use each of the values.

Here is another intuitive example:

public static partial class Closure
{
    private static Func<int> longLifeFunction;

    public static void Reference()
    {
        // https://msdn.microsoft.com/en-us/library/System.Array.aspx
        byte[] shortLifeVariable = new byte[0X7FFFFFC7];
        // Some code...
        longLifeFunction = () =>
        {
            // Some code...
            byte value = shortLifeVariable[0]; // Reference.
            // More code...
            return 0;
        };
        // More code...
    }
}

If Reference method is called, a closure will be created:

  • A lambda expression is created, and it persists a reference to its non-local variable shortLifeVariable.
  • Then the lambda expression is persisted by Closure class’s static field longLifeFunction

Here shortLifeVariable is no longer a short lifetime temporary variable inside method Reference. Its  lifetime is extended to be the same as longLifeFunction, which can be forever. When Reference method finishes executing, the allocated memory for the big byte array cannot be garbage collected. In closure, the reference can be very unapparent and unobvious. Other languages with closure support, like VB, F#, JavaScript, etc., have the same issue too. Closure must be used with caution.

14 Comments

  • I want to know if the implementation below is correct as this can capture changes to nonLocalVariable as shown in example ChangedNonLocal()

    public static void Outer()
    {
    DisplayClass0 displayClass0 = new DisplayClass0();
    displayClass0.nonLocalVariable = nonLocalVariable; // original code displayClass0.nonLocalVariable = 0;
    Func<int> add = displayClass0.Add;
    int result = add(); // 1.
    }

    Is it an Referenced outer variable's life time an issue ? In c++ of language I have made mistake of passing reference but unconsciously clearing referenced variable. Compared to that is not c#'s approach better ? Is there a way to avoid this overhead (life of outer variable).

    Regards
    Nagendra

  • @Nagendra This reference is an issue. Because this reference is generated by compiler, and cannot be seen in the code. C#'s approach is no better, just like any other language supporting closure. To avoid this issue, Resharper can detect this kind of reference: in C# code: https://www.jetbrains.com/resharper/

  • خرید لایک اینستاگرام
    فالوور اینستاگرام

  • thanks for post

  • Thank you for publishing this article

  • https://ma-study.blogspot.com/

  • Your content is really new to me. i like to read very much. I really enjoyed seeing you write such great articles. And I don't think I can find anywhere else.

  • From some point on, I am preparing to build my site while browsing various sites. It is now somewhat completed. If you are interested, please come to play with <a href="https://toolbarqueries.google.it/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">casinosite</a> !!

  • I saw your article well. You seem to enjoy <a href="https://toolbarqueries.google.is/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">baccaratsite</a> for some reason. We can help you enjoy more fun. Welcome anytime :-)

  • It's too bad to check your article late. I wonder what it would be if we met a little faster. I want to exchange a little more, but please visit my site <a href="https://toolbarqueries.google.iq/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">slotsite</a> and leave a message!!

  • Your explanation is organized very easy to understand!!! I understood at once. Could you please post about <a href="https://toolbarqueries.google.im/url?sa=t&url=https%3A%2F%2Fwww.mtclean.blog/">baccaratcommunity</a> ?? Please!!

  • Arson can also result in an increase in the rate of homeowners and business insurance policies. Before going to the topic, does home insurance cover arson? Let’s look at why homeowners should be concerned about arson.

  • Atau situs taruhan bola online mana yang paling bisa diandalkan?

  • Temukan link login tokyo88 sebagai situs alternatif utama tokyoslot88 tanpa adanya blokiran internet positif, Klik link tokyo88 disini untuk memudahakan login kedalam permainan slot anda.

Add a Comment

As it will appear on the website

Not displayed

Your website