概念
闭包是指捕获(引用)了作用域外的函数的局部变量的函数。
由于外部函数的局部变量被捕获,即使外层函数执行已终止,内部定义的函数也可以访问外部函数的局部变量,所以局部变量的生命周期得到了延长。
闭包本质是一个对象(编译后) 。
当定义一个闭包时,编译器会生成一个类,这个类的实例就是闭包对象,这个类包含了闭包所引用的外部变量和代码块。当调用闭包时,实际上是在调用这个类的实例。
在C#中我们可以使用委托和Lambda表达式来实现闭包。
案例
public class Test
{
public Action action;
static void Main(string[] args)
{
F1();
action();
}
//外部函数F1
public void F1()
{
//定义局部变量a
int a = 1;
//内部定义嵌套函数F2
void F2()
{
a += 1;
Console.WriteLine(a);
}
//订阅函数F2
action += F2;
//内部定义嵌套函数F3
void F3()
{
a += 1;
Console.WriteLine(a);
}
//订阅函数F3
action += F3;
//订阅Lambda表达式匿名函数
action += () =>
{
a += 0;
Console.WriteLine(a);
};
}
}
执行结果为 2,3,3;
过程解析:
内部定义的函数F2,函数F3,匿名函数都捕获了外部函数F1的局部变量a,并将三个方法依次订阅action委托并调用。
由于三个内部定义函数都是直接引用a,所以捕获的是a的内存地址,修改会影响到a本身。
即使外层函数F1执行已终止,内部定义的三个函数也可以访问外层函数F1的局部变量a,所以局部变量a的生命周期得到了延长。
第一个结果为函数F2执行的a += 1;此时a为1 + 1 = 2
第二个结果为函数F3执行的a += 1;此时a为2 + 1 = 3
第三个结果为匿名函数执行的a += 0;此时a为3 + 0 = 3
用途
- 用于创建私有变量,限制外部对变量的直接访问和修改,提高代码的封装性;
- 用来模拟私有方法,将数据隐藏和封装;
- 可以重复使用变量,并且不会造成变量污染;
- 实现回调函数时常常会使用闭包,将函数和相关的数据引用打包在一起,方便在特定事件发生时调用;
隐患
内存泄露
变量长期驻扎在内存中,可能导致内存泄漏。如果闭包不再需要,可以手动释放,比如将闭包设置为null,以便让垃圾回收器回收。
闭包陷阱
Action<int> actions = new Action<int>();
for (int i = 0; i < 5; i++)
{
actions += (() =>
{
Console.WriteLine(i)
});
}
foreach (int i in actions)
{
Console.WriteLine(i);
}
首先创建了一个委托,然后for循环里的5个匿名函数都捕获了循环变量i,因此形成了闭包。
当执行这个程序时,期望输出的是数字1到5,但实际上输出的是数字5,5,5,5,5。这是因为每个匿名函数都引用了同一个变量i的内存地址,而这个变量的值在循环结束后变成了5。所以当我们调用这些匿名函数时,它们都输出变量i的最终值5。
为了避免这个陷阱,我们可以在每次循环时定义一个新的局部变量j,将循环变量i的值复制到局部变量j中,然后让匿名函数捕获(引用)局部变量j。这样,每个匿名函数都引用的是新定义的不同的变量j,而不同一个循环变量i。修改后的代码:
Action<int> actions = new Action<int>();
for (int i = 0; i < 5; i++)
{
//局部变量j
int j = i;
//Lambda简写,省略了花括号
actions+=(() => Console.WriteLine(j));
}
foreach (int i in actions)
{
Console.WriteLine(i);
}