技术C#C#笔记——闭包
‘祈止’闭包是现代类型编程语言的特性,本文将从委托开始逐步深入闭包的原理。
前置知识
Delegate(委托)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private delegate void Myfunc(int a);
Myfunc myfunc;
private void func(int a){ ... }
myfunc = func;
myfunc(1);
myfunc?.Invoke(1);
|
所谓委托,可以类似于C++的函数指针。可视为是函数的“容器”。可以看做是创建了一种类型。
Multicasting(多播)
一个委托可以储存多个函数,当委托调用时。储存的函数会按照存储的顺序逐个执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private delegate int Myfunc(int a, int b); private Myfunc myfunc;
private int add(int a, int b){ return a+b; }
private int multiply(int a, int b){ return a*b; }
myfunc += add; myfunc += multiply;
myfunc -= multiply;
|
注意,当一个委托内加入多个有返回值的函数时,返回值是最后一个函数的返回值。
1 2 3 4 5
| myfunc += multiply;
print(myfunc(1,2));
|
匿名函数和Lambda表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private delegate void Myfunc(int a); private Myfunc myfunc;
myfunc = delegate(){ ... }
myfunc = () => { ... }
|
库工具Action和Func
Action
用于定义无返回值的委托
1 2 3 4 5 6 7 8 9
| private Action _action;
_action = () => pass;
private Action<int a> _action;
_action = (int a) => pass;
|
Func
用于定义有返回值的委托
1 2 3 4 5
| private Func<int, int> _func;
_func = (int a) => return a;
|
最后一个泛型参数表示函数的返回值
event(事件)
我们先用普通的delegate做一个事件系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
public class Manager{ public Action systemStart(); public Action systemEnd(); private Load _load; private Save _save; public void main(){ systemStart?.Invoke();
systemEnd?.Invoke(); } }
public class Load{ Manager _manager; Load(Manager manager){ _manager = manager; _manager.systemStart += load; } ~Load(){ _manager.systemStart -= load; } private void load(){ pass; } }
public class Save{ Manager _manager; Save(Manager manager){ _manager = manager; _manager.systemStart += save; } ~Save(){ _manager.systemStart -= save; } private void save(){ pass; } }
|
这样实现了解耦合。但是这样的代码有危险性。
为了让外部类注册事件,委托的访问权限是public。但这样就导致,委托可以在外部被触发。这是不安全,我们不希望总系统的事件被外界触发。
可以使用event关键字,定义事件。让事件不能被外界触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
public class Manager{ public event Action systemStart(); public event Action systemEnd(); private Load _load; private Save _save; public void main(){ systemStart?.Invoke();
systemEnd?.Invoke(); } }
public class Load{ Manager _manager; Load(Manager manager){ _manager = manager; _manager.systemStart += load; } ~Load(){ _manager.systemStart -= load; } private void load(){ pass; } }
public class Save{ Manager _manager; Save(Manager manager){ _manager = manager; _manager.systemStart += save; } ~Save(){ _manager.systemStart -= save; } private void save(){ pass; } }
|
这样事件就不能再外部触发了。
闭包
经过漫长的前言,终于来到了本文的重点——闭包。
所谓闭包,表现为匿名函数(包括Lambda表达式,以下都简称匿名函数),在使用外界变量时,会延长该变量的生命周期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void Action _func;
void make(){ int a = 10; _func = () => { print(a); } }
_func?.Invoke();
|
这样就是一个闭包,但闭包有个坑。
闭包的循环问题
1 2 3 4 5 6 7 8 9 10 11 12 13
| void Action _func;
void make(){ for(int i = 0; i < 10; i++){ _func += () => print(i); } }
_func?.Invoke();
|
这超出了我们的预期,按理来说我们希望输出0-9。但现在却输出了10次10。这是为什么?
原因是C#编译时,匿名函数会被编译器自动生成一个匿名的类(事实上这个类不是匿名的,只是对于程序原来说类名是不可知的)。而且这个匿名类只会创建一次。
如果匿名函数使用了外部变量,这个外部变量在编译后会在匿名类内被定义。成为匿名类的一个内部的public的变量。
又因为for循环的i是在循环之前定义的,这就导致匿名类也必须在循环外定义——因为i在编译后是匿名类的类内变量。
而我们像委托添加的函数都是匿名类内的同一个函数,使用的类就是这个类内的变量“i”。所以代码最后输出了10次10。
反编译的代码类似(请注意这里只是类似,但本实例仅用于展现反编译代码的“精髓”。):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class A{ void Action my_action;
void make(){ Nclass nclass = new Nclass(); for(nclass.i = 0; nclass.i < 10; nclass.i++){ this.my_action = (Action)Delegate. Combine( (Delegate) this._myaction, (Delegate) new Action(nclass.Nfunc)); } }
my_action?.Invoke(); }
public class Nclass{ int i; public Nclass(){ base..ctor(); } internal void Nfunc(){ print(this.i.ToString()); } }
|
循环问题的解决方法
解决这个”bug”很简单。既然问题的根本在于直接使用了for循环的i,导致闭包匿名类在循环外部创建。那么只要不使用i,让闭包匿名类在循环内部创建就行了。
实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void Action _func;
void make(){ for(int i = 0; i < 10; i++){ int j = i; _func += () => print(j); } }
_func?.Invoke();
|
这样我们在循环内部定义临时变量j,这样因为j是在循环内定义的。那么编译后的代码也会在循环内创建匿名类。这样每次添加的函数就是不同的类实例的方法,每个类方法调用自己的内部变量j。
反编译代码类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class A{ void Action my_action;
void make(){ for(nclass.i = 0; nclass.i < 10; nclass.i++){ Nclass nclass = new Nclass(); nclass.j = i; this.my_action = (Action)Delegate. Combine( (Delegate) this._myaction, (Delegate) new Action(nclass.Nfunc));
} }
my_action?.Invoke(); }
public class Nclass{ int j; public Nclass(){ base..ctor(); } internal void Nfunc(){ print(this.j.ToString()); } }
|
这样我们就解决了闭包在循环中导致的问题。但看到这里,很显然闭包每次都要创建对象。这些对象都会储存在堆内存中,由GC负责处理。这就会导致性能损耗,如果可以尽量避免频繁的使用闭包。