C# —— Closure Capture(闭包捕获)

例子

先看两个典型的使用闭包例子,并思考其输出不同的原因

1
2
3
4
5
6
7
8
9
10
11
Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
// 闭包创建
actions[i] = () => { Debug.Log("i = " + i); };
// 闭包直接执行
actions[i]();
}

// 输出 0,1,2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
// 闭包创建
actions[i] = () => { Debug.Log("i = " + i); };
}

for (int i = 0; i < 3; i++)
{
// 闭包延迟执行
actions[i]();
}

// 输出 3,3,3

总结

闭包捕获外部变量时是使用引用的方式,所以当闭包执行时,捕获变量将以最新值参与运算

因此,当使用闭包并需要捕获外部变量时,需要思考:

  • 闭包执行的时机与捕获变量的变化之间的关系

  • 闭包的生命周期与捕获变量的生命周期,需要明确只要有任何一个闭包的委托实例在引用捕获变量,它就不会被回收

进阶

其实当闭包需要捕获外部变量时,编译器会生成一个临时类,并以该临时类的实例来持有对外部变量的引用;而当无需捕获外部变量时,不会生成临时类(可反编译深入了解)

所以,有时在考虑程序性能时,也可以考虑避免闭包捕获外部变量:

  • 在设计闭包时,可考虑添加一个或多个可选的范型参数,以适配各种需要传入外部变量的调用,如:

    1
    2
    3
    public delegate TResult Func<T, TResult>(T arg1);

    public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
  • 当使用需要传入闭包的方法,并且闭包需要捕获外部变量时,可留意是否还有其他可传参形式的方法可用,如:UniRx 中的各种 xxxWithState() 方法就是为了避免闭包捕获