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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写高质量代码改善C#程序的157个建议——建议38:小心闭包中的陷阱相关的知识,希望对你有一定的参考价值。

 

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

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

        static void Main(string[] args)
        {
            List<Action> lists = new List<Action>();
            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`1<class [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/Enumerator<class [mscorlib]System.Action> CS$5$0001)
    L_0000: nop 
    L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<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`1<class [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 <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 = 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<Action> lists = new List<Action>();
            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<Action> lists = new List<Action>();
            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:小心闭包中的陷阱的主要内容,如果未能解决你的问题,请参考以下文章

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

编写高质量代码改善C#程序的157个建议——建议141:不知道该不该用大括号时,就用

编写高质量代码改善C#程序的157个建议——建议36:使用FCL中的委托声明

编写高质量代码改善C#程序的157个建议——建议52:及时释放资源

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

编写高质量代码改善C#程序的157个建议——建议41:实现标准的事件模型