共用方式為


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 會傳回表達式的結果,並採用下列基本格式:

(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)

一般而言,元組的欄位會命名為 Item1Item2等等。 不過,您可以定義具有具名元件的 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> 定義了一個委派,該委派具有兩個輸入參數:intstring,並且具有返回類型 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);

編譯程式可以推斷 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.LambdaExpressionSystem.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# 語言規格

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

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

另請參閱