在前一天學習 LINQ 時,其實可能會注意到有兩種寫法:
可以使用 Lambda 運算式 來建立匿名函式,Lambda 使用運算子 =>,用來分隔參數清單與主體 (body)。Lambda 運算式可以有兩種形式:
(input-parameters) => expression
(input-parameters) => { <sequence-of-statements> }
要建立一個 Lambda 運算式:在 => 左邊指定輸入參數(若有的話)、在右邊則是運算式或陳述式區塊。
任何 Lambda 運算式都可以被轉換為 委派型別 (delegate type),Lambda 的參數型別與回傳值型別,會決定它可以轉換為哪種委派:
Action<T1,T2>
代表有兩個參數且沒有回傳值。Func<T, TResult>
代表有一個參數且有回傳值。例如:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// 輸出:25
Lambda 也可以被轉換為 運算式樹 (Expression Tree):
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// 輸出:x => (x * x)
Lambda 運算式可以用在任何需要委派實例或運算式樹的地方,例如:背景工作:當作 Task.Run(Action) 的參數,將要在背景執行的程式碼傳入。LINQ:當作 LINQ 查詢的參數,來進行資料篩選或轉換。
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// 輸出:4 9 16 25
在 LINQ to Objects 或 LINQ to XML 中,呼叫 Enumerable.Select 方法時,參數型別是 Func<T,TResult>
。而在 LINQ to SQL 中,呼叫 Queryable.Select 方法時,參數型別則是 Expression<Func<TSource,TResult>>
。雖然底層使用的型別不同,但兩者都可以使用相同的 Lambda 運算式,因此它們的程式碼看起來非常相似。
在此段要來詳細說明上方 Lambda 的基本語法所說到的運算式Lambda以及陳述式的Lambda。
在 => 運算子的右側若是單一運算式,就稱為「運算式 Lambda」。運算式 Lambda 會回傳該運算式的結果,其基本形式為:
(input-parameters) => expression
運算式 Lambda 的主體也可以是方法呼叫。但如果你要建立由 查詢提供者 (query provider) 評估的運算式樹 (expression tree),就必須限制只能呼叫查詢提供者能夠轉換的方法。不同的查詢提供者有不同的支援能力,例如:SQL 型別的查詢提供者通常可以把 String.StartsWith
轉換成 SQL 的 LIKE 運算式。如果查詢提供者無法辨識某個方法呼叫,它就無法轉換或執行該運算式。
陳述式 Lambda 與運算式 Lambda 類似,不同之處在於主體用大括號 { } 包住多個陳述式:
(input-parameters) => { <sequence-of-statements> }
陳述式 Lambda 的主體可以包含多個陳述式,但實務上通常不會超過兩三個。
範例:
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// 輸出:Hello World!
⚠️ 陳述式 Lambda 無法用來建立運算式樹。
Action line = () => Console.WriteLine();
Func<double, double> cube = x => x * x * x;
Func<int, int, bool> testForEquality = (x, y) => x == y;
而編譯器通常會自動推斷參數型別 (隱含型別參數清單),也可以明確指定型別:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
可以用丟棄符號 _ 來忽略 Lambda 中未使用的參數:
Func<int, int, int> constant = (_, _) => 42;
這在事件處理器中很常見,但如果 Lambda 只有一個參數名為 _,它會被當作真正的參數名稱,而不是丟棄符號。
從 C# 12 開始,Lambda 可以定義預設值,語法與方法或區域函式相同:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
Lambda 也可以使用 params 陣列或集合:
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
此外,若一個方法群組有預設參數或 params,指派給 Lambda 時也會保留這些參數特性。但這類 Lambda 不會自然對應到 Func<>
或 Action<>
型別,需要自訂委派型別:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
或者直接用 var 讓編譯器自動合成正確的委派型別:
var add = (int x, int y = 10) => x + y;
Console.WriteLine(add(5)); // 15
Console.WriteLine(add(5, 2)); // 7
可以使用 async 和 await 關鍵字輕鬆建立包含非同步處理的 lambda 表達式和語句。例如下方程式碼範例, Windows 窗體範例包含一個呼叫並等待非同步方法 ExampleMethodAsync 的事件處理程序:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
可以使用非同步 lambda 新增相同的事件處理程序,若要新增此處理程序,請在 lambda 參數清單前新增 async 修飾符,如下例所示:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
C# 語言內建了對元組的支援,可以將元組作為參數提供給 lambda 表達式,並且 lambda 表達式也可以傳回元組。在某些情況下,C# 編譯器會使用型別推論來決定元組元素的型別。
定義元組時,需用括號括起元組的各組成部分,並以逗號分隔,以下範例使用包含三個組成部分的元組,將一個數字序列傳遞給 lambda 表達式,該表達式將每個值加倍,並傳回一個包含三個組成部分的元組,其中包含乘法的結果。
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
通常,元組的欄位名稱為 Item1、Item2,依此類推。不過,你也可以定義一個包含命名元件的元組,如下例所示:
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
LINQ 的許多方法(Where、Select、Count…)都需要 Func
或 Expression<Func>
委派作為參數。
可以先看以下例子,此案例可以看到 Func<int, bool>
的用法,其中 int 是輸入參數,bool 是回傳值。返回值始終在最後一個類型參數中指定。例如,Func<int, string, bool>
定義一個委託,它有兩個輸入參數(int 和 string),以及一個傳回型別 bool。以下 Func 委託在呼叫時傳回布林值,該值指示輸入參數是否等於 5:
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
當參數類型為 Expression<TDelegate>
時,您也可以提供 lambda 表達式,例如 Queryable 類型中定義的標準查詢運算子中。當您指定 Expression<TDelegate>
參數時,lambda 會被編譯為表達式樹。 以下範例使用了 Count 標準查詢運算子:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
Output:
There are 5 odd numbers in 5 4 1 3 9 8 6 7 2 0
編譯器可以推斷輸入參數的類型,或者我們也可以明確指定,這個特定的 lambda 表達式會統計除以 2 後餘數為 1 的整數 (n)。 以下範例產生的序列包含 numbers 陣列中 9 之前的所有元素,因為 9 是序列中第一個不滿足條件的數字:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
以下範例透過將多個輸入參數括在括號中來指定它們。此方法傳回 numbers 陣列中的所有元素,直到找到一個值小於其在陣列中的序號位置的數字:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
不能在查詢表達式中直接使用 lambda 表達式,但可以在查詢表達式中的方法呼叫中使用它們,如下例所示:
var numberSets = new List<int[]>
{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};
var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;
foreach (var numberSet in setsWithManyPositives)
{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0
今天的學習主題Lambda 表達式與匿名函數內容稍微長了些,今天進度先到這邊,明天接續還沒結束的內容,參考資料在此,若順利完成進度,會進到主題:委派與事件。