匿名方法中的捕获变量

  乍一接触"匿名方法中的捕获变量"这一术语可能会优势蒙,那什么是"匿名方法中的捕获变量"呢?在章节未开始以前,咱们先定义一个委托:public delegate void MethodInvoke();闭包

一、闭包和不一样类型的变量:ide

  首先,你们应该都知道"闭包",它的概念是:一个函数除了能经过提供给它的参数交互以外,还能同环境进行更大程度的互动。但这个定义过于抽象,还须要理解两个术语:函数

  1)外部变量(outer variable)指做用域内包括匿名方法的局部变量或参数(不包括ref和out参数)。在类的实例成员内部的匿名方法中,this引用也被认为是一个外部变量。this

  2)捕获的外部变量(captured outer variable)一般简称捕获变量(captured variable),它是在匿名方法内部使用的外部变量。编码

  这些定义看起来云里雾里的,那接下来以一个例子来讲明: spa

 1 public void EnClosingMethod()  2 {  3     int outerVariable = 5; // 外部变量
 4     string captureVariable = "captured"; // 被匿名方法捕获的外部变量
 5     if (DateTime.Now.Hour == 23)  6  {  7         int normalLocalVariable = DateTime.Now.Minute; // 普通方法的局部变量
 8  Console.WriteLine(normalLocalVariable);  9  } 10     MethodInvoke x = delegate () 11  { 12         string anonLocal = "local to anonymous method"; // 匿名方法的局部变量
13         Console.WriteLine(captureVariable + anonLocal); // 捕获外部变量captureVariable
14  }; 15  Console.WriteLine(outerVariable); 16  x(); 17 }

二、捕获变量的行为:线程

  若是你运行了上述代码,你会发现匿名方法捕捉到的确实是变量,而不是建立委托实例时该变量的值。通俗的说就是只有在匿名方法被调用时才会被使用。 debug

 1 string captured = "before x is created";  2 MethodInvoke x = delegate
 3 {  4  Console.WriteLine(captured);  5     captured = "change by x";  6 };  7 captured = "directly before x is invoked";  8 x();  9 Console.WriteLine(captured); 10 captured = "before second invocation"; 11 x();

  上述代码的执行顺序是这样子的(能够debug):定义变量captured => 声明匿名方法MethodInvoke x => 将captured的值修改成"directly before x is invoked" => 紧接着调用委托x(),这个时候会进入匿名方法 => 首先输出captured的值"directly before x is invoked",而后修改成"change by x" => 匿名方法调用结束,来到第9行,输出captured的值"change by x" => 第10行从新给captured赋值"before second invocation" => 调用x()设计

三、捕获变量到底有什么用处:code

  捕获变量能简化避免专门建立一些类来存储一个委托须要处理的信息。

1 List<People> FindAllYoungerThan(List<People> people, int limit) 2 { 3     return people.Where(person => person.Age < limit).ToList(); 4 }

  咱们在委托实例内部捕获了limit参数——若是仅有匿名方法而没有捕获变量,就只能在匿名方法中使用一个"硬编码"的限制年龄,而不能使用做为参数传递的limit。这样的设计可以准备描述咱们的"目的",而不是将大量的精力放在"过程"上。

四、捕获变量的延长生存期:

  到目前为止,我么一直在建立委托实例的方法内部使用委托实例。在这种状况下,你对捕获变量的生存期(lifetime)不会又太大的疑问。可是,假如委托实例"逃"到另外一个黑暗的世界(big bad world),那会发生什么?假如建立它的那个方法结束了,它将何以应对?

  在理解这种问题时,最简单的办法就是指定一个规则,给出一个例子,而后思考假如没有那个规则,会发生什么:对于一个捕获变量,只要还有任何委托实例在引用它,它就会一直存在。

 1 private static void Main(string[] args)  2 {  3     MethodInvoke x = CreateDelegateInstance();  4  x();  5  x();  6 }  7 
 8 private static MethodInvoke CreateDelegateInstance()  9 { 10     int counter = 5; 11 
12     MethodInvoke ret = delegate
13  { 14  Console.WriteLine(counter); 15         counter++; 16  }; 17 
18  ret(); 19     return ret; 20 }

  输出的结果:

  咱们通常认为counter在栈上,因此只要与CreateDelegateInstance对应的栈帧被销毁,counter随之消失,可是从结果来看,显然咱们的认知是有问题的。事实上,编译器建立了一个额外的类容纳变量。CreateDelegateInstance方法拥有对该类的一个实例的引用,因此它能使用counter。另外,委托也对该实例的一个引用,这个实例和其余实例同样都在堆上。除非委托准备好垃圾回收,不然那个实例是不会被回收的。

五、局部变量实例化:

  下面将展现一个例子。

1 int single; 2 for (int i = 0; i < 10; i++) 3 { 4     single = 5; 5     Console.WriteLine(single + i); 6 }
1 for (int i = 0; i < 10; i++) 2 { 3     int multiple = 5; 4     Console.WriteLine(multiple + i); 5 }

  上述两段代码在语义和功能上是同样的,但在内存开销上显然第一种写法比第二种占用较小的内存。single变量只实例化一次,而multiple变量将实例化10次。当一个变量被捕获时,捕捉的是变量的"实例"。若是在循环内捕捉multiple,第一次循环迭代时捕获的变量与第二次循环时捕获的变量是不一样的。

 1 List<MethodInvoke> list = new List<MethodInvoke>();  2 for (int index = 0; index < 5; index++)  3 {  4     int counter = index * 10;  5     list.Add(delegate
 6  {  7  Console.WriteLine(counter);  8         counter++;  9  }); 10 } 11 foreach (MethodInvoke t in list) 12 { 13  t(); 14 } 15 
16 list[0](); 17 list[0](); 18 list[0](); 19 
20 list[1]();

  输出结果:

  上述代码首先建立了5个不一样的委托实例,调用委托时,会先打印counter值,再对它进行递增。因为counter变量是在循环内部声明的,因此每次循环迭代,它都会被实例化。这样一来,每一个委托捕捉到的都是不一样的变量。

六、共享和非共享的变量混合使用:

 1 MethodInvoke[] delegates = new MethodInvoke[2];  2 int outside = 0;  3 
 4 for (int i = 0; i < 2; i++)  5 {  6     int inside = 0;  7     delegates[i] = delegate
 8  {  9         Console.WriteLine($"{outside},{inside}"); 10         outside++; 11         inside++; 12  }; 13 } 14 
15 MethodInvoke first = delegates[0]; 16 MethodInvoke second = delegates[1]; 17 
18 first(); 19 first(); 20 first(); 21 
22 second(); 23 second();

  输出结果:

  首先outside变量只声明了一次,但inside变量每次循环迭代,都会实例化一个新的inside变量。这意味着当咱们建立委托实例时,outside变量将由委托实例共享,但每一个委托实例都有它们本身的inside变量。

七、总结:

  如何合理使用捕获变量?

    1)若是用或不用捕获变量的代码一样简单,那就不要用。

    2)捕获由for或foreach语句声明的变量以前,思考你的委托是否须要再循环迭代结束以后延续,以及是否想让它看到那个变量的后续值。若是不是,就在循环内另建一个变量,用来复制你想要的值。

    3)若是建立多个委托实例,并且捕获了变量,思考下是否但愿它们捕获同一变量。

    4)若是捕获的变量不会发生变化,就不须要担忧。

    5)若是你建立的委托实例永远不会存储别的地方,不会返回,也不会启动线程。

    6)从垃圾回收的角度,思考任何捕获变量被延长的生存期。这个问题通常都不大,但假如捕获的对象会产生昂贵的内存开销,问题就会凸显出来。

参考:深刻理解C#_第三版