1. 程式人生 > >【轉】編寫高質量代碼改善C#程序的157個建議——建議38:小心閉包中的陷阱

【轉】編寫高質量代碼改善C#程序的157個建議——建議38:小心閉包中的陷阱

class對象 輸出 局部變量 als lambda rate 完全 attribute 方法

建議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:小心閉包中的陷阱