C# 2.0 引入匿名方法,允許在需要委托值的地方直接編寫代碼塊,類似 Lambda 表達式的前身。它不僅簡化語法,更通過外部變量捕獲實現閉包語義,極大增強了委托的表達力。本文完整解析規範第 21 章:表達式語法、簽名兼容、轉換規則、塊限制、外部變量捕獲與生命週期、計算語義、委托創建優化。
anonymous_method_expression:
delegate anonymous_method_signature_opt block
anonymous_method_signature:
( anonymous_method_parameter_list_opt )
anonymous_method_parameter_list:
anonymous_method_parameter (',' anonymous_method_parameter)*
anonymous_method_parameter:
parameter_modifier_opt type identifier
params 不影響兼容性。void → 匿名方法中 return 不得帶運算式。R → 所有 return 運算式需隱式轉換至 R,且塊結束點不可達(即所有路徑必須 return/throw)。delegate void D(int x);
D d1 = delegate { }; // OK (無簽名)
D d2 = delegate() { }; // 錯誤:簽名不匹配 (無參 vs 1參)
D d3 = delegate(long x) { }; // 錯誤:類型不匹配
D d4 = delegate(int x) { }; // OK
D d5 = delegate(int x) { return; }; // OK
D d6 = delegate(int x) { return x; }; // 錯誤:返回類型不匹配
delegate void E(out int x);
E e1 = delegate { }; // 錯誤:委托有 out 參數
E e2 = delegate(out int x) { x = 1; }; // OK
E e3 = delegate(ref int x) { x = 1; }; // 錯誤:ref/out 不匹配
delegate int P(params int[] a);
P p1 = delegate { }; // 錯誤:塊結束點可達
P p2 = delegate { return; }; // 錯誤:返回類型不匹配
P p3 = delegate { return 1; }; // OK
P p4 = delegate { return "Hello"; }; // 錯誤
P p5 = delegate(int[] a) { return a[0]; } // OK
P p6 = delegate(params int[] a) { return a[0]; } // 錯誤:匿名方法簽名不可含 params
object 或其他類型。new D(delegate { ... }) 等價於強制轉換 (D)delegate { ... }。| 限制 | 說明 |
|---|---|
| 參數訪問 | 有簽名:參數可訪問。無簽名:參數不可訪問(即使委托有參數)。 |
| ref/out 參數 | 僅能訪問最近封閉匿名方法簽名中的 ref/out 參數。外層方法的 ref/out 禁止訪問。 |
| this 訪問 | 若 this 為結構類型 → 編譯錯誤(顯式 this.x 或隱式 x 皆禁)。類類型無限制。 |
| 外部變量 | 可自由訪問捕獲的外部變量(21.5)。計算時引用活躍實例。 |
| 跳轉語句 | 塊內 goto/break/continue 目標若在塊外或內嵌匿名方法塊內 → 錯誤。 |
| return | 從最近封閉匿名方法返回,非封閉函數成員。運算式需與委托兼容。 |
| 實現細節 | 編譯器可合成命名方法/類型實現,合成名稱保留雙底線(__)。 |
作用域包含匿名方法表達式的區域變數、值參數、參數陣列,以及實例函數成員中的 this(視為值參數)。
外部變量被匿名方法引用即被捕獲。正常區域變量生命週期限於關聯塊/語句執行期;捕獲變量生命週期至少延長至引用其的委托符合垃圾回收條件為止。
delegate int D();
static D F() {
int x = 0;
D result = delegate { return ++x; } // 捕獲 x
return result;
}
static void Main() {
D d = F();
Console.WriteLine(d()); // 1
Console.WriteLine(d()); // 2
Console.WriteLine(d()); // 3
}
x 被捕獲,生命週期超出 F(),三次調用操作同一實例,輸出 1,2,3。
捕獲變量不再視為
fixed(固定),變為movable(可移動)。Unsafe 代碼取址需先用fixed語句固定。
程序執行進入變量作用域時發生實例化。循環中聲明 → 每次迭代實例化一次;循環外聲明 → 單次實例化。這直接影響捕獲行為:
// 情況 A:循環內聲明 → 每次迭代新實例
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1; // 每次迭代新 x
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
// 輸出:1, 3, 5
// 情況 B:循環外聲明 → 單一實例
static D[] F() {
D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1; // 同一 x
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
// 輸出:5, 5, 5
編譯器允許但不強制將語義相同、捕獲變量集合相同的匿名方法優化為同一委托實例。
static D[] F() {
D[] result = new D[3];
int x = 0; // 共享實例
for (int i = 0; i < 3; i++) {
int y = 0; // 每次迭代新實例
result[i] = delegate { Console.WriteLine("{0} {1}", ++x, ++y); };
}
return result;
}
// 輸出:
// 1 1
// 2 1
// 3 1
多個匿名方法可捕獲同一外部變量實例實現「通信」:
int x = 0;
Setter s = delegate(int value) { x = value; };
Getter g = delegate { return x; };
s(5); Console.WriteLine(g()); // 5
s(10); Console.WriteLine(g()); // 10
this 值均未指定(實現細節)。static double[] Apply(double[] a, Function f) { ... }
static void F(double[] a, double[] b) {
a = Apply(a, delegate(double x) { return Math.Sin(x); });
b = Apply(b, delegate(double y) { return Math.Sin(y); });
// 兩匿名方法語義相同、捕獲集合均為空 → 編譯器可合並為同一委托
}
兩個委托實例相等(==)條件:同類型、同目標對象、同目標方法。匿名方法產生的委托目標未指定,故不保證相等性比較結果,除非編譯器合並優化。
ref/out 跨層訪問、結構 this 訪問、跳轉語句逃逸,皆為防止破壞棧幀/閉包結構完整性。=>)在語法上簡化匿名方法,語義完全繼承。理解匿名方法規範即理解 Lambda 底層機制。掌握第 21 章規範,不僅能精確預測捕獲行為、避免閉包陷阱,更能深入理解 C# 委托、事件、LINQ、異步編程的基石機制。