捕获变量:捕获的是变量,而不是创建委托实例时它的值!!!!
1.捕获外部变量
class Program { static void Main(string[] args) { Action action = MethodInvoker(); action(); action(); Console.ReadKey(); } static Action MethodInvoker() { int count = 10; Action action = delegate () { Console.WriteLine($"count={count}"); count++; }; action(); return action; } }
运行结果如下:
首先,count 相对于匿名方法来说,是一个外部变量,
同时,方法内部定义的值类型是存储在栈上的,因此 count 是存储在栈上的,
那为什么在 MethodInvoker() 方法运行完之后, count 没有随之消失呢?
原因是:
由于在匿名方法中捕获了外部变量 count,
因此,编译器创建了一个额外的密封类来容纳这个匿名方法及其捕获的外部变量,如图:
我们知道,引用类型的成员都是存放在堆中的
这个额外的类里面的 count 也是存放在堆中的,并且看得出来有个 装箱 的过程
action 这个委托变量拥有对该额外类的一个实例的引用
所以,只要当委托还没有被GC回收,这个额外的类的实例就不会被回收.
再来两个例子加深印象:
1)捕获循环内的变量:
static void Main(string[] args) { List<Action> list = new List<Action>(); for (int i = 0; i < 5; i++) { int count = i * 10; list.Add(delegate () { //由于 count 在循环内部创建,因此每次捕获的都是一个不同的变量 //也就是说,集合中的每个action指向的额外类的实例的count不是同一个 Console.WriteLine(count); count++; }); } /* * 0 * 10 * 20 * 30 * 40 * */ foreach (var action in list) { action(); } list[0]();//1 list[0]();//2 list[0]();//3 list[1]();//11 Console.ReadKey(); }
2)捕获循环外的变量:
static void Main(string[] args) { int count = 0; List<Action> list = new List<Action>(); for (int i = 0; i < 5; i++) { list.Add(delegate () { //这里的 count 为 Main 方法的全局变量 //所以 这里只会创建一个 匿名类 的对象 Console.WriteLine(count); count++; }); } /* * 0 * 1 * 2 * 3 * 4 * */ foreach (var action in list) { action(); } list[0]();//5 list[0]();//6 list[0]();//7 list[1]();//8 Console.ReadKey(); }
IL代码解析
0000:newobj 执行匿名类的构造函数,在托管堆上(下简称堆)创建一个对象 new <>c_DisplayClass0_0(),并将引用压入计算堆栈(下简称栈) 0005:stloc.0 将栈顶的值弹出,赋值给第一个局部变量 这里,栈顶的值为上一行的引用, <>c_DisplayClass0_0 class_ = new <>c_DisplayClass0_0() 0006:nop 没有实际意义 0007:ldloc.0 将本地变量 class_ 加载到计算堆栈上 0008:ldc.i4.0 将整数值 0 作为 int32 推送到计算堆栈上。 0009:stfld 用一个新值替换对象的字段的值 用栈顶的值 : 0 , 赋值给 栈中的第二个值: class_ 的字段 count, 即 class_.count = 0 000e:newobj new List<Action>(); 0013:List<Action> list = new List<Action>(); 0014:将整数值 0 作为 int32 推送到计算堆栈上。 0015:int num = 0; 0016:无条件地将控制转移到目标指令(短格式)。跳到 0044 0018:无实际意思 0019:将 本地变量 list 加载到计算堆栈上 001a:将 本地变量 class_ 加载到计算堆栈上 001b:ldfld 查找对象中其引用当前位于计算堆栈的字段的值。 查找 class_ 对象中 <>9_0 字段的值,并压到堆栈上 0020:复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。即复制 class_.<>9_0 的值 推送到栈上 0021:如果 class_.<>9_0 的副本 不为 null,则跳到 0039 0023:移除当前位于计算堆栈顶部的值。即移除 class_.<>9_0 的副本 ,因为此时 它为 null 0024:将 class_ 加载到计算堆栈上 ( 0005 已经给 class_ 赋了值 ) 0025:将 class_ 加载到计算堆栈上 ( 0005 已经给 class_ 赋了值 ) 0026:将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。 将一个指向 <>c_DisplayClass0_0 类中的 <Main>b_0() 方法的指针(Delegate类中的 _methodPtr 字段) 推送到计算堆栈上 002c:创建一个委托实例 new Action(object,native int) 之所以是 Action,因为匿名方法没有返回值,没有入参,默认就是 Action 0031:复制计算堆栈上当前最顶端的值(委托的实例),然后将副本推送到计算堆栈上。 0032:将 委托的实例的副本 从栈上弹出,赋值给 本地变量列表中的 action,即 action = new Action(<Main>b_0()); 0033:将 委托的实例 从栈上弹出 赋值给 class_ .<>9_0 0038:将 action 推送到计算堆栈上 0039:执行 list.Add(a.<>9_0) 003e:没卵用 003f:没卵用 0040:将 num 加载到计算堆栈上 0041:将整数值 1 作为 int32 推送到计算堆栈上 0042:将两个值相加并将结果推送到计算堆栈上。 num + 1 ,并将结果推送到计算堆栈上. 0043:将计算堆栈最顶端的值,即上一行的结果赋值给num 0044:将 num 加载到计算堆栈上 0045:将整数值 5 作为 int32 推送到计算堆栈上 0046:clt 比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。 比较 num 和 5 0048:stloc.flag 从计算堆栈的顶部弹出当前值( 1 或者 0 ,即上一行的计算结果 )并将其存储在局部变量列表的 flag 变量中。 004a:将 flag 加载到计算堆栈上 004c:brtrue.s 判断栈顶的值,如果为 true、非空或非零,则将控制转移到目标指令(短格式,如果 flag 为 true、非空或非零,则跳到0018
3)强化训练
Action[] actionArray = new Action[2]; int outside = 0; for (int i = 0; i < 2; i++) { int inside = 0; actionArray[i] = delegate () { Console.WriteLine($"outside = {outside} , inside = {inside}"); outside++; inside++; }; } Action first = actionArray[0]; Action second = actionArray[1]; first(); //outside = 0 , inside = 0 first(); //outside = 1 , inside = 1 first(); //outside = 2 , inside = 2 second(); //outside = 3 , inside = 0 second(); //outside = 4 , inside = 1