共用方式為


Lambda 運算式和匿名函式

使用 lambda 表達 式來建立一個匿名函式。 使用 lambda 宣告運算子 =>,將 Lambda 的參數列表與其主體分開。

C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。

文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。

小提示

欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。

lambda 表達式可以是以下兩種形式之一:

  • 表達式 lambda,其主體為一個表達式:

    (input-parameters) => expression
    
  • Lambda 表達式,它包含一個語句區塊作為其主體:

    (input-parameters) => { <sequence-of-statements> }
    

要建立 lambda 運算式,請在 lambda 運算子的左側指定輸入參數(若有的話),另一側則指定一個表達式或語句區塊。

你可以將任何 lambda 運算式轉換成 代理 型態。 其參數的類型和傳回值會定義 Lambda 運算式可以轉換的委派類型。 如果 lambda 運算式沒有回傳某個值,就把它轉換成代理 Action 型別之一。 如果回傳一個值,就將其轉換成其中一個 Func 代理類型。 例如,轉換一個包含兩個參數且不返回值給 Action<T1,T2> 代理的 lambda 表達式。 轉換一個只有一個參數的 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

當您使用方法型語法在 Enumerable.Select 類別中呼叫 System.Linq.Enumerable 方法時,例如在 LINQ to Objects 和 LINQ to XML 中,參數是委派類型 System.Func<T,TResult>。 當您在 Queryable.Select 類別中呼叫 System.Linq.Queryable 方法時,例如在 LINQ to SQL 中,參數類型是表示式樹狀結構類型 Expression<Func<TSource,TResult>>。 在這兩種情況下,您可以使用相同的 Lambda 運算式來指定參數值。 這會使兩個 Select 呼叫顯得相似,但事實上,由拉姆達創建的物件類型是不同的。

Lambda 表達式

具有運算子 => 右側表達式的 Lambda 運算式稱為 運算式 Lambda。 表達式 Lambda 會傳回表達式的結果,並採用下列基本格式:

(input-parameters) => expression

表達式 Lambda 的主體可以包含方法呼叫。 然而,當你建立由查詢提供者評估的 表達式樹 時,限制方法呼叫範圍為查詢提供者能轉換成其格式的方法。 不同的查詢服務提供者有不同的功能。 例如,許多基於 SQL 的提供者可以將 這 String.StartsWith 類方法轉換成適當的 SQL 表達式,例如 LIKE。 如果查詢提供者無法辨識方法呼叫,則無法轉譯或執行表達式。

語句函數式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;

編譯器通常會將參數的型別推導到 lambda 表達式,這稱為 隱式型別的參數列表。 你可以明確指定型別,這稱為 明確型別的參數列表。 以下範例展示了一個明確型別的參數列表:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

輸入參數類型必須全部為明確或全隱含。 否則, CS0748 編譯器會發生錯誤。 在 C# 14 之前,如果參數有任何修飾詞,例如 refout,您必須在 參數中包含明確類型。 在 C# 14 中,已移除該限制。 不過,如果您使用 params 修飾詞,您仍必須宣告類型。

使用 棄置 來指定兩個或以上未用於表達式的 lambda 表達式輸入參數:

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

當您使用 Lambda 表達式來 提供事件處理程式時,Lambda 捨棄參數會很有用。

註記

為了向下相容,若僅命名 _一個輸入參數,編譯器會 _ 將該參數視為該 lambda 運算式中該參數的名稱。

從 C# 12 開始,你可以為明確型別的參數列表提供 預設值 。 預設參數值的語法和限制與方法和區域函式相同。 以下範例宣告一個帶有預設參數的 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,並且具有返回類型 boolFunc以下代理在呼叫時會回傳一個布林值,指示輸入參數是否等於五:

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 表達式可以有 原生類型。 編譯器不需要你宣告代理型別, Func<...> 例如對 lambda Action<...> 表達式,而是從 lambda 表達式推斷代理型別。 例如,請考慮下列宣告:

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

編譯器推 parse 斷為 Func<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 語句也是錯誤。

為防止 lambda 無意中捕捉局部變數或實例狀態,請對 lambda 表達式施加修 static 飾符:

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

靜態 lambda 無法從包圍的範圍捕捉本地變數或實例狀態,但它可以參考靜態成員和常數定義。

C# 語言規格

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

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

另請參閱