【转】编写高质量代码改善C#程序的157个建议——建议38:小心闭包中的陷阱

2021-04-24 15:28

阅读:518

标签:virt   col   eric   manage   cal   write   编译   nop   temp   

 

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

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

        static void Main(string[] args)
        {
            List lists = new List();
            for (int i = 0; i 5; 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 3
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1class [mscorlib]System.Action> lists,
        [1] class [mscorlib]System.Action t,
        [2] class [mscorlib]System.Action CS$9__CachedAnonymousMethodDelegate1,
        [3] class MyTest.Program/c__DisplayClass2 CS$8__locals3,
        [4] bool CS$4$0000,
        [5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumeratorclass [mscorlib]System.Action> CS$5$0001)
    L_0000: nop 
    L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1class [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::
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`1class [mscorlib]System.Action>::Add(!0) 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$4$0000 L_004f: ldloc.s CS$4$0000 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 
b__0() cil managed { } .field public int32 i }

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

        static void Main(string[] args)
        {
            List lists = new List();
            TempClass tempClass = new TempClass();
            for (tempClass.i = 0; tempClass.i 5; 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 lists = new List();
            for (int i = 0; i 5; 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 lists = new List();
            for (int i = 0; i 5; 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:小心闭包中的陷阱

标签:virt   col   eric   manage   cal   write   编译   nop   temp   

原文地址:http://www.cnblogs.com/farmer-y/p/7943739.html


评论


亲,登录后才可以留言!