在 C# 2.0 中,編譯器會根據匿名函數的使用場景自動決定生成的方法是否為 static。這也是為何規範禁止在匿名方法中使用 goto、break、continue 跳出,或從外部跳入的原因——雖然代碼寫在同一作用域,但實際編譯後並不在同一位置。
更方便的是,編譯器能根據上下文自動推斷匿名函數的參數,無需開發者顯式指定。例如:
button1.Click += delegate(Object sender, EventArgs e) { MessageBox.Show("The Button was clicked!"); };
若不使用參數,可簡寫為:
button1.Click += delegate { MessageBox.Show("The Button was clicked!"); };
相較於匿名函數本身的實現,更複雜的是它對外層作用域變量的捕獲與生命週期管理。微軟的 Grant Ri 在部落格中有系列文章深入探討此主題:
核心挑戰有二:一是匿名函數如何訪問不在同一作用域的父函數或類變量;二是被捕獲變量的生命週期必須與匿名函數綁定,而非父函數的調用週期。這兩點迫使 C# 編譯器採用獨立類封裝的方式來實現。
首先,匿名函數使用的父函數局部變量(無論引用類型或值類型)都必須從棧上轉移到堆上,以便在作用域外仍可被訪問並控制生命週期。因為棧變量的生命週期與擁有者函數綁定,函數返回後棧框銷毀,無法實現變量生命週期與函數調用的解耦。
例如下面這個簡單案例,匿名函數捕獲了父函數的局部變量 i,雖然僅在父函數內部使用,編譯器仍會生成封裝類:
delegate void Delegate1();
public void Method1()
{
int i = 0;
Delegate1 d1 = delegate() { i++; };
d1();
}
編譯器生成的封裝代碼類似如下:
delegate void Delegate1();
private sealed class __LocalsDisplayClass$00000002
{
public int i;
public void __AnonymousMethod$00000001()
{
this.i++;
}
}
public void Method1()
{
__LocalsDisplayClass$00000002 local1 = new __LocalsDisplayClass$00000002();
local1.i = 0;
Delegate1 d1 = new Delegate1(local1.__AnonymousMethod$00000001);
d1();
}
這種「顯式環境類」模式正是 C# 實現閉包的核心機制,將捕獲變量提升為堆上物件的欄位,確保其生命週期延長至委托不再被引用為止。