Lambda 表達式、代理與事件

Tip

剛開始開發軟體嗎? 先從 入門 教學開始。 在使用 lambda 表達式之前,先在那裡建立核心的類型和方法技能。

有時候你會想把一小段行為、一個函式直接傳給另一個方法。 例如,你可能想篩選一個清單,但過濾條件會根據情況而改變。 你不是為每個可能的條件寫一個獨立命名的方法,而是把條件本身當作參數傳遞。

Lambda 運算式 是 C# 功能,使此成為可能。 Lambda 表達式是一個緊湊的內聯函式,你可以寫出它,但不會給它命名。 你可以使用箭頭運算 => 子將參數清單與主體分開:

x => x * 2

從左到右讀:x 是輸入參數,=> 表示 "映射到",x * 2 是主體。 它會計算回傳的值。 當沒有參數或多於一個時,用括號 () => 42 包覆:或 (left, right) => left + right。

代理支援 lambda 表達式

要使用 lambda 運算式,C# 編譯器需要知道兩件事:參數的類型和回傳類型。 這種描述,也就是參數類型加上回傳類型,稱為 代理型別。

代理型別是用來表示方法簽章的一種型別。 代理型態的變數可以容納任何匹配方法,如 lambda 運算式或命名方法,只要其參數型別與回傳型別相符即可。

你宣告一個代理類型,關鍵字為 delegate :

delegate int Transform(int value);

此聲明說:「Transform 是接受 1 int 並回傳 的方法 int的代理類型。」接著你可以為該類型的變數指派 lambda 運算式或命名方法:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

兩者doubler和squarer都具有型別Transform的值。 你把它們叫做跟一般方法一樣。 編譯器會驗證你指派的檔案是否與宣告的簽章相符。

內建代理類型: Func 以及 Action

為每一種情況宣告自訂委派類型可能顯得重複。 .NET 提供兩類通用代理類型,分別是 Func 和 Action,涵蓋大多數情境,因此你很少需要自己使用 delegate 關鍵字。

這兩個家族都有 0 到 16 個輸入參數的版本,因此可以擴展到任意數量的輸入。 這兩個家族的主要差異在於:

  • System.Func<T,TResult> (以及 Func<T1, T2, TResult>,依此類推)代表一個方法,該方法回傳一個值。 最後一個型態參數總是返回型態;所有早期的都是輸入型態。
  • System.Action<T> (和 Action<T1, T2>,依此類推)代表 一個不回傳任何東西 的方法(void)。 所有型別參數皆為輸入型別。 System.Action 若無型別參數,則表示一個沒有輸入且沒有回傳值的方法。

例如,Func<int, int, int> 描述一種具有兩個 int 輸入和一個 int 結果的方法。 Action<string> 描述一個帶有 string 輸入且無回傳值的方法。

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

在 lambda 中使用描述性參數名稱,讓讀者在不掃描整個方法主體的情況下,也能理解意圖。

將 lambda 表達式傳遞給一個方法

當方法宣告 Func 或 Action 參數時,呼叫者會傳遞一個與該委派類型相符的 lambda 表達式。 編譯器會檢查 lambda 的參數型別與回傳型別是否符合宣告的代理型別。 如果不匹配,程式碼就無法編譯。

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

該 Filter 方法宣告一個 Func<int, bool> 名為 predicate的參數。 該 Func<int, bool> 型別告訴呼叫者預期的形狀:一個 int 輸入,一個 bool 結果。 呼叫方將 value => value % 2 == 0 作為該參數傳遞。 此模式在 LINQ 及許多 .NET API 中普遍存在。

保持 lambda 表達式自包含

lambda 運算式可以引用周圍程式碼中的變數。 擷取 是指 lambda 持有對自身主體外宣告的變數的參考。 lambda 與其所捕捉變數的組合稱為 閉包。

當你不需要捕捉任何東西時,就把修正值加 static 到lambda上。 靜態 lambda 只能使用其內部宣告的參數與值。 它無法從包圍的範圍內擷取本地變數或實例狀態。

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

靜態 lambda 表達式能明確表達意圖,防止意外捕獲。

當輸入無關時,使用丟棄參數

有時候代理簽名會包含你不需要的參數。 用棄牌 _ 明確表示這個選擇。

常見例子包括事件處理程序,不用 sender 或 EventArgs;回調,只需部分輸入;還有 LINQ 過載,提供你不用的索引。

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

丟棄能提升可讀性,因為它們顯示哪些參數重要。

活動提供可選通知

事件是一種機制,一個物件(發布者)用來通知其他物件(訂閱者)當發生某些事情時。 出版商不需要知道誰在聽或有多少訂閱者。 訂閱者可選擇加入。

活動建立在代表之上。 事件是一個委派欄位,並由關鍵字event 強制執行額外限制:外部程式碼只能訂閱(+=)或取消訂閱(-=)事件。只有宣告事件的類別才能引發該事件。

活動代理類型的.NET慣例是 System.EventHandler<TEventArgs>,其中 T 是通知中包含的資料類型。 其簽名總是包含兩個參數: sender (引發事件的物件)以及型別 T為 的事件資料。

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

逐步解析程式碼:

  • MessagePublisher宣稱event EventHandler<string>? MessagePublished. event 關鍵字表示使用者無法直接調用,只能訂閱或取消訂閱。
  • publisher.MessagePublished += (_, message) => ... 以 lambda 表達式訂閱。 _ 會捨棄 sender 參數,因為這個處理器不需要它。
  • publisher.Publish("Records updated") 會引發事件並執行所有訂閱的事件處理函式。

訂閱為自願。 在?.Invoke(...)方法中,Publish事件只有在至少有一名訂閱者連接時才會觸發。 出版商在不知情或在意是否有人在聽的情況下提出這個事件。

參見