建议38:小心闭包中的陷阱

先看一下下面的代码,设想一下输出的是什么?

        static void Main(string[] args)
{
List<Action> lists = new List<Action>();
for (int i = ; i < ; i++)
{
Action t = () =>
{
Console.WriteLine(i.ToString());
};
lists.Add(t);
}
foreach (Action t in lists)
{
t();
}
}

我们的设计意图是让匿名方法(在这里表现为Lambda表达式)接受参数 i ,并输出:

0

1

2

3

4

而实际上输出为:

5

5

5

5

5

这段代码并不像我们想象的那么简单,要完全理解运行时代码是怎么运行的,首先必须理解C#编译器为我们做了什么。

IL代码如下:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack
.locals init (
[] class [mscorlib]System.Collections.Generic.List`<class [mscorlib]System.Action> lists,
[] class [mscorlib]System.Action t,
[] class [mscorlib]System.Action CS$<>9__CachedAnonymousMethodDelegate1,
[] class MyTest.Program/<>c__DisplayClass2 CS$<>8__locals3,
[] bool CS$$,
[] valuetype [mscorlib]System.Collections.Generic.List`/Enumerator<class [mscorlib]System.Action> CS$$)
L_0000: nop
L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`<class [mscorlib]System.Action>::.ctor()
L_0006: stloc.0
L_0007: ldnull
L_0008: stloc.2
L_0009: newobj instance void MyTest.Program/<>c__DisplayClass2::.ctor()
L_000e: stloc.3
L_000f: ldloc.3
L_0010: ldc.i4.0
L_0011: stfld int32 MyTest.Program/<>c__DisplayClass2::i
L_0016: br.s L_0044
L_0018: nop
L_0019: ldloc.2
L_001a: brtrue.s L_002b
L_001c: ldloc.3
L_001d: ldftn instance void MyTest.Program/<>c__DisplayClass2::<Main>b__0()
L_0023: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
L_0028: stloc.2
L_0029: br.s L_002b
L_002b: ldloc.2
L_002c: stloc.1
L_002d: ldloc.0
L_002e: ldloc.1
L_002f: callvirt instance void [mscorlib]System.Collections.Generic.List`<class [mscorlib]System.Action>::Add(!)
L_0034: nop
L_0035: nop
L_0036: ldloc.3
L_0037: dup
L_0038: ldfld int32 MyTest.Program/<>c__DisplayClass2::i
L_003d: ldc.i4.1
L_003e: add
L_003f: stfld int32 MyTest.Program/<>c__DisplayClass2::i
L_0044: ldloc.3
L_0045: ldfld int32 MyTest.Program/<>c__DisplayClass2::i
L_004a: ldc.i4.5
L_004b: clt
L_004d: stloc.s CS$$
L_004f: ldloc.s CS$$
L_0051: brtrue.s L_0018
L_0053: nop
L_0054: ldloc.0 //以下省略

在L_0009行,发现编译器为我们创建了一个类“<>c__DisplayClass2”,并且在循环内部每次会为这个类的一个实例变量 i 赋值。

这个类的IL代码为:

.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass2
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
} .method public hidebysig instance void <Main>b__0() cil managed
{
} .field public int32 i }

经过分析,会发现前面的这段代码实际和下面这段代码是一致的:

        static void Main(string[] args)
{
List<Action> lists = new List<Action>();
TempClass tempClass = new TempClass();
for (tempClass.i = ; tempClass.i < ; tempClass.i++)
{
Action t = tempClass.TempFuc;
lists.Add(t);
}
foreach (Action t in lists)
{
t();
}
} class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}

这段代码演示的就是闭包对象。所谓闭包对象,指的是上面这种情形中的TempClass对象(在第一段代码中,就是编译器为我们生成的<>c__DisplayClass2对象)。如果匿名方法(lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到闭包对象中,即将for循环中的变量 i 修改成了引用闭包对象的公共变量 i 。这样,即使代码执行离开了原局部变量 i 的作用域(如for循环),包含该闭包对象的作用域还存在。理解了这一点,就理解了代码的输出了。

要实现本建议开始时所预期的输出,可以将闭包对象的产生放在for循环内部:

        static void Main(string[] args)
{
List<Action> lists = new List<Action>();
for (int i = ; i < ; i++)
{
int temp = i;
Action t = () =>
{
Console.WriteLine(temp.ToString());
};
lists.Add(t);
}
foreach (Action t in lists)
{
t();
}
}

此代码和下面的代码一致:

        static void Main(string[] args)
{
List<Action> lists = new List<Action>();
for (int i = ; i < ; i++)
{
TempClass tempClass = new TempClass();
tempClass.i = i;
Action t = tempClass.TempFuc;
lists.Add(t);
}
foreach (Action t in lists)
{
t();
}
} class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

编写高质量代码改善C#程序的157个建议——建议38:小心闭包中的陷阱的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. git 查看、创建、切换、删除、重命名和推送分支

    1.查看本地所有分支:前面有 “*” 的是当前所处的分支 $ git branch test-A * test-B 2.查看本地和远程服务器的所有分支: $ git branch -a test-A ...

  2. flask之flask-restful

    0.需要的库flask_restful from flask import Flask from flask_cors import CORS 1.参数的获取self.parser.add_argum ...

  3. php end()

    end()的用法

  4. Java-Maven-Runoob:Maven 自动化构建

    ylbtech-Java-Maven-Runoob:Maven 自动化构建 1.返回顶部 1. 自动化构建定义了这样一种场景: 在一个项目成功构建完成后,其相关的依赖工程即开始构建,这样可以保证其依赖 ...

  5. Cocos暂停和重新开始游戏

    创建按钮 cc.MenuItemFont.setFontSize(18); cc.MenuItemFont.setFontName("Arial"); var systemMenu ...

  6. Py修行路 python基础(一)初识

    编译:把铭文代码执行前,先转成二进制,再执行,这个过程就叫编译. 编译型c,c++go特点:运行效率高依赖编译平台,linux 操作系统 跟CPU交互的接口,与windows不是完全一样不能跨平台,开 ...

  7. 【树莓派智能门锁】使用脚本控制GPIO来开锁【4】

    假定你已经通过此文章或者其他方式完成了树莓派的基本配置 [树莓派]RASPBIAN镜像初始化配置 我们通过VNC View连接到树莓派查看一下~ 1.更新一下基本的设置:更新一下源,把python-d ...

  8. js 正则匹配 小结

    JS的正则表达式 rge.test(str) 检验目标对象中是否包含匹配模式,并相应的返回true或false   rge.source str.search(rge) 将返回一个整数值,指明这个匹配 ...

  9. Tuple、list的区别以及dict和set

    元组(Tuple): 定义方法:使用小括号() 使用方法: count:可以统计某个元组段在整个元组中出现的次数 index:可以查询某个元组段在整个元组中的元组号 name_tuple = ('xi ...

  10. 提高ListView的效率

    按照普通的写法,如果ListView里面有比较多的东西的话,在加载每一个Item的时候,是非常非常卡的.具体表现就是滚动起来的时候会看见明显的卡顿. 关键还是处理自定义Adapter里面的getVie ...