闭包是一个函数 + 它所捕获的变量上下文的组合体。
换句话说:
当一个函数(通常是 lambda 或匿名函数)访问了其外部作用域中的变量,并且这个函数被作为对象传递或延迟执行,那么它就形成了一个“闭包”。
这整个结构就叫做一个闭包。
想象你是厨师(函数),你做菜时随手拿了几样家里冰箱里的食材(外部变量)带去了餐厅,等顾客来了你再做菜(延迟执行)。
csharpvoid Example()
{
int count = 0; // 外部变量
Action action = () =>
{
Console.WriteLine(count); // Lambda 捕获了外部变量 count
};
count = 42;
action(); // 输出:42
}
这里 lambda ()=> Console.WriteLine(count)
捕获了外部变量 count
,所以编译器会创建一个闭包类来保存这个 count
变量,并生成一个委托对象。
它大致会生成一个类似这样的代码:
csharpclass DisplayClass
{
public int count;
public void Lambda()
{
Console.WriteLine(count);
}
}
void Example()
{
DisplayClass dc = new DisplayClass();
dc.count = 0;
Action action = dc.Lambda;
dc.count = 42;
action(); // 输出:42
}
编译器悄悄帮你创建了一个类(通常名为
<>c__DisplayClass...
),变量变成它的字段,lambda 变成它的方法。执行 lambda 本质上就是执行这个类的方法。
每次创建一个捕获了变量的 lambda,就会:
new
一个闭包对象实例(堆上)因此:频繁写闭包就会频繁堆分配,导致 GC 压力
特性 | 是否成立 |
---|---|
捕获外部变量 | ✅ |
延迟执行时仍能访问原值 | ✅ |
编译为类 + 实例 | ✅ |
会在堆上产生 GC | ✅ |
不捕获变量时不是闭包 | ❌ |
lambda 内部是否访问了函数外部的局部变量?
lambda 是否在每次执行处都被重新声明/注册?
csharpvar actions = new List<Action>();
for (int i = 0; i < 3; i++) {
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action(); // 输出 3, 3, 3
上述代码中,当触发事件的时候,得到的是i最终值为3,因为这里所有的闭包捕获的是同一个变量i,i是引用而非拷贝的值,i在循环中是不停变化的,当我们触发事件的时候,循环已经结束了,但是捕获的又是同一个变量i,所以输出的值都是相同的,所以需要一个临时变量来存储每次循环中i的值
解决方法,使用临时变量来存储
csharpfor (int i = 0; i < 3; i++) {
int temp = i;
actions.Add(() => Console.WriteLine(temp));
}
onClick.AddListener(() => Method())
,(这里Button在注册委托的时候会在堆上分配一个闭包对象 + 一个委托实例。而后续每次点击按钮(触发事件)时,仅调用已经存在的委托对象,不会再次分配内存,也不会创建新的闭包对象)AddListener(Method)
没有闭包,零 GC闭包是指一个函数连同它所捕获的外部变量一起打包起来的结构。它通常由 lambda 表达式触发,在编译时会生成一个类,在运行时
new
出一个对象,进而可能导致 GC。
本文作者:xuxuxuJS
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
预览: