閉包允許內部函數訪問外部作用域變量,並在外部函數返回後**延長變量壽命**。C# 編譯器通過**生成顯示類** 實現此機制,將捕獲變量「提升」為類字段。本文通過 IL 反編譯、值類型/引用類型差異、foreach 陷阱三個維度,徹底講透捕獲原理。
public Func<string> CreateFunction() {
string str = "我的幸運數字是";
int num = 17;
Func<string> func = () => str + num; // 捕獲 str、num
return func;
}
// 調用
var f = CreateFunction();
Console.WriteLine(f()); // 輸出: 我的幸運數字是17
關鍵點:CreateFunction 棧幀銷毀後,str、num 理應銷毀,但閉包 f 仍能訪問 —— **編譯器在堆上為它們「續命」**。
// 簡化後的編譯器生成代碼
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0 {
public string str; // 捕獲的引用類型字段
public int num; // 捕獲的值類型字段 (無裝箱!)
public string <CreateFunction>b__0() {
return this.str + this.num; // Lambda 體變為實例方法
}
}
public Func<string> CreateFunction() {
var display = new <>c__DisplayClass0_0();
display.str = "我的幸運數字是";
display.num = 17;
return new Func<string>(display.<CreateFunction>b__0);
}
public int num),避免裝箱/拆箱開銷。this 訪問捕獲變量。| 類型 | 捕獲前存儲 | 捕獲後存儲 | 壽命延長機制 |
|---|---|---|---|
| 引用類型 (string, class) | 棧上引用 → 堆上對象 | 顯示類字段引用 → 同一堆上對象 | 顯示類實例引用計數 +1,GC 保留對象 |
| 值類型 (int, struct) | 棧上直接值 | 顯示類字段直接值 (無裝箱) | 顯示類實例在堆上,值類型隨實例生存 |
結論:C# 選擇**顯示類字段提升**而非**裝箱**,優勢:無裝箱開銷、類型安全、支持 ref/out 捕獲 (C# 7+)。
var funcs = new List<Func<int>>();
for (int i = 0; i < 3; i++) {
funcs.Add(() => i); // 捕獲同一個 i
}
foreach (var f in funcs) Console.WriteLine(f());
// 輸出: 3, 3, 3 (循環結束 i=3)
var funcs = new List<Func<int>>();
foreach (int i in Enumerable.Range(0, 3)) {
funcs.Add(() => i); // 每次迭代捕獲新的 i
}
foreach (var f in funcs) Console.WriteLine(f());
// 輸出: 0, 1, 2
for (int i = 0; i < 3; i++) {
funcs.Add(() => i); // 共享 i
}
// 輸出: 3, 3, 3
// 修正: 引入局部副本
for (int i = 0; i < 3; i++) {
int captured = i;
funcs.Add(() => captured);
}
// 輸出: 0, 1, 2
ref int / out int (由 System.Runtime.CompilerServices.IsByRef 標記)。<>c__DisplayClass1_0 包含 <>c__DisplayClass0_0 字段)。async 方法編譯為狀態機結構體,捕獲變量同樣提升為狀態機字段。| 場景 | 開銷 | 優化 |
|---|---|---|
| 熱路徑頻繁創建閉包 | 顯示類分配 + 委託分配 (2次堆分配) | 改用 struct 狀態 + 靜態方法 / delegate* 函數指針 (C# 11+) |
| 捕獲大量變量 | 顯示類體積大,GC 掃描慢 | 精簡捕獲列表,僅捕獲必要變量 |
| 值類型大結構體捕獲 | 逐字段複製到顯示類 | 改捕獲引用 (ref struct / 類包裝) |
閉包是「編譯器為你寫的類」。理解顯示類機制,能讓你在面對 foreach 陷阱、記憶體洩漏、熱路徑性能時,從 IL 層面精準定位、優雅解決。