建议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个建议》陆敏技

最新文章

  1. JAVA抓取URL
  2. nginx location模块--匹配规则
  3. tmpFile.renameTo(classFile) failed 错误
  4. sed替换字符串时,使用正则表达式的注意事项
  5. SQL将金额转换为汉子
  6. IOS 开发qq登陆界面
  7. Android Wear开发 - 卡片通知 - 第一节 : 添加Android Wear通知特性
  8. Sql Server 2005 CLR实例
  9. Samba通过ad域进行认证并限制空间大小《转载》
  10. Qt浅谈之二十App自动重启及关闭子窗口(六种方法)
  11. C#学习日志 day 5 ------ windows phone 8.1真机调试手机应用
  12. __main() 和 main() 【转】
  13. EF实例创建问题
  14. 201521123117 《Java程序设计》第12周学习总结
  15. web前端教程:CSS 布局十八般武艺都在这里了
  16. handsontable 事件汇总
  17. Ambari Log Search
  18. SpringBoot学习之集成mybatis
  19. docker报Error response from daemon: client is newer than server (client API version: 1.24, server API version: 1.19)
  20. Linker Scripts3--SECTIONS Command

热门文章

  1. Ambari client
  2. android生命周期参考
  3. 安装S_S相关报错的troubleshooting
  4. linux(centos7) 安装nginx
  5. Goclipse on Eclipse
  6. DSP SYS/BIOS开发
  7. 转:oracle几组重要的常见视图-v$latch,v$latch_children,v$lock,v$locked_object
  8. call()和apply()的认知
  9. Android UI学习 - ListView (android.R.layout.simple_list_item_1是个什么东西)
  10. STA组件好资料