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# 中的 LINQ 時,也可以使用 Lambda 運算式,如下列範例所示:

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」。 運算式 Lambda 會傳回運算式的結果,並採用下列基本形式:

(input-parameters) => expression

運算式 Lambda 的主體可以包含方法呼叫。 不過,如果您要建立在 .NET 通用語言執行平台 (CLR) 外部 (例如在 SQL Server 中) 評估的運算式樹狀架構,則不應在 Lambda 運算式中使用方法呼叫。 這些方法在 .NET 通用語言執行平台 (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 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);

或者,您可以使用引射類型變數搭配 var 宣告來定義委派類型。 編譯會合成正確的委派型別。

如需詳細資訊,請參閱 Lambda 運算式上預設參數的功能規格。

非同步 Lambda

您可以使用 asyncawait 關鍵字,輕鬆建立結合非同步處理的 Lambda 運算式和陳述式。 例如,下列 Windows Form 範例包含呼叫並等候非同步方法 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 加入相同的事件處理常式。 若要加入這個處理常式,請將 async 修飾詞加入至 Lambda 參數清單前面,如下列範例所示:

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# 7.0 開始,C# 語言提供內建的元組支援。 您可以將元組當做引數提供給 Lambda 運算式,而您的 Lambda 運算式也可以傳回元組。 在某些情況下,C# 編譯器會使用型別推斷來判斷元組元件的類型。

若要定義元組,請以括號括住其元件的逗號分隔清單。 下列範例使用具有 3 個元件的元組將一連串數字傳遞至 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)

通常,元組的欄位會命名為 Item1Item2,依此類推。 不過,您可以定義具有具名元件的元組,如下列範例所示。

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> 定義具有兩個輸入參數 (intstring) 的委派與 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)}");

編譯器會推斷輸入參數的類型,您也可以明確指定類型。 這個特殊 Lambda 運算式會計算除以二之後餘數為 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 運算式中的型別推斷

撰寫 Lambda 時,您通常不需要指定輸入參數的類型,這是因為編譯器可以根據 Lambda 主體、參數類型,以及 C# 語言規格中所述的其他因素來推斷類型。 對於大多數的標準查詢運算子而言,第一項輸入是來源序列中項目的類型。 如果您要查詢 IEnumerable<Customer>,則輸入變數就會推斷為 Customer 物件,這表示您可以存取其方法和屬性:

customers.Where(c => c.City == "London");

Lambda 型別推斷的一般規則如下所示:

  • Lambda 必須包含與委派類型相同數目的參數。
  • Lambda 中的每個輸入參數都必須能夠隱含轉換為其對應的委派參數。
  • Lambda 的傳回值 (如果有的話) 必須能夠隱含轉換為委派的傳回類型。

Lambda 運算式的自然類型

Lambda 運算式本身並沒有型別,因為一般型別系統沒有內建的「Lambda 運算式」概念。不過,有時候一般所稱的 Lambda 運算式「類型」會很實用。 這種非正式的「類型」是指委派類型或是 Lambda 運算式轉換成的 Expression 類型。

從 C# 10 開始,Lambda 運算式可能有自然類型。 編譯器可能會從 Lambda 運算式推斷委派類型,而不是強制宣告委派類型,例如針對 Lambda 運算式的 Func<...>Action<...>。 例如,請參考下列宣告:

var parse = (string s) => int.Parse(s);

編譯器可以推斷 parseFunc<string, int>。 如果有適當項目存在,編譯器會選擇可用的 FuncAction 委派。 否則會合成委派類型。 例如,如果 Lambda 運算式具有 ref 參數,則會合成委派類型。 當 Lambda 運算式具有自然類型時,可以將指派給較不明確的類型,例如 System.ObjectSystem.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

從 C# 10 開始,您可以在輸入參數前指定 Lambda 運算式的傳回類型。 當您指定明確的傳回型別時,您必須為輸入參數加上括弧:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

屬性

從 C# 10 開始,您可以將屬性新增至 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 運算式無法直接從封入方法擷取 inrefout 參數。
  • Lambda 運算式中的 return 陳述式不會造成封入方法傳回。
  • 如果該跳躍陳述式的目標位於 Lambda 運算式區塊之外,則 Lambda 運算式不能包含 gotobreakcontinue 陳述式。 即使目標位於區塊內,跳躍陳述式出現在 Lambda 運算式區塊外部也一樣是錯誤。

您可以將 static 修飾詞套用至 Lambda 運算式,以防止 Lambda 意外擷取區域變數或執行個體狀態:

Func<double, double> square = static x => x * x;

靜態 Lambda 無法從封閉範圍擷取區域變數或執行個體狀態,但可能會參考靜態成員和常數定義。

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格匿名函式運算式一節。

如需這些功能的詳細資訊,請參閱下列功能提案筆記:

另請參閱