1. 程式人生 > >C# 匿名函式引用區域性變數解析

C# 匿名函式引用區域性變數解析

using System;

namespace Application
{
	class Test
	{
		Action action;

		public Test()
		{
			int value = 2046;
			action = () => Console.WriteLine(value);
		}

		static void Main(string[] args)
		{
			Test test = new Test();
			test.action();
		}
	}
}

在 Test 建構函式裡,區域性變數 value 在建構函式執行結束後出棧,那麼 C# 是如何實現在函式執行以後訪問其中的區域性變數的?

 

 

你必須瞭解:引用型別、值型別、引用、物件、值型別的值(簡稱值)。

關於引用、物件和值在記憶體的分配有如下幾點規則: •物件分配在堆中。 •作為欄位的引用分配在堆中(內嵌在物件中)。
•作為區域性變數(引數也是區域性變數)的引用分配在棧中。 •作為欄位的值分配在堆中(內嵌在物件中)。
•作為區域性變數(引數也是區域性變數)的值用分配在棧中。 •區域性變數只能存活於所在的作用域(方法中的大括號確定了作用域的長短)。

注:按值傳遞和按引用傳遞也是需要掌握的知識點,C# 預設是按值傳遞的。

概念

內層的函式可以引用包含在它外層的函式的變數,即使外層函式的執行已經終止。但該變數提供的值並非變數建立時的值,而是在父函式範圍內的最終值。

條件

閉包是將一些執行語句的封裝,可以將封裝的結果像物件一樣傳遞,在傳遞時,這個封裝依然能夠訪問到原上下文。
形成閉包有一些值得總結的非必要條件:
1、巢狀定義的函式。
2、匿名函式。
3、將函式作為引數或者返回值。
4、在.NET中,可以通過匿名委託形成閉包:函式可以作為引數傳遞,也可以作為返回值返回,或者作為函式變數。而在.NET中,這都可以通過委託來實現。這些是實現閉包的前提。

閉包的優點:

  使用閉包,我們可以輕鬆的訪問外層函式定義的變數,這在匿名方法中普遍使用。比如有如下場景,在winform應用程式中,我們希望做這麼一個效果,當用戶關閉窗體時,給使用者一個提示框。

private void Form1_Load(object sender, EventArgs e)
{
       string msg= "您將關閉當前對話方塊";
       this.FormClosing += delegate
       {
            MessageBox.Show(msg);
       };
}

匿名函式很容易的訪問到了作用域之外的變數。

閉包陷阱

全域性變數

public static int i;//這個不是閉包

static void Main(string[] args)
{
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        i = counter;
        actions.Add(() => Console.WriteLine(i));
    }
    i = 123;
    //執行動作
    foreach (Action action in actions)
        action();

    Console.ReadKey();

}
public static int i;//這個不是閉包
static void TempMethod()
{
    Console.WriteLine(i);
}
static void Main(string[] args)
{
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        i = counter;
        actions.Add(new Action(TempMethod));
    }
    //執行動作
    foreach (Action action in actions)
        action();

    Console.ReadKey();

}

閉包示例一

static void Main()
{
    int i;//[1]閉包一
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        i = counter;
        actions.Add(() => Console.WriteLine(i));
    }
    //執行動作
    foreach (Action action in actions)
        action();
    Console.ReadKey();
}

執行結果:
這裡寫圖片描述

顯然這個結果不是我們想要的,上面的程式相當於下面的示例程式碼:

static void Main()
{
    TempClass tc = new TempClass();
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        tc.i = counter;
        actions.Add(tc.TempMethod);
    }
    //執行動作
    foreach (Action action in actions)
        action();

    Console.ReadKey();
}

class TempClass
{
    public int i;
    public void TempMethod()
    {
        Console.WriteLine(i);
    }
}

閉包示例二

static void Main()
{
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int i = 0; i < 10; i++)//[3]閉包二
    {
        actions.Add(() => Console.WriteLine(i));
    }
    //執行動作
    foreach (Action action in actions)
        action();
    Console.ReadKey();
}

上面的程式相當於下面的示例程式碼:

static void Main()
{
    //定義動作組
    List<Action> actions = new List<Action>();
    TempClass tc = new TempClass();
    for (tc.i = 0; tc.i < 10; tc.i++)
    {
        actions.Add(new Action(tc.TempMethod));
    }
    //執行動作
    foreach (Action action in actions)
        action();
    Console.ReadKey();
}
class TempClass
{
    public int i;
    public void TempMethod()
    {
        Console.WriteLine(i);
    }
}

執行結果:
這裡寫圖片描述
這個結果也不是我們預期的。

分析

以示例一為例說明程式碼執行機制:

首先:C#編譯器 為我們生成了一個 ‘<>c__DisplayClass0_0’的類,一個 “< Main > b__0”的方法 和 一個 變數 i。這個public int32 i 的變數就是程式一開始我們定義的變數i,現在被包裝到了類中。

這裡寫圖片描述

.method assembly hidebysig instance void 
        '<Main>b__0'() cil managed
{
  // 程式碼大小       13 (0xd)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldfld      int32 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::i
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_000b:  nop
  IL_000c:  ret
} // end of method '<>c__DisplayClass0_0'::'<Main>b__0'

上面這個是”< Main > b__0”方法的IL程式碼:就是輸出

System.Console::WriteLine(int32)

下面是Main主程式的IL程式碼:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // 程式碼大小       140 (0x8c)
  .maxstack  4
  .locals init ([0] class 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0' 'CS$<>8__locals0',
           [1] class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action> actions,
           [2] int32 counter,
           [3] class [mscorlib]System.Action V_3,
           [4] bool V_4,
           [5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action> V_5,
           [6] class [mscorlib]System.Action action)
  IL_0000:  newobj     instance void 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  nop
  IL_0007:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::.ctor()
  IL_000c:  stloc.1
  IL_000d:  ldc.i4.0
  IL_000e:  stloc.2
  IL_000f:  br.s       IL_0044
  IL_0011:  nop
  IL_0012:  ldloc.0
  IL_0013:  ldloc.2
  IL_0014:  stfld      int32 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::i
  IL_0019:  ldloc.1
  IL_001a:  ldloc.0
  IL_001b:  ldfld      class [mscorlib]System.Action 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::'<>9__0'
  IL_0020:  dup
  IL_0021:  brtrue.s   IL_0039
  IL_0023:  pop
  IL_0024:  ldloc.0
  IL_0025:  ldloc.0
  IL_0026:  ldftn      instance void 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::'<Main>b__0'()
  IL_002c:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_0031:  dup
  IL_0032:  stloc.3
  IL_0033:  stfld      class [mscorlib]System.Action 'CSharp閉包之區域性變數一'.Program/'<>c__DisplayClass0_0'::'<>9__0'
  IL_0038:  ldloc.3
  IL_0039:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::Add(!0)
  IL_003e:  nop
  IL_003f:  nop
  IL_0040:  ldloc.2
  IL_0041:  ldc.i4.1
  IL_0042:  add
  IL_0043:  stloc.2
  IL_0044:  ldloc.2
  IL_0045:  ldc.i4.s   10
  IL_0047:  clt
  IL_0049:  stloc.s    V_4
  IL_004b:  ldloc.s    V_4
  IL_004d:  brtrue.s   IL_0011
  IL_004f:  nop
  IL_0050:  ldloc.1
  IL_0051:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::GetEnumerator()
  IL_0056:  stloc.s    V_5
  .try
  {
    IL_0058:  br.s       IL_006b
    IL_005a:  ldloca.s   V_5
    IL_005c:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>::get_Current()
    IL_0061:  stloc.s    action
    IL_0063:  ldloc.s    action
    IL_0065:  callvirt   instance void [mscorlib]System.Action::Invoke()
    IL_006a:  nop
    IL_006b:  ldloca.s   V_5
    IL_006d:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>::MoveNext()
    IL_0072:  brtrue.s   IL_005a
    IL_0074:  leave.s    IL_0085
  }  // end .try
  finally
  {
    IL_0076:  ldloca.s   V_5
    IL_0078:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action>
    IL_007e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0083:  nop
    IL_0084:  endfinally
  }  // end handler
  IL_0085:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_008a:  pop
  IL_008b:  ret
} // end of method Program::Main

  編譯器生成IL程式碼後,將作用域外的變數i,放到了匿名型別‘<>c__DisplayClass0_0’中當做成員欄位來使用,由此,本來應該在堆疊上的int型i,被編譯器包裝成了object類型別的成員欄位,而object被儲存在堆中。

  其實C#並不會對每個需要捕獲的值型別變數進行裝箱操作,而是把所有捕獲的變數統統放到同一個大“箱子”裡——當編譯器遇到需要變數捕獲的情況時,它會默默地在後臺構造一個匿名型別,這個匿名型別包含了每一個閉包所捕獲的變數(包括值型別變數和引用型別變數)作為它的一個公有欄位。這樣,編譯器就可以維護那些在匿名函式或lambda表示式中出現的外部變量了。

總結

編譯器將閉包引用的區域性變數轉換為匿名型別的欄位,導致了局部變數分配在堆中。

避免閉包陷阱

如何避免閉包陷阱呢?C#中普遍的做法是,將匿名函式引用的變數用一個臨時變數儲存下來,然後在匿名函式中使用臨時變數。

閉包示例三

static void Main()
{
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        int i;//[1]閉包三
        i = counter;
        //int copy = counter;//換種寫法
        actions.Add(() => Console.WriteLine(i));
    }
    //執行動作
    foreach (Action action in actions)
        action();

    Console.ReadKey();
}

上面的程式相當於下面的示例程式碼:

static void Main()
{
    //定義動作組
    List<Action> actions = new List<Action>();
    for (int counter = 0; counter < 10; counter++)
    {
        TempClass tc = new TempClass();
        tc.i = counter;
        actions.Add(tc.TempMethod);
    }
    //執行動作
    foreach (Action action in actions)
        action();
    Console.ReadKey();
}
class TempClass
{
    public int i;
    public void TempMethod()
    {
        Console.WriteLine(i);
    }
}

執行結果:
這裡寫圖片描述

與此同時,我們也可以在知道閉包的副作用的情況下(內層的函式可以引用包含在它外層的函式的變數,即使外層函式的執行已經終止。但該變數提供的值並非變數建立時的值,而是在父函式範圍內的最終值)加以利用。

 

轉自:https://blog.csdn.net/cjolj/article/details/60868305