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事件只有在至少有一名訂閱者連接時才會觸發。 出版商在不知情或在意是否有人在聽的情況下提出這個事件。