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

來源:互聯網
上載者:User

標籤:virt   col   eric   manage   cal   write   編譯   nop   temp   

 

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.