1. 程式人生 > >C# 中閉包問題

C# 中閉包問題

首先來看一個簡單的例子。

       var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = () => { Console.WriteLine(i); };
            }
            foreach (var item in list)
            {
                item();
            }

輸出結果為:

5
5
5
5
5

通過這個簡單的例子,我來簡單講解一下C#中的閉包。
概念:
In essence, a closure is a block of code which can be executed at a later time, but which maintains the environment in which it was first created - i.e. it can still use the local variables etc of the method which created it, even after that method has finished executing.

大概的意思是:從本質上說,閉包是一段可以在晚些時候執行的程式碼塊,但是這段程式碼塊依然維護著它第一個被建立時環境(執行上下文)。 即它仍可以使用建立它的方法中區域性變數,即使那個方法已經執行完了。
當然在C#中通常通過匿名函式和Lamada表示式來實現閉包。
經過搜尋,我在msdn的一篇部落格中(https://blogs.msdn.microsoft.com/ericlippert/2009/11/12/closing-over-the-loop-variable-considered-harmful/) 見到了這樣一句話:
Because ()=>v means “return the current value of variable v“, not “return the value v was back when the delegate was created”. Closures close over variables, not over values


因為()=> v意味著“返回變數v的當前值”,而不是“返回值v在委託建立時返回”。 閉合變數,而不是值
也就是說 在委託中填入的變數,是最終的那個變數。這樣就合理解釋了上面為何最終輸出的結果都為5。因為i跳出迴圈時最終的值為5。
接著我們先看一下通過IL,(關於IL指令說明,可以參考這篇文章的最後http://blog.csdn.net/u010533180/article/details/53064257)
反編譯出來的程式碼,建議大家根據上一篇文章畫畫流程圖。

.method private hidebysig static void  ThreadThree() cil managed
{
  // 程式碼大小       130 (0x82)
  .maxstack  4
  .locals init ([0] class [mscorlib]System.Action[] list,
           [1] class [mscorlib]System.Action 'CS$<>9__CachedAnonymousMethodDelegateb',
           [2] class NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc' 'CS$<>8__localsd',
           [3] class [mscorlib]System.Action item,
           [4] bool CS$4$0000,
           [5] class [mscorlib]System.Action[] CS$6$0001,
           [6] int32 CS$7$0002)
  IL_0000:  nop
  //將整數值 5 作為 int32 推送到計算堆疊上。
  IL_0001:  ldc.i4.5
  //將對新的從零開始的一維陣列(其元素屬於特定型別)的物件引用推送到計算堆疊上。
  IL_0002:  newarr     [mscorlib]System.Action
  //從計算堆疊的頂部彈出當前值並將其儲存到索引 0 處的區域性變數列表中。
  IL_0007:  stloc.0
  //  將空引用(O 型別)推送到計算堆疊上。
  IL_0008:  ldnull
  //從計算堆疊的頂部彈出當前值並將其儲存到索引 1 處的區域性變數列表中。
  IL_0009:  stloc.1
  //建立一個值型別的新物件或新例項,並將物件引用(O 型別)推送到計算堆疊上。
  IL_000a:  newobj     instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::.ctor()
  //從計算堆疊的頂部彈出當前值並將其儲存到索引 2 處的區域性變數列表中。
  IL_000f:  stloc.2
  //將索引 2 處的區域性變數載入到計算堆疊上。
  IL_0010:  ldloc.2
  //將整數值 0 作為 int32 推送到計算堆疊上。
  IL_0011:  ldc.i4.0
  //用新值替換在物件引用或指標的欄位中儲存的值。
  IL_0012:  stfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //無條件地將控制轉移到目標指令(短格式)。等於轉移到了IL_0044指令
  IL_0017:  br.s       IL_0044
  IL_0019:  nop
 //將索引 0 處的區域性變數載入到計算堆疊上。
  IL_001a:  ldloc.0
  //將索引 2 處的區域性變數載入到計算堆疊上。
  IL_001b:  ldloc.2
  //查詢物件中其引用當前位於計算堆疊的欄位的值。等於查詢i的值
  IL_001c:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //將索引 1 處的區域性變數載入到計算堆疊上。
  IL_0021:  ldloc.1
  //如果 value 為 true、非空或非零,則將控制轉移到目標指令(短格式)。 此時判斷指令IL_0021的值如果為true ,則跳轉到指令IL_0033
    IL_0022:  brtrue.s   IL_0033
 //將索引 2 處的區域性變數載入到計算堆疊上。
  IL_0024:  ldloc.2
  //將指向實現特定方法的本機程式碼的非託管指標(native int 型別)推送到計算堆疊上。
  IL_0025:  ldftn      instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::'<ThreadThree>b__a'()
  //建立一個值型別的新物件或新例項,並將物件引用(O 型別)推送到計算堆疊上。
  IL_002b:  newobj     instance void [mscorlib]System.Action::.ctor(object,
  //從計算堆疊的頂部彈出當前值並將其儲存到索引 1 處的區域性變數列表中。                                                                 native int)
  IL_0030:  stloc.1
  //無條件地將控制轉移到目標指令(短格式)。等於轉移到了IL_0044指令
  IL_0031:  br.s       IL_0033
 //將索引 1 處的區域性變數載入到計算堆疊上。
  IL_0033:  ldloc.1
  //用計算堆疊上的物件 ref 值(O 型別)替換給定索引處的陣列元素。這裡其實指的就是那個Action型別
  IL_0034:  stelem.ref
  IL_0035:  nop
  //將索引 2 處的區域性變數載入到計算堆疊上。
  IL_0036:  ldloc.2
  //複製計算堆疊上當前最頂端的值,然後將副本推送到計算堆疊上。
  IL_0037:  dup
  //查詢物件中其引用當前位於計算堆疊的欄位的值。等於查詢i的值
  IL_0038:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  將整數值 1 作為 int32 推送到計算堆疊上。
  IL_003d:  ldc.i4.1
  //將兩個值相加並將結果推送到計算堆疊上。
  IL_003e:  add
  //用新值替換在物件引用或指標的欄位中儲存的值。
  IL_003f:  stfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //將索引 2 處的區域性變數載入到計算堆疊上。
  IL_0044:  ldloc.2
  //查詢物件中其引用當前位於計算堆疊的欄位的值。等於查詢i的值
  IL_0045:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
   //將索引 0 處的區域性變數載入到計算堆疊上。
  IL_004a:  ldloc.0
  //  將從零開始的、一維陣列的元素的數目推送到計算堆疊上。
  IL_004b:  ldlen
  //  將位於計算堆疊頂部的值轉換為 int32。
  IL_004c:  conv.i4
  //  比較兩個值。如果第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆疊上;反之,將 0 (int32) 推送到計算堆疊上。
  IL_004d:  clt
  //從計算堆疊的頂部彈出當前值並將其儲存在區域性變數列表中的 index 處(短格式)。
  IL_004f:  stloc.s    CS$4$0000  即 CS$4$0000 這個所在的索引
  //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_0051:  ldloc.s    CS$4$0000
  // 判斷此時是否ture,如果為true 則跳轉到指令IL_0019
  IL_0053:  brtrue.s   IL_0019
  IL_0055:  nop
  //將索引 0 處的區域性變數載入到計算堆疊上。
  IL_0056:  ldloc.0
   //從計算堆疊的頂部彈出當前值並將其儲存在區域性變數列表中的 index 處(短格式)。
  IL_0057:  stloc.s    CS$6$0001
  //將整數值 0 作為 int32 推送到計算堆疊上。
  IL_0059:  ldc.i4.0
  //從計算堆疊的頂部彈出當前值並將其儲存在區域性變數列表中的 index 處(短格式)。
  IL_005a:  stloc.s    CS$7$0002
  //  無條件地將控制轉移到目標指令(短格式)。 轉移到IL_OO73
  IL_005c:  br.s       IL_0073
  //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_005e:  ldloc.s    CS$6$0001
    //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_0060:  ldloc.s    CS$7$0002
  //將位於指定陣列索引處的包含物件引用的元素作為 O 型別(物件引用)載入到計算堆疊的頂部。
  IL_0062:  ldelem.ref
  //從計算堆疊的頂部彈出當前值並將其儲存到索引 3 處的區域性變數列表中。
  IL_0063:  stloc.3
  IL_0064:  nop
  //  將索引 3 處的區域性變數載入到計算堆疊上。
  IL_0065:  ldloc.3
  //呼叫虛方法 執行Action 方法
  IL_0066:  callvirt   instance void [mscorlib]System.Action::Invoke()
  IL_006b:  nop
  IL_006c:  nop
  //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_006d:  ldloc.s    CS$7$0002
    //將整數值 1作為 int32 推送到計算堆疊上。
  IL_006f:  ldc.i4.1
  //將兩個值相加並將結果推送到計算堆疊上。
  IL_0070:  add
 //從計算堆疊的頂部彈出當前值並將其儲存在區域性變數列表中的 index 處(短格式)。
  IL_0071:  stloc.s    CS$7$0002
  //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_0073:  ldloc.s    CS$7$0002
  //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_0075:  ldloc.s    CS$6$0001
  //  將從零開始的、一維陣列的元素的數目推送到計算堆疊上。
  IL_0077:  ldlen
   //  將位於計算堆疊頂部的值轉換為 int32。
  IL_0078:  conv.i4
  //比較兩個值。如果第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆疊上;反之,將 0 (int32) 推送到計算堆疊上。
  IL_0079:  clt
    //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_007b:  stloc.s    CS$4$0000
    //將特定索引處的區域性變數載入到計算堆疊上(短格式)。
  IL_007d:  ldloc.s    CS$4$0000
  //判斷此時的值是否為true,如果為true 則跳轉到指令IL_005e.這是應該判斷陣列是否遍歷到了末尾
  IL_007f:  brtrue.s   IL_005e
  IL_0081:  ret
} // end of method ThreadDemo::ThreadThree

.NET Reflector 反編譯的程式碼:

 Action[] actionArray = new Action[5];
    Action action = null;
    for (int i = 0; i < actionArray.Length; i++)
    {
        if (action == null)
        {
            action = () => Console.WriteLine(i);
        }
        actionArray[i] = action;
    }
    foreach (Action action2 in actionArray)
    {
        action2();
    }

那麼上面的例子,如何輸出0-4呢?根據上句話的提示,只需要建立一個變數,儲存當前執行狀態的值即可。修改後的結果為:

            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                int localI = i;
                list[i] = () => { Console.WriteLine(localI); };
            }
            foreach (var item in list)
            {
                item();
            }

或者是新增一個額外的方法也行,這樣就相當於建立了一個區域性變數。程式碼如下:

     var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                AddList(list, i);
            }
            foreach (var item in list)
            {
                item();
            }
        static void AddList(Action[] list, int i)
        {
            list[i] = () => { Console.WriteLine(i); };
        }

上面兩種方法執行輸出的結果都為:0-4.

通過上面的分析,加深理解了C#中的閉包,以後要謹慎使用。匿名函式和Lambda表示式給我們的程式設計帶來了許多快捷簡單的實現,如(List.Max((a)=>a.Level)等寫法)。但是我們要清醒的意識到這兩個糖果後面還是有個”坑“(閉包)。這再次告訴我們技術工作人,要”知其然,也要知其所以然“。

下面給出完整的程式碼,其中有一些是我自己研究的,上面沒有給出分析,建議讀者自己分析,加深理解:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace NowCoderProgrammingProject
{
    class ThreadDemo
    {
        public static void Main()
        {
            ThreadOne();
            ThreadOne2();
            ThreadTwo();
            ThreadThree();
            ThreadThree1();
        }

        private static void ThreadOne()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(() =>
                {
                    Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, i));
                });
                t.Name = string.Format("Thread{0}", i);
                t.IsBackground = true;
                t.Start();
            }
            Console.ReadLine();
        }

        private static void ThreadOne2()
        {
            for (int i = 0; i < 10; i++)
            {
                int localId = i;
                Thread t = new Thread(() =>
                {
                    Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, localId));
                });
                t.Name = string.Format("Thread{0}", i);
                t.IsBackground = true;
                t.Start();
            }
            Console.ReadLine();
        }

        private static void ThreadTwo()
        {
            int id = 0;
            for (int i = 0; i < 10; i++)
            {
                NewMethod(i, id++);
            }
            Console.ReadLine();
        }

        private static void NewMethod(int i, int readTimeID)
        {
            Thread t = new Thread(() =>
            {
                Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, readTimeID));
            });
            t.Name = string.Format("Thread{0}", i);
            t.IsBackground = true;
            t.Start();
        }

        static void ThreadThree()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = () => { Console.WriteLine(i); };
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void ThreadThree1()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                int localI = i;
                list[i] = () => { Console.WriteLine(localI); };
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void ThreadThree2()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                AddList(list, i);
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void AddList(Action[] list, int i)
        {
            list[i] = () => { Console.WriteLine(i); };
        }
    }
}

為了防止上面的文章失效,下面一篇部落格進行對其翻譯。