在 C# 中,當派生類別的方法內使用匿名委派(或 Lambda 運算式)呼叫基類別的方法時,編譯器產生的 IL 代碼可能會導致「無法驗證」的錯誤。這是一個經典且深入的問題,涉及閉包的實作機制、編譯器優化策略以及 .NET 執行環境的驗證規則。本文將延續前幾篇的討論,深入剖析這一現象的成因、編譯器的優化行為以及 Mono 與 .NET Framework 的差異。
當使用 PEVerify 工具驗證編譯後的組件時,會出現如下錯誤:
Microsoft (R) .Net Framework PE Verifier. Version 3.5.20706.1
Copyright (c) Microsoft Corporation. All rights reserved.
[IL]: Error: [F:\FX\share\testClosure.exe : Bravo+<>c__CompilerGenerated0::<CharlIE>c__1][offset 0x00000011] The 'this' parameter to the call must be the calling method's 'this' parameter.
1 Error Verifying testClosure.exe
對應的 IL 代碼為:
IL_0011: call instance void Alpha::Blah()
這行 IL 正是原始碼中 base.Blah() 呼叫所生成的指令。問題的核心在於:匿名委派被編譯器提升到一個編譯器生成的私有巢狀類別中,而在該巢狀類別的方法中呼叫 call instance void Alpha::Blah() 時,傳遞的 this 指標並非該巢狀類別的實例,而是外部類別 Bravo 的實例。這違反了 CLI 驗證規則:call 指令要求傳遞的 this 必須是當前方法所屬類別的實例。
在 C# 3.0 中使用 Lambda 運算式時,情況完全相同:
public void CharlIE() {
int x = 123;
D d = () => {
this.Blah();
base.Blah();
Console.WriteLine(x);
};
d();
}
編譯器同樣會生成不可驗證的代碼,並發出警告 CS1911。
this 逃逸時有趣的是,如果移除區域變數 x,使得匿名委派捕獲的唯一外部變數只有 this:
public void CharlIE() {
D d = delegate {
this.Blah();
base.Blah();
};
d();
}
.NET Framework 的 C# 編譯器會進行優化:它不再生成私有巢狀類別,而是將匿名委派直接生成為外部類別 Bravo 的一個私有成員方法。偽代碼如下:
public class Bravo : Alpha {
public override void Blah() {
Console.WriteLine("Bravo.Blah");
base.Blah();
}
// compiler generated method
private void __method() {
this.Blah();
// 非虛擬呼叫基類方法,C# 中無直接語法,此處用 __nonvirtual__ 表示
__nonvirtual__ ((Alpha)this).Blah();
}
public void CharlIE() {
D d = new D(this.__method);
d();
}
}
在這種情況下,編譯器生成的代碼是可驗證的,因為 __method 位於 Bravo 類別中,呼叫 base.Blah() 時的 this 正是 Bravo 的實例,符合驗證規則。然而,為了行為一致性,編譯器仍會發出 CS1911 警告。
Mono 編譯器並未實作這項優化,即使只有 this 逃逸,它仍會生成巢狀類別,導致同樣的不可驗證代碼,且不發出任何警告。這反映了 Mono 為了相容性選擇模仿 .NET Framework 的一般行為,而非採用相同的優化策略。
這一系列文章詳細闡述了 C# 匿名委派捕獲基類方法呼叫時的閉包實作細節、編譯器生成代碼的驗證問題、優化條件以及跨平台差異。理解這些機制有助於開發者編寫更安全、可驗證的代碼,並在必要時通過輔助方法規避警告。