Closures and unexpected pits in C#

Closures and unexpected pits in C#

Using delegates or lambda expressions, you can also use closures in C#.

最后更新 6/15/2022 10:49 PM
老胡写代码
预计阅读 5 分钟
分类
.NET
标签
.NET C#

Although closures are mainly functional programming, and the main feature of C#is object-oriented, C#can also write code with a flavor of functional programming using delegates or lambda expressions. Similarly, you can use closures in C#using delegates or lambda expressions.

According to WIKI's definition, closures, also known as grammatical closures or functional closures, are a technique for implementing grammatical binding in functional programming languages. A closure is implemented as a structure that stores a function (usually its entry address) and an associated environment (equivalent to a symbolic lookup table). Closures can also delay the lifetime of a variable.

Um... It seems a bit confusing to look at the definition. Let's take a look at the following example

class Program
{
    static Action CreateGreeting(string message)
    {
        return () => { Console.WriteLine("Hello " + message); };
    }

    static void Main()
    {
        Action action = CreateGreeting("DeathArthas");
        action();
    }
}

This example is very simple. Use a lambda expression to create an Action object and then call the Action object.

但是仔细观察会发现,当 Action 对象被调用的时候,CreateGreeting方法已经返回了,作为它的实参的 message 应该已经被销毁了,那么为什么我们在调用 Action 对象的时候,还是能够得到正确的结果呢?

The mystery turns out that the closure is formed here. Although CreateGreeting has been returned, its local variables are captured by the returned lambda expression, delaying its lifetime. How about it? Looking back at the definition of closures this way, is it clearer?

Closures are that simple. In fact, we use them all the time, but sometimes we don't even know it. For example, everyone must have written code similar to the following.

void AddControlClickLogger(Control control, string message)
{
	control.Click += delegate
	{
		Console.WriteLine("Control clicked: {0}", message);
	}
}

The code here actually uses closures because we can be sure that when control is clicked, the message has long exceeded its declaration period. Reasonable use of closures ensures that we write delegates that are decoupled in space and time.

However, when using closures, there is a trap to be aware of. Because closures delay the life cycle of local variables, in some cases the program may produce different results than expected. Let's take a look at the following example.

class Program
{
    static List<Action> CreateActions()
    {
        var result = new List<Action>();
        for(int i = 0; i < 5; i++)
        {
            result.Add(() => Console.WriteLine(i));
        }
        return result;
    }

    static void Main()
    {
        var actions = CreateActions();
        for(int i = 0;i<actions.Count;i++)
        {
            actions[i]();
        }
    }
}

This example is also very simple, create a list of actions and execute them in turn. look at the results

5
5
5
5
5

I believe many people have this expression when they see this result!! Shouldn't it be 0, 1, 2, 3, 4? What's the problem?

To get to the bottom of it, the problem here still lies in the nature of closures. On the other side of the coin "closures delay the life cycle of variables", a variable may be inadvertently referenced by multiple closures.

In this example, the local variable i is referenced by five closures at the same time, and these five closures share i, so the value they print out is the same, which is the value 5 when i finally exited the loop.

To solve this problem, it is easy to declare one more local variable and let each closure refer to its own local variable.

//其他都保持与之前一致
static List<Action> CreateActions()
{
    var result = new List<Action>();
    for (int i = 0; i < 5; i++)
    {
        int temp = i; //添加局部变量
        result.Add(() => Console.WriteLine(temp));
    }
    return result;
}
0
1
2
3
4

In this way, each closure references different local variables, and the problem just now is solved.

除此之外,还有一个修复的方法,在创建闭包的时候,使用foreach而不是for。至少在 C# 7.0 的版本上面,这个问题已经被注意到了,使用 foreach 的时候编译器会自动生成代码绕过这个闭包陷阱。

//这样fix也是可以的
static List<Action> CreateActions()
{
    var result = new List<Action>();
    foreach (var i in Enumerable.Range(0,5))
    {
        result.Add(() => Console.WriteLine(i));
    }
    return result;
}

This is a small trap in the use of closures in C#and its use. I hope everyone can understand this knowledge point through Lao Hu's article and avoid detours in development!

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 4/22/2026

Support for. NET by operating system versions (250707 update)

Use virtual machines and test machines to test the support of each version of the operating system for. NET. After installing the operating system, it is passed by measuring the corresponding running time of the installation and being able to run the Stardust Agent.

继续阅读
同分类 / 同标签 2/7/2026

Summary of experience in using AOT

From the very beginning of project creation, you should develop a good habit of conducting AOT release testing in a timely manner whenever new features are added or newer syntax is used.

继续阅读