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

1、闭包和不同类型的变量:

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

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

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

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

 public void EnClosingMethod()
{
int outerVariable = ; // 外部变量
string captureVariable = "captured"; // 被匿名方法捕获的外部变量
if (DateTime.Now.Hour == )
{
int normalLocalVariable = DateTime.Now.Minute; // 普通方法的局部变量
Console.WriteLine(normalLocalVariable);
}
MethodInvoke x = delegate ()
{
string anonLocal = "local to anonymous method"; // 匿名方法的局部变量
Console.WriteLine(captureVariable + anonLocal); // 捕获外部变量captureVariable
};
Console.WriteLine(outerVariable);
x();
}

2、捕获变量的行为:

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

 string captured = "before x is created";
MethodInvoke x = delegate
{
Console.WriteLine(captured);
captured = "change by x";
};
captured = "directly before x is invoked";
x();
Console.WriteLine(captured);
captured = "before second invocation";
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()

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

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

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

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

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

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

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

 private static void Main(string[] args)
{
MethodInvoke x = CreateDelegateInstance();
x();
x();
} private static MethodInvoke CreateDelegateInstance()
{
int counter = ; MethodInvoke ret = delegate
{
Console.WriteLine(counter);
counter++;
}; ret();
return ret;
}

  输出的结果:

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

5、局部变量实例化:

  下面将展示一个例子。

 int single;
for (int i = ; i < ; i++)
{
single = ;
Console.WriteLine(single + i);
}
 for (int i = ; i < ; i++)
{
int multiple = ;
Console.WriteLine(multiple + i);
}

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

 List<MethodInvoke> list = new List<MethodInvoke>();
for (int index = ; index < ; index++)
{
int counter = index * ;
list.Add(delegate
{
Console.WriteLine(counter);
counter++;
});
}
foreach (MethodInvoke t in list)
{
t();
} list[]();
list[]();
list[](); list[]();

  输出结果:

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

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

 MethodInvoke[] delegates = new MethodInvoke[];
int outside = ; for (int i = ; i < ; i++)
{
int inside = ;
delegates[i] = delegate
{
Console.WriteLine($"{outside},{inside}");
outside++;
inside++;
};
} MethodInvoke first = delegates[];
MethodInvoke second = delegates[]; first();
first();
first(); second();
second();

  输出结果:

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

7、总结:

  如何合理使用捕获变量?

    1)如果用或不用捕获变量的代码同样简单,那就不要用。

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

    3)如果创建多个委托实例,而且捕获了变量,思考下是否希望它们捕获同一变量。

    4)如果捕获的变量不会发生变化,就不需要担心。

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

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

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

C#2匿名方法中的捕获变量的更多相关文章

  1. C#:在匿名方法中捕获外部变量

    先来一段代码引入主题.如果你可以直接说出代码的输出结果,说明本文不适合你.(代码引自<深入理解C#>第三版) class Program { private delegate void T ...

  2. CI控制器中设置在其它方法中可用的变量

    开发过程中,某些变量可能需要被控制器中的其它方法所调用,这个变量改怎么设置呢? 其实可以用ci的$this->load->vars($array);和$this->load-> ...

  3. 解决Vue方法中setTimeout改变变量的值无效

    把data里的变量继承过来重新封装一下 let that = this; this.rightAnswer = false; setTimeout(function() { that.rightAns ...

  4. 委托、事件、匿名方法、Lambda

    一.委托(delegate)     定义:public delegate  void/类型  DefinedDelegate(参数1,参数2...) 委托是类型安全的. 委托实例:DefinedDe ...

  5. c#类中字段和方法中变量的声明问题

    字段和局部变量的作用域冲突 某些情况下可以区分名称相同,作用域相同的两个标识符.原因是C#在变量之间有一个基本的区分,它把在类级别声明的变量看作 字段,而把在方法中声明的变量看作局部变量. class ...

  6. 匹夫细说C#:委托的简化语法,聊聊匿名方法和闭包

    0x00 前言 通过上一篇博客<匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的>的内容,我们实现了使用委托来构建我们自己的消息系统的过程.但是在日常的开发中,仍然有很多开发者因为 ...

  7. C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质

    前言 C# 3.0 引入了 Lambda 表达式,程序员们很快就开始习惯并爱上这种简洁并极具表达力的函数式编程特性. 本着知其然,还要知其所以然的学习态度,笔者不禁想到了几个问题. (1)匿名函数(匿 ...

  8. 关于C#匿名方法

    作者  陈嘉栋(慕容小匹夫) 阅读目录 0x00 前言 0x01 不必构造委托对象 0x02 匿名方法初探 0x03 使用匿名方法省略参数 0x04 匿名方法和闭包 0x05 匿名方法如何捕获外部变量 ...

  9. C#2.0新增功能03 匿名方法

    连载目录    [已更新最新开发文章,点击查看详细] 在 2.0 之前的 C# 版本中,声明委托的唯一方式是使用命名方法. C# 2.0 引入匿名方法,在 C# 3.0 及更高版本中,Lambda 表 ...

随机推荐

  1. Web前端安全分析

    随着互联网高速的发展,信息安全已经成为企业重点关注焦点之一,而前端又是引发安全问题的高危据点,所以,作为一个前端开发人员,需要了解前端的安全问题,以及如何去预防.修复安全漏洞. 一.XSSS攻击 1. ...

  2. E-MAZE_2019牛客暑期多校训练营(第二场)

    题意 给出n行m列的迷宫0可走1不可走,有两个操作,操作1变换点(a,b)的值,操作2查询(1,a)到(n,b)的方案数 题解 设\(F[i][j]\)为第i-1行到达第i行第j列的方案数,若点\(( ...

  3. poj 3169 Layout(差分约束+spfa)

    题目链接:http://poj.org/problem?id=3169 题意:n头牛编号为1到n,按照编号的顺序排成一列,每两头牛的之间的距离 >= 0.这些牛的距离存在着一些约束关系:1.有m ...

  4. Java微服务(二):服务消费者与提供者搭建

    本文接着上一篇写的<Java微服务(一):dubbo-admin控制台的使用>,上篇文章介绍了docker,zookeeper环境的安装,并参考dubbo官网演示了dubbo-admin控 ...

  5. Dinic算法学习

    转自 此文虽为转载,但博主的网络流就是从这开始的,认为写的不错 网络流基本概念 什么是网络流 在一个有向图上选择一个源点,一个汇点,每一条边上都有一个流量上限(以下称为容量),即经过这条边的流量不能超 ...

  6. 一小时入门 Python

    因为需求, 需要用到py, 所以来学学py, 因为有java基础 一小时入门py语法是不成问题的, 但是仅仅入门基础语法而已, 不涉及算法,不涉及大数据,机器学习,人工智能, 但是py这么火爆,就在于 ...

  7. Vert.x Web 文档手册

    Vert.x Web 中英对照表 Container:容器 Micro-service:微服务 Bridge:桥接 Router:路由器 Route:路由 Sub-Route: 子路由 Handler ...

  8. FreeSql (二)自动迁移实体

    FreeSql 支持 CodeFirst 迁移结构至数据库,这应该是(O/RM)必须标配的一个功能. 与其他(O/RM)不同FreeSql支持更多的数据库特性,而不只是支持基础的数据类型,这既是优点也 ...

  9. FreeSql (十六)分页查询

    IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.MySql, "Data ...

  10. div标签嵌套原则详解(转载)

    这个也许平时人们不注意,但是非常有用,尤其是当你实在找不到原因为什么网页显示错误的时候. XHTML 的标签有许多:div.ul.li.dl.dt.dd.h1~h6.p.a.addressa.span ...