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