根據 Unified C# 3.0 Specification 第 7.14.4 節,任何包含在 Lambda 運算式或匿名方法範圍內的區域變數、值參數或參數陣列,都稱為該匿名函數的「外部變數」。在類別的實例函數成員中,this 被視為一個值參數,因此也是任何包含在該函數成員內的匿名函數的外部變數。
當外部變數被匿名函數參考時,該變數就被稱為被「捕獲」了。被捕獲的外部變數其生命週期會延長,至少持續到由該匿名函數建立的委派或運算式樹符合垃圾回收條件為止。
不同語言對閉包的記憶體配置方式不同。C# 編譯器的做法是:將逃逸的變數提升為欄位,並將匿名委派提升為一個方法——但不是提升到原本的類別中,而是一個編譯器生成的私有巢狀類別中。以先前的範例為例,編譯器會將其轉換為類似以下的偽代碼:
public class Bravo : Alpha {
public override void Blah() {
Console.WriteLine("Bravo.Blah");
base.Blah();
}
// compiler generated inner class
private class __locals {
public int __x;
public Bravo __this;
public void __method() {
this.__this.Blah();
// 非虛擬呼叫基類方法
__nonvirtual__ ((Alpha)this.__this).Blah());
Console.WriteLine(this.__x);
}
}
public void CharlIE() {
__locals locals = new __locals();
locals.__x = 123;
locals.__this = this;
D d = new D(locals.__method);
d();
}
}
此處的 __nonvirtual__ 並非真實 C# 關鍵字,用以表示編譯器生成非虛擬呼叫(call IL 指令)而非虛擬呼叫(callvirt)。一般方法呼叫使用 callvirt,但透過 base 關鍵字呼叫基類方法時,為避免虛擬調度,必須使用 call。
base 作用域洩露可以看到,原始碼中匿名委派內對 base 的存取,實際上被生成到了另一個類別(私有巢狀類別)的方法中。而該巢狀類別的基類是 System.Object,並非 Alpha。這導致 base 關鍵字的作用域似乎「洩露」到了它不應該出現的地方。雖然編譯出的代碼能正常執行,但因為違反了 CLI 的驗證規則,所以屬於「不可驗證代碼」。
這不是程式設計師的錯誤,他們只是在正確的地方正確使用 base。.NET Framework 的應對方式是發出警告(CS1911),提醒開發者將這類存取移至輔助方法中以避開問題。可惜的是,Mono 1.2.5.1 並未提供任何警告。若用 Mono 編譯上述代碼,再用 .NET Framework 的 PEVerify 驗證,會看到前文所述的錯誤訊息。