1, 闭包的含义
首先闭包并不是针对某一特定语言的概念, 而是一个通用的概念. 除了在各个支持函数式编程的语言中, 我们会接触到它. 一些不支持函数式编程的语言中也能支持闭包(如 java8 之前的匿名内部类).
在看过的对于闭包的定义中, 个人觉得比较清晰的是在JavaScript 高级程序设计这本书中看到的. 具体定义如下:
闭包是指有权访问另一个函数作用域中的变量的函数.
注意, 闭包这个词本身指的是一种函数. 而创建这种特殊函数的一种常见方式是在一个函数中创建另一个函数.
2, 在 C# 中使用闭包(例子选取自C# 函数式程序设计)
下面我们通过一个简单的例子来理解 C# 闭包
- class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine(GetClosureFunction()(30));
- }
- static Func<int, int> GetClosureFunction()
- {
- int val = 10;
- Func<int, int> internalAdd = x => x + val;
- Console.WriteLine(internalAdd(10));
- val = 30;
- Console.WriteLine(internalAdd(10));
- return internalAdd;
- }
- }
上述代码的执行流程是 Main 函数调用 GetClosureFunction 函数, GetClosureFunction 返回了委托 internalAdd 并被立即执行了.
输出结果依次为 20,40,60
对应到一开始提出的闭包的概念. 这个委托 internalAdd 就是一个闭包, 引用了外部函数 GetClosureFunction 作用域中的变量 val.
注意: internalAdd 有没有被当做返回值和闭包的定义无关. 就算它没有被返回到外部, 它依旧是个闭包.
3, 理解闭包的实现原理
我们来分析一下这段代码的执行过程. 在一开始, 函数 GetClosureFunction 内定义了一个局部变量 val 和一个利用 lamdba 语法糖创建的委托 internalAdd.
第一次执行委托 internalAdd 10 + 10 输出 20
接着改变了被 internalAdd 引用的局部变量值 val, 再次以相同的参数执行委托, 输出 40. 显然局部变量的改变影响到了委托的执行结果.
GetClosureFunction 将 internalAdd 返回至外部, 以 30 作为参数, 去执行得到的结果是 60, 和 val 局部变量最后的值 30 是一致的.
val 作为一个局部变量. 它的生命周期本应该在 GetClosureFunction 执行完毕后就结束了. 为什么还会对之后的结果产生影响呢?
我们可以通过反编译来看下编译器为我们做的事情.
为了增加可读性, 下面的代码对编译器生成的名字进行修改, 并对代码进行了适当的整理.
- class Program
- {
- sealed class DisplayClass
- {
- public int val;
- public int AnonymousFunction(int x)
- {
- return x + this.val;
- }
- }
- static void Main(string[] args)
- {
- Console.WriteLine(GetClosureFunction()(30));
- }
- static Func<int, int> GetClosureFunction()
- {
- DisplayClass displayClass = new DisplayClass();
- displayClass.val = 10;
- Func<int, int> internalAdd = displayClass.AnonymousFunction;
- Console.WriteLine(internalAdd(10));
- displayClass.val = 30;
- Console.WriteLine(internalAdd(10));
- return internalAdd;
- }
- }
编译器创建了一个匿名类(如果不需要创建闭包, 匿名函数只会是与 GetClosureFunction 生存在同一个类中, 并且委托实例会被缓存, 参见 clr via C# 第四版 362 页), 并在 GetClosureFunction 中创建了它实例. 局部变量实际上是作为匿名类中的字段存在的.
4, C#7 对于不作为返回值的闭包的优化
如果在 vs2017 中编写第二节的代码. 会得到一个提示, 询问是否把 lambda 表达式 (匿名函数) 托转为本地函数. 本地函数是 c#7 提供的一个新语法. 那么使用本地函数实现闭包又会有什么区别呢?
如果还是第二节那样的代码, 改成本地函数, 查看 IL 代码. 实际上不会发生任何变化.
- class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine(GetClosureFunction()(30));
- }
- static Func<int, int> GetClosureFunction()
- {
- int val = 10;
- int InternalAdd(int x) => x + val;
- Console.WriteLine(InternalAdd(10));
- val = 30;
- Console.WriteLine(InternalAdd(10));
- return InternalAdd;
- }
- }
但是当 internalAdd 不需要被返回时, 结果就不一样了.
下面分别来看下匿名函数和本地函数创建不作为返回值的闭包的时候演示代码及经整理的反编译代码.
匿名函数
- static void GetClosureFunction()
- {
- int val = 10;
- Func<int, int> internalAdd = x => x + val;
- Console.WriteLine(internalAdd(10));
- val = 30;
- Console.WriteLine(internalAdd(10));
- }
经整理的反编译代码
- sealed class DisplayClass
- {
- public int val;
- public int AnonymousFunction(int x)
- {
- return x + this.val;
- }
- }
- static void GetClosureFunction()
- {
- DisplayClass displayClass = new DisplayClass();
- displayClass.val = 10;
- Func<int, int> internalAdd = displayClass.AnonymousFunction;
- Console.WriteLine(internalAdd(10));
- displayClass.val = 30;
- Console.WriteLine(internalAdd(10));
- }
本地函数
- class Program
- {
- static void Main(string[] args)
- {
- }
- static void GetClosureFunction()
- {
- int val = 10;
- int InternalAdd(int x) => x + val;
- Console.WriteLine(InternalAdd(10));
- val = 30;
- Console.WriteLine(InternalAdd(10));
- }
- }
经整理的反编译代码
- // 变化点 1: 由原来的 class 改为了 struct
- struct DisplayClass
- {
- public int val;
- public int AnonymousFunction(int x)
- {
- return x + this.val;
- }
- }
- static void GetClosureFunction()
- {
- DisplayClass displayClass = new DisplayClass();
- displayClass.val = 10;
- // 变化点 2: 不再构建委托实例, 直接调用值类型的实例方法
- Console.WriteLine(displayClass.AnonymousFunction(10));
- displayClass.val = 30;
- Console.WriteLine(displayClass.AnonymousFunction(10));
- }
上述这两点变化在一定程度上能够带来性能的提升, 所以在官方的推荐中, 如果委托的使用不是必要的, 更推荐使用本地函数而非匿名函数.
如果本博客描述的内容存在问题, 希望大家能够提出宝贵的意见. 坚持写博客, 从这一篇开始.
来源: https://www.cnblogs.com/blurhkh/p/9535289.html