Although closures are primarily a concept in functional programming, and C#'s main characteristic is object-oriented programming, using delegates or lambda expressions, C# can also write code with a functional programming flavor. Similarly, using delegates or lambda expressions, closures can also be used in C#.
According to Wikipedia's definition, a closure, also known as a lexical closure or function closure, is a technique for implementing lexical binding in functional programming languages. In implementation, a closure is a struct that stores a function (usually its entry address) and an associated environment (equivalent to a symbol lookup table). Closures can also delay the lifetime of variables.
Hmm... the definition seems a bit confusing. Let's look at the example below.
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. It uses a lambda expression to create an Action object, and then calls that Action object.
But if you look closely, when the Action object is called, the CreateGreeting method has already returned, and its argument message should have been destroyed. So why do we still get the correct result when calling the Action object?
The secret lies in the formation of a closure. Although CreateGreeting has returned, its local variable is captured by the returned lambda expression, delaying its lifetime. Now, going back and looking at the closure definition, does it make more sense?
Closures are really that simple. In fact, we often use them without even realizing it. For example, you've probably written code similar to the following.
void AddControlClickLogger(Control control, string message)
{
control.Click += delegate
{
Console.WriteLine("Control clicked: {0}", message);
}
}
This code actually uses a closure, because by the time the control is clicked, the message has definitely exceeded its lifetime. Proper use of closures ensures we can write delegates that are decoupled in both space and time.
However, there is a pitfall to be aware of when using closures. Because closures delay the lifetime of local variables, in some cases the program's results may differ from expectations. Let's look at the example below.
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 one by one. Look at the output:
5
5
5
5
5

Many people would see this result and be like!! Shouldn't it be 0, 1, 2, 3, 4? What went wrong?
Getting to the root of the problem, it still relates to the nature of closures. The flip side of "closures delay variable lifetime" is that a variable may unintentionally be referenced by multiple closures.
In this example, the local variable i is referenced by five closures simultaneously. These five closures share i, so they all print the same value: the value of i when the loop ends, which is 5.
To fix this problem, simply declare an additional local variable so each closure references its own local variable.
// everything else remains the same
static List<Action> CreateActions()
{
var result = new List<Action>();
for (int i = 0; i < 5; i++)
{
int temp = i; // add local variable
result.Add(() => Console.WriteLine(temp));
}
return result;
}
0
1
2
3
4
Now each closure references a different local variable, and the problem is solved.
There is also another fix: use foreach instead of for when creating closures. At least in C# 7.0, this issue has been addressed. When using foreach, the compiler automatically generates code to bypass this closure trap.
// This fix also works
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 how closures are used in C# and a small pitfall in their usage. I hope you can learn this knowledge through Lao Hu's article and avoid detours in development!