Lambda 運算式 (C# 參考)

您可以使用 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 的參數,並傳回 squared 的值 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 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 編譯器錯誤。

從 C# 9.0 開始,您可以使用 discards 來指定運算式中未使用之 Lambda 運算式的兩個或多個輸入參數:

Func<int, int, int> constant = (_, _) => 42;

當您使用 Lambda 運算式來 提供事件處理常式時,Lambda 捨棄參數可能會很有用。

注意

為了回溯相容性,如果只有單一輸入參數命名 _ 為 ,則 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# 語言提供 Tuple的內建支援。 您可以將元組當做引數提供給 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)

一般而言,Tuple 的欄位會命名 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}");

如需 C# Tuple 的詳細資訊,請參閱 Tuple 類型

具有標準查詢運算子的 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 運算式推斷委派類型,而不是強制宣告委派類型,例如 Func<...>Action<...> 針對 Lambda 運算式。 例如,請參考下列宣告:

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 = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";

您也可以將屬性新增至輸入參數或傳回值,如下列範例所示:

var sum = ([Example(1)] int a, [Example(2), Example(3)] int b) => a + b;
var inc = [return: Example(1)] (int s) => s++;

如上述範例所示,當您將屬性新增至 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 陳述式不會造成封入方法傳回。
  • 如果 Jump 語句的目標不在 Lambda 運算式區塊之外,Lambda 運算式就無法包含 gotobreakcontinue 語句。 即使目標位於區塊內,跳躍陳述式出現在 Lambda 運算式區塊外部也一樣是錯誤。

從 C# 9.0 開始,您可以將 修飾詞套用 static 至 Lambda 運算式,以防止 Lambda 意外擷取區域變數或實例狀態:

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

靜態 Lambda 無法從封入範圍擷取區域變數或實例狀態,但可能會參考靜態成員和常數定義。

C# 語言規格

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

如需 C# 9.0 和更新版本中新增功能的詳細資訊,請參閱下列功能提案附注:

另請參閱