Lambda 運算式和匿名函式
您可以使用 Lambda 運算式 來建立匿名函式。 使用 lambda 宣告運算子 =>
,將 Lambda 的參數列表與其主體分開。 Lambda 表達式可以是下列兩種形式之一:
表示式 lambda,其表達式為主體:
(input-parameters) => expression
Lambda 表達式,它包含一個語句區塊作為其主體:
(input-parameters) => { <sequence-of-statements> }
若要建立 Lambda 運算式,您需要在 Lambda 運算子左邊指定(若有)輸入參數,右邊則是運算式或語句區塊。
任何 Lambda 運算式都可以轉換成 委派 類型。 其參數的類型和傳回值會定義 Lambda 運算式可以轉換的委派類型。 如果 Lambda 運算式未傳回值,則可以轉換成其中一個 Action
委派類型;否則,它可以轉換成其中一個 Func
委派類型。 例如,具有兩個參數且不傳回任何值的 lambda 表達式可以轉換成 Action<T1,T2> 委派。 具有一個參數並傳回值的 Lambda 運算式,可以轉換成 Func<T,TResult> 委派。 在下列範例中,lambda 運算式 x => x * x
,它指定名為 x
的參數,且傳回 x
平方的值,被指派給一個委派類型的變數:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Lambda 表達式也可以轉換成 表示式樹 類型,如下列範例所示:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
您可以在任何需要委派類型或表達式樹實例的程式碼中使用 Lambda 運算式。 其中一個範例是 Task.Run(Action) 方法的自變數,用來傳遞應該在背景中執行的程序代碼。 當您在 C#中撰寫
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
當您使用方法型語法在 System.Linq.Enumerable 類別中呼叫 Enumerable.Select 方法時,例如在 LINQ to Objects 和 LINQ to XML 中,參數是委派類型 System.Func<T,TResult>。 當您在 System.Linq.Queryable 類別中呼叫 Queryable.Select 方法時,例如在 LINQ to SQL 中,參數類型是表示式樹狀結構類型 Expression<Func<TSource,TResult>>
。 在這兩種情況下,您可以使用相同的 Lambda 運算式來指定參數值。 這會使兩個 Select
呼叫顯得相似,但事實上,由拉姆達創建的物件類型是不同的。
表達式 Lambda
在 =>
運算子右側具有表達式的 Lambda 表示式稱為 表示式 lambda。 表達式 Lambda 會傳回表達式的結果,並採用下列基本格式:
(input-parameters) => expression
表達式 Lambda 的主體可以包含方法呼叫。 不過,如果您要建立 表達式樹狀結構, 在 .NET Common Language Runtime (CLR) 的內容之外進行評估,例如在 SQL Server 中,您不應該在 Lambda 表達式中使用方法呼叫。 方法在 .NET Common Language Runtime (CLR) 的內容之外沒有意義。
語句函數式lambda
語句 Lambda 類似於表達式 Lambda,不同之處在於其語句會以大括弧括住:
(input-parameters) => { <sequence-of-statements> }
語句 Lambda 的主體可以包含任意數目的語句;不過,實際上通常不超過兩到三個。
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
您無法使用語句 Lambda 來建立運算式樹狀架構。
Lambda 表達式的輸入參數
您會以括弧括住 Lambda 表達式的輸入參數。 使用空括號指定零輸入參數:
Action line = () => Console.WriteLine();
如果 Lambda 運算式只有一個輸入參數,括弧是選擇性的:
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;
輸入參數類型必須是所有明確或全部隱含的;否則,會發生 CS0748 編譯程序錯誤。
您可以使用 捨棄 來指定表示式中未使用之 Lambda 表達式的兩個或多個輸入參數:
Func<int, int, int> constant = (_, _) => 42;
當您使用 Lambda 表達式來 提供事件處理程式時,Lambda 捨棄參數會很有用。
注意
為了回溯相容性,如果只有單一輸入參數命名為 _
,則在 Lambda 運算式內,_
會被視為該參數的名稱。
從 C# 12 開始,您可以針對 Lambda 運算式上的參數提供 預設值。 預設參數值的語法和限制與方法和區域函式相同。 下列範例會宣告具有預設參數的 Lambda 運算式,接著使用預設值呼叫它一次,並使用兩個明確參數呼叫一次:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
您也可以將具有 params
陣列或集合的 Lambda 運算式宣告為參數:
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
在這些更新中,當具有預設參數的方法群組指派給 Lambda 運算式時,該 Lambda 運算式也有相同的預設參數。 具有 params
集合參數的方法群組也可以指派給 Lambda 運算式。
具有預設參數或以 params
集合作為參數的 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
宣告來定義委派類型。 編譯程式會合成正確的委派類型。
如需 Lambda 運算式上預設參數的詳細資訊,請參閱 lambda 運算式上 預設參數的功能規格。
異步 Lambda
您可以使用 非同步 和 等待 關鍵詞,輕鬆建立包含非同步處理的 Lambda 表達式及語句。 例如,下列 Windows Forms 範例包含呼叫和等候異步方法的事件處理程式,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);
}
}
如需有關建立和使用異步方法的更多資訊,請參閱使用 async 和 await 的異步程式設計 。
Lambda 運算式和元組
C# 語言提供內建支援 元組。 您可以提供 Tuple 做為 Lambda 表達式的自變數,而您的 Lambda 運算式也可以傳回 Tuple。 在某些情況下,C# 編譯程式會使用類型推斷來判斷 Tuple 元件的類型。
您可以用括號括住一個以逗號分隔的元件列表來定義元組。 下列範例使用具有三個元組的 Tuple,將數字序列傳遞至 lambda 運算式,該運算式會將每個值加倍,然後傳回一個包含乘法結果的三個元組的 Tuple。
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
等等。 不過,您可以定義具有具名元件的 Tuple,如下列範例所示。
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}");
如需 C# 元組的詳細資訊,請參閱 元組類型。
具有標準查詢運算子的 Lambda
LINQ to Objects 以及其他實作中,有一個輸入參數,其類型是泛型委派 Func<TResult> 系列之一。 這些委派會使用類型參數來定義輸入參數的數目和類型,以及委派的傳回類型。
Func
委派適用於封裝套用至一組源數據中每個元素的使用者定義表達式。 例如,請考慮 Func<T,TResult> 委派類型:
public delegate TResult Func<in T, out TResult>(T arg)
委派可以具現化為 Func<int, bool>
實例,其中 int
是輸入參數,bool
是傳回值。 傳回值一律會在最後一個類型參數中指定。 例如,Func<int, string, bool>
定義了一個委派,該委派具有兩個輸入參數:int
和 string
,並且具有返回類型 bool
。 當下列 Func
委派被叫用時,它會傳回一個布爾值,表示輸入參數是否等於五。
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)}");
編譯程式可以推斷輸入參數的類型,也可以明確指定它。 這個特定的 Lambda 表達式會計算這些整數(n
),當除以 2 時的餘數為 1。
下列範例會產生一個序列,其中包含位於9之前之 numbers
陣列中的所有元素,因為這是序列中不符合條件的第一個數位:
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 運算式中的類型推斷
撰寫 Lambda 時,您通常不需要指定輸入參數的類型,因為編譯程式可以根據 Lambda 主體、參數類型及 C# 語言規格中所述的其他因素來推斷類型。 對於大部分的標準查詢運算符,第一個輸入是來源序列中的元素類型。 如果您要查詢 IEnumerable<Customer>
,則輸入變數會推斷為 Customer
物件,這表示您可以存取其方法和屬性:
customers.Where(c => c.City == "London");
Lambda 類型推斷的一般規則如下所示:
- Lambda 必須包含與委派類型相同的參數數目。
- Lambda 中的每個輸入參數都必須隱含轉換成其對應的委派參數。
- Lambda 的傳回值(如果有的話)必須隱式轉換成委派的傳回型別。
Lambda 表達式的自然類型
Lambda 運算式本身沒有類型,因為通用類型系統沒有“Lambda 運算式”的內建概念。不過,有時方便非正式地說出 Lambda 表達式的「類型」。 該非正式的「類型」是指將 Lambda 運算式轉換成的委派類型或 Expression 類型。
Lambda 表達式可以有 原生類型。 編譯程式無法強制宣告委派類型,例如 lambda 表達式的 Func<...>
或 Action<...>
,而是可以從 Lambda 表達式推斷委派類型。 例如,請考慮下列宣告:
var parse = (string s) => int.Parse(s);
編譯程式可以推斷 parse
為 Func<string, int>
。 如果適當的委派存在,編譯程式會選擇可用的 Func
或 Action
委派。 否則,它會生成一個委派類型。 例如,如果 lambda 運算式具有 ref
個參數,則會合成委派類型。 當 Lambda 運算式具有自然類型時,可以將它指派給較不明確的類型,例如 System.Object 或 System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
方法群組(也就是沒有參數清單的方法名稱),只有一個多載具有自然類型:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
如果您將 Lambda 表達式指派給 System.Linq.Expressions.LambdaExpression或 System.Linq.Expressions.Expression,而 Lambda 具有自然委派類型,則表達式具有自然類型的 System.Linq.Expressions.Expression<TDelegate>,且使用自然委派類型做為類型參數的自變數:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
並非所有 Lambda 運算式都有自然類型。 請考慮下列宣告:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
編譯程式無法推斷 s
的參數類型。 當編譯程式無法推斷自然類型時,您必須宣告類型:
Func<string, int> parse = s => int.Parse(s);
明確傳回類型
一般而言,Lambda 表達式的傳回類型是顯而易見且推斷的。 對於某些無法運作的表示式:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
您可以在輸入參數之前指定 Lambda 表達式的傳回類型。 當您指定明確的傳回型別時,您必須將輸入參數加上括號:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
屬性
您可以將屬性新增至 Lambda 運算式及其參數。 下列範例示範如何將屬性新增至 Lambda 運算式:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
您也可以將屬性新增至輸入參數或傳回值,如下列範例所示:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
如上述範例所示,當您將屬性加入 Lambda 表達式或其參數時,您必須將輸入參數加上括號。
重要
Lambda 運算式是透過基礎委派類型叫用。 這與方法和區域函式不同。 委派的 Invoke
方法不會檢查 Lambda 運算式上的屬性。 叫用 Lambda 運算式時,屬性沒有任何作用。 Lambda 運算式上的屬性對於程式碼分析很有用,而且可以透過反射來探索。 此決策的其中一個後果是,System.Diagnostics.ConditionalAttribute 無法套用至 Lambda 表達式。
在 Lambda 運算式中擷取外部變數和變數範圍
Lambda 可以參考 外部變數。 這些 外部變數 是定義 Lambda 表達式之方法範圍中的變數,或包含在包含 Lambda 運算式之型別的範圍內。 以這種方式捕捉的變數,即使在通常情況下會超出範圍並被垃圾回收,仍會儲存起來供 lambda 運算式使用。 在 Lambda 運算式中取用外部變數之前,必須明確地指定外部變數。 下列範例示範這些規則:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
下列規則適用於 Lambda 運算式中的變數範圍:
- 擷取的變數將不會進行垃圾收集,直到參考該變數的委派才有資格進行垃圾收集。
- 在 Lambda 運算式中引進的變數不會顯示在封入方法中。
- Lambda 運算式無法直接從封入方法擷取、
ref 或參數中的 。 - lambda 表達式中的 傳回 語句不會造成封入方法傳回。
- Lambda 表達式不能包含 跳轉、中斷或 繼續 語句,除非這些跳躍語句的目標位於 Lambda 表達式區塊之外。 如果目標位於 區塊內,在 Lambda 運算式區塊之外建立 jump 語句也是錯誤。
您可以將 static
修飾詞套用至 Lambda 表達式,以防止 Lambda 意外擷取局部變數或實例狀態:
Func<double, double> square = static x => x * x;
靜態 Lambda 無法從封入範圍擷取局部變數或實例狀態,但可以參考靜態成員和常數定義。
C# 語言規格
如需詳細資訊,請參閱
如需這些功能的詳細資訊,請參閱下列功能提案附註: