1. 介紹
在很多現代的 C# 專案中,幾乎看不到用來處理事件的「普通」方法。這是因為 lambda 表達式可以讓你在訂閱的地方(運算子 +=)快速且簡潔地聲明處理器,當處理邏輯簡單且不會在其他地方重用時特別方便。這有點像直接把一張小便條貼在咖啡機上寫著「按這個按鈕」,而不是寫一個詳細的手冊放在單獨的資料夾裡。如果任務是局部且一次性的,lambda 就很適合!
在實務中的使用場景
- 在 ASP.NET 中(例如處理頁面生命週期事件),
- 在 WPF/WinForms 的 UI 中(例如按鈕點擊),
- 在伺服器端程式中(例如 pipeline 裡的邏輯),
- 在測試中,當處理器不需要單獨名稱時。
語法:程式碼長什麼樣子
先看用普通方法處理事件的例子:
// 聲明事件
public event EventHandler? MyEvent;
// 使用普通方法訂閱事件
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("事件發生了!");
}
public void Subscribe()
{
MyEvent += Handler;
}
現在看用 lambda 表達式(匿名函式)的版本:
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("事件發生了(lambda)!");
}
注意:我們沒有創建單獨的方法,而是直接在訂閱時指定處理器。lambda 的簽名會自動匹配事件的類型(EventHandler)。
方法比較
| 普通方法 | Lambda | |
|---|---|---|
| 程式碼量 | 較多(方法 + 訂閱) | 較少,所有東西都在一起 |
| 可重用性 | 可重用 | 通常不可重用 |
| 程式碼就近性 | 分散 | 集中在一起 |
| 清晰度/可讀性 | 適合複雜邏輯 | 適合簡單情況 |
2. 有事件處理與 lambda 的應用示例
基本結構
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"選單項目 {index} 已選擇。");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
用 lambda 訂閱
class Program
{
static void Main()
{
var menu = new Menu();
// 用 lambda 訂閱事件
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("感謝你的選擇!Lambda 處理器被觸發了。");
};
menu.SelectItem(1);
}
}
預期輸出:
選單項目 1 已選擇。
感謝你的選擇!Lambda 處理器被觸發了。
從外部作用域捕獲變數
lambda 的一個優點是它可以「記住」外部 scope 的值(閉包)。例如,統計選單被選了多少次:變數 counter 會被閉包捕獲。
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"項目被選擇了 {counter} 次!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
預期輸出:
選單項目 1 已選擇。
項目被選擇了 1 次!
選單項目 2 已選擇。
項目被選擇了 2 次!
這就是閉包的魔力:變數 counter 在 lambda 裡持續存在!
帶事件參數的範例
如果你的事件使用 EventHandler<T>,其中 T 是帶額外資訊的自定義類別,lambda 會自動匹配相應的簽名。
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"選單項目 {index}: {description} 已選擇。");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// 使用範例
static void Main()
{
var menu = new Menu();
// 帶事件參數的 lambda
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"選擇了項目 #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "關於程式");
}
預期輸出:
選單項目 3: 關於程式 已選擇。
選擇了項目 #3: 關於程式(以大寫顯示)
3. 區域處理器與 lambda
lambda 很適合在以下情況使用:
- 處理邏輯短且清晰,
- 處理器只在一個地方使用,
- 需要從本地上下文「閉包」變數。
如果處理邏輯複雜、需要重用或可能在宣告地點之外被呼叫,最好使用命名方法。
範例:把邏輯放在內部或外部
Lambda(理想情況):
button.Click += (s, e) => MessageBox.Show("按鈕被按下!");
方法(當邏輯較複雜或需重用時):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("資料已保存!");
}
}
4. 底層:lambda 處理器背後發生了什麼
lambda 本質上也是 delegate,和普通處理器沒兩樣。編譯器會在背後生成匿名方法,如果裡面有被捕獲的變數,還會生成隱藏類別來儲存那些變數。
要注意一個常見錯誤:如果你在迴圈內創建 lambda 並訂閱事件,所有迭代可能會捕獲同一個迴圈變數。
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"點擊了按鈕 #{i}");
};
}
結果可能是所有處理器都顯示 i 為 5!為避免這種情況,迴圈內建立變數的副本:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // 本地副本
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"點擊了按鈕 #{buttonIndex}");
};
}
現在結果就跟預期一樣了。
lambda 處理器的實際威力
這類 lambda 表達式能節省時間並加快開發速度,特別是當邏輯簡單時。它們讓程式碼「更貼近任務」,不會把邏輯散落在不同檔案和類別中。在真實專案裡,你會在很多地方見到它們:從 UI 事件處理到在非同步事件匯流排中訂閱訊息。
5. 常見錯誤
錯誤 #1:忘記對長壽命物件取消訂閱(-=)。
如果訂閱者沒有從發佈者那邊取消訂閱,delegate 的引用會讓訂閱者無法被垃圾回收 —— 即使沒有其他引用。結果會導致記憶體洩漏和懸掛的依賴,特別是當發佈者存活很久(靜態事件、singleton、服務)時。
如何避免:在釋放/銷毀時總是取消訂閱(例如在 Dispose、OnDisable、OnDestroy 中)。考慮使用弱引用(weak events / WeakEventManager)、事件管理器模式或 IObservable/Rx,當情境比較複雜時。
錯誤 #2:在迴圈中捕獲變數卻不建立本地副本。
典型陷阱是把訂閱寫在迴圈裡,結果閉包捕獲了同一個迴圈變數,導致所有處理器看到的是該變數的最終值,而不是各自創建時的值。這會產生意外結果(所有處理器輸出相同的數字等)。
如何避免:在迴圈內建立本地副本並捕獲它:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
或者把需要的值傳給一個包裝方法。這樣更可靠,也讓意圖清楚。
錯誤 #3:在訂閱處放置過於龐大的 lambda。
如果你把大型業務邏輯直接放在匿名函式裡,程式碼會難以閱讀、測試和除錯。而且用匿名 lambda 會讓取消訂閱變得麻煩,因為需要同一個 delegate 實例。結果會讓邏輯散落、結構變差。
如何避免:將複雜邏輯抽成命名方法或服務;如有需要,把 delegate 存在變數/欄位中以便取消訂閱;在 lambda 中只保留小的包裝/轉發邏輯。這會提高程式碼的可讀性與可維護性。
GO TO FULL VERSION