编辑
2023-06-16
编程语言
00

目录

一、什么是“闭包”?
二、闭包由什么组成?
三、通俗示例:现实比喻
六、闭包带来的 GC 问题
七、闭包的特点总结
八、如何判断某个 lambda 是否形成闭包?
九、闭包变量共享问题
十、Unity中的注意事项
🔚 总结

一、什么是“闭包”?

闭包是一个函数 + 它所捕获的变量上下文的组合体。

换句话说:

当一个函数(通常是 lambda 或匿名函数)访问了其外部作用域中的变量,并且这个函数被作为对象传递或延迟执行,那么它就形成了一个“闭包”。


二、闭包由什么组成?

  1. 一个函数(通常是匿名函数或 lambda 表达式)
  2. 一组被捕获的变量(来自函数外部作用域)
  3. 编译器会将这些变量打包为一个类实例对象

这整个结构就叫做一个闭包。


三、通俗示例:现实比喻

想象你是厨师(函数),你做菜时随手拿了几样家里冰箱里的食材(外部变量)带去了餐厅,等顾客来了你再做菜(延迟执行)。

  • 你 = 函数
  • 冰箱里的食材 = 被捕获的变量
  • 带出门的购物袋 = 闭包对象(把变量装起来)
  • 顾客点单 = 调用这个闭包

四、C# 中闭包的代码示例

csharp
void Example() { int count = 0; // 外部变量 Action action = () => { Console.WriteLine(count); // Lambda 捕获了外部变量 count }; count = 42; action(); // 输出:42 }

这里 lambda ()=> Console.WriteLine(count) 捕获了外部变量 count,所以编译器会创建一个闭包类来保存这个 count 变量,并生成一个委托对象。


五、C# 编译器如何处理闭包(背后发生了什么)

它大致会生成一个类似这样的代码:

csharp
class 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 本质上就是执行这个类的方法。


六、闭包带来的 GC 问题

每次创建一个捕获了变量的 lambda,就会:

  • 编译生成一个闭包类
  • 运行时 new 一个闭包对象实例(堆上)
  • 创建一个委托绑定它的方法

因此:频繁写闭包就会频繁堆分配,导致 GC 压力


七、闭包的特点总结

特性是否成立
捕获外部变量
延迟执行时仍能访问原值
编译为类 + 实例
会在堆上产生 GC
不捕获变量时不是闭包

八、如何判断某个 lambda 是否形成闭包?

  1. lambda 内部是否访问了函数外部的局部变量?

    • 是:会创建闭包对象(产生 GC)
    • 否:不会创建闭包对象,可能为 static 方法引用或静态委托
  2. lambda 是否在每次执行处都被重新声明/注册?

    • 是:可能每次创建新的闭包对象
    • 否:只创建一次闭包对象,可忽略 GC 影响

九、闭包变量共享问题

csharp
var 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的值

解决方法,使用临时变量来存储

csharp
for (int i = 0; i < 3; i++) { int temp = i; actions.Add(() => Console.WriteLine(temp)); }

十、Unity中的注意事项

  • UI 注册事件中最常见闭包来源:onClick.AddListener(() => Method()),(这里Button在注册委托的时候会在堆上分配一个闭包对象 + 一个委托实例。而后续每次点击按钮(触发事件)时,仅调用已经存在的委托对象,不会再次分配内存,也不会创建新的闭包对象
  • 如果 lambda 中使用了外部变量,就形成闭包
  • 方法组 AddListener(Method) 没有闭包,零 GC
  • 重复注册 lambda(如在 Update 里)会导致大量 GC

🔚 总结

闭包是指一个函数连同它所捕获的外部变量一起打包起来的结构。它通常由 lambda 表达式触发,在编译时会生成一个类,在运行时 new 出一个对象,进而可能导致 GC。

本文作者:xuxuxuJS

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.14.8