Lambda 運算式 (C# 程式設計手冊)

Lambda 運算式是可用來建立委派或運算式樹狀架構型別的匿名函式。 使用 Lambda 運算式,您可以撰寫可當做引數或傳回做為函式呼叫之值的區域函式。 Lambda 運算式為撰寫 LINQ 查詢運算式中特別有用。

若要建立 Lambda 運算式,您在 Lambda 運算子 =>指定輸入參數 (如果有的話),因此,您在另一端將運算式或陳述式區塊放。 例如, Lambda 運算式 x => x * x 指定名為 x 的參數和傳回值的 x 平方了。 如下列範例所示,您可以指派運算式給委派型別:

delegate int del(int i);
static void Main(string[] args)
{
    del myDelegate = x => x * x;
    int j = myDelegate(5); //j = 25
}

若要建立運算式樹狀架構型別:

using System.Linq.Expressions;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Expression<del> myET = x => x * x;
        }
    }
}

=> 運算子具有與指派運算子 (=) 相同的優先順序,而且是右向關聯的。

在方法架構 LINQ 查詢中,Lambda 會用來做為標準查詢運算子方法的引數,例如 Where

當您使用方法架構語法呼叫 Enumerable 類別中的 Where 方法時 (就像是在 LINQ to Objects 和 LINQ to XML 中),此參數就會是委派型別 System.Func<T, TResult>。 Lambda 運算式是建立委派的最便利方式。 例如,當您在 System.Linq.Queryable 類別中呼叫相同方法時 (就像是在 LINQ to SQL 中的方式),參數型別就會是 System.Linq.Expressions.Expression<Func>,其中 Func 是具有多達十六個輸入參數的任何 Func 委派。 此外,Lambda 運算式只是建構該運算式樹狀架構的極致簡潔方式。 Lambda 會使得 Where 呼叫看起來相似,但是實際上從 Lambda 建立的物件型別並不相同。

在上一個範例中,請注意委派簽章具有一個型別為 int 的隱含型別輸入參數,而且會傳回 int。 因為 Lambda 運算式也有一個輸入參數 (x),以及可由編譯器 (Compiler) 隱含轉換為 int 型別的傳回值,所以 Lambda 運算式可以轉換為該型別的委派 (型別推斷將於下列各節中詳細討論)。當使用輸入參數 5 叫用 (Invoke) 委派時,便會傳回 25 的結果。

Lambda 不允許出現在 isas 運算子的左邊。

所有適用於匿名方法的限制,也都適用於 Lambda 運算式。 如需詳細資訊,請參閱匿名方法 (C# 程式設計手冊)

運算式 Lambda

左邊具有運算式的 Lambda 運算式稱為「運算式 Lambda」(Expression Lambda)。 運算式 Lambda 會在運算式樹狀架構 (C# 和 Visual Basic)的建構過程中大量使用。 運算式 Lambda 會傳回運算式的結果並採用下列基本形式:

(input parameters) => expression

當 Lambda 具有一個輸入參數時,括號才會是選擇性的,否則括號會是必要項目。 兩個或更多個輸入參數則會由包括在括號中的逗號分隔:

(x, y) => x == y

有時候編譯器會很難或是無法推斷輸入型別。 當這種情形發生時,您就可以明確指定型別,如下列範例所示:

(int x, string s) => s.Length > x

以空括號指定零個輸入參數:

() => SomeMethod()

請注意,在上述範例中,運算式 Lambda 的主體可以包含一個方法呼叫。 然而,如果要建立將在另一個網域中 (例如 SQL Server) 使用的運算式樹狀架構,您就不應該在 Lambda 運算式中使用方法呼叫。 這些方法在 .NET Common Language Runtime 內容的外部將不具任何意義。

陳述式 Lambda

除了陳述式是括在大括號內之外,陳述式 Lambda 與運算式 Lambda 非常相似:

(input parameters) => {statement;}

陳述式 Lambda 的主體可以包含任何數目的陳述式,但是實際上通常不會最多為兩個或三個陳述式。

delegate void TestDelegate(string s);
…
TestDelegate myDel = n => { string s = n + " " + "World"; Console.WriteLine(s); };
myDel("Hello");

陳述式 Lambda 就像匿名方法,它不能用來建立運算式樹狀架構。

非同步 Lambdas

您可以輕鬆地建立使用 asyncawait 關鍵字,合併非同步處理中的 Lambda 運算式和陳述式。 例如,下列範例包含 Windows Form 呼叫並等候非同步方法的事件處理常式, ExampleMethodAsync。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        // ExampleMethodAsync returns a Task.
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\r\n";
    }

    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) =>
        {
            // ExampleMethodAsync returns a Task.
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\r\n";
        };
    }

    async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

如需如何建立和使用非同步方法的詳細資訊,請參閱 使用 Async 和 Await 設計非同步程式 (C# 和 Visual Basic)

具有標準查詢運算子的 Lambda

許多標準查詢運算子,都具有型別為一種 Func<T, TResult> 系列泛型委派的輸入參數。 Func<T, TResult> 委派使用型別參數來定義輸入參數的數目和型別,以及委派的傳回型別。 對於封裝套用至一組來源資料中每個項目的使用者定義運算式,Func 委派是非常有用的。 例如,以下列委派型別為例:

public delegate TResult Func<TArg0, TResult>(TArg0 arg0)

這個委派可以具現化 (Instantiated) 為 Func<int,bool> myFunc,其中 int 是輸入參數,而 bool 是傳回值。 傳回值永遠在最後一個型別參數中指定。 Func<int, string, bool> 定義具有兩個輸入參數,int 和 string,以及 bool 之傳回型別的委派。 下列 Func 委派會在叫用時傳回 true 或 false,以表示輸入參數是否等於 5:

Func<int, bool> myFunc = x => x == 5;
bool result = myFunc(4); // returns false of course

您也可以在引數型別為 Expression<Func> 時提供 Lambda 運算式,例如已定義於 System.Linq.Queryable 中的標準查詢運算子。 當您指定 Expression<Func> 引數時,Lambda 將會編譯為運算式樹狀架構。

以下顯示一個標準查詢運算子,即 Count 方法:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);

編譯器會推斷輸入參數的型別,或者您也可以明確予以指定。 這個特定的 Lambda 運算式會計算這些在除以二時會產生餘數 1 的整數 (n)。

下列方法會使的 numbers 陣列中所有元素是左邊的一組數的序列,因為那是不符合條件的第一個數字:

var firstNumbersLessThan6 = numbers.TakeWhile(n => n < 6);

這個範例會示範如何用括號括住以指定多個輸入參數。 此方法會傳回數字陣列中的所有元素,直到遇到數值小於其位置的數字為止。 請勿混淆 Lambda 運算子 (=>) 與大於或等於運算子 (>=)。

var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);

Lambda 中的型別推斷

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

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

以下是 Lambda 的一般規則:

  • Lambda 必須包含與委派型別相同數目的參數。

  • Lambda 中的每個輸入參數都必須能夠隱含轉換為其對應的委派參數。

  • Lambda 的傳回值 (如果存在) 必須能夠隱含轉換為委派的傳回型別。

請注意,Lambda 運算式本身並沒有型別,這是因為通用的型別系統沒有「Lambda 運算式」的內建概念。然而,非正式地說出 Lambda 運算式的「型別」,有時是很方便的功能。 在這些情況下,該型別所指的會是委派型別,或是 Lambda 運算式所轉換成為的 Expression 型別。

Lambda 運算式中的變數範圍

Lambda 可以參考到封入方法或封入型別 (其中定義該 Lambda) 中範圍的「外部變數」(Outer Variable)。 以這種方式擷取的變數會加以儲存以便在 Lambda 運算式中使用,即使這些變數超出範圍而遭到記憶體回收。 外部變數必須在確實指派後,才能用於 Lambda 運算式中。 下列範例會示範這些規則:

delegate bool D();
delegate bool D2(int i);

class Test
{
    D del;
    D2 del2;
    public void TestMethod(int input)
    {
        int j = 0;
        // Initialize the delegates with lambda expressions.
        // Note access to 2 outer variables.
        // del will be invoked within this method.
        del = () => { j = 10;  return j > input; };

        // del2 will be invoked after TestMethod goes out of scope.
        del2 = (x) => {return x == j; };
      
        // Demonstrate value of j:
        // Output: j = 0 
        // The delegate has not been invoked yet.
        Console.WriteLine("j = {0}", j);        // Invoke the delegate.
        bool boolResult = del();

        // Output: j = 10 b = True
        Console.WriteLine("j = {0}. b = {1}", j, boolResult);
    }

    static void Main()
    {
        Test test = new Test();
        test.TestMethod(5);

        // Prove that del2 still has a copy of
        // local variable j from TestMethod.
        bool result = test.del2(10);

        // Output: True
        Console.WriteLine(result);
           
        Console.ReadKey();
    }
}

下列規則適用於 Lambda 運算式中的變數範圍:

  • 已擷取的變數要等到參考它的委派超出範圍以後,才會遭到記憶體回收。

  • 引入到 Lambda 運算式內的變數在外部方法中是不可見的。

  • Lambda 運算式不能直接從封入方法擷取 ref 或 out 參數。

  • Lambda 運算式中的 return 陳述式不會使封入方法傳回。

  • Lambda 運算式不能包含 goto 陳述式、break 陳述式或 continue 陳述式,這些陳述式的目標位在主體外部,或是在所包含之匿名函式的主體中。

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格。語言規格是 C# 語法和用法的限定來源。

精選書籍章節

C# 3.0 Cookbook, Third Edition: More than 250 solutions for C# 3.0 programmers 中的 Delegates, Events, and Lambda Expressions

請參閱

參考

匿名方法 (C# 程式設計手冊)

is (C# 參考)

概念

C# 程式設計手冊

運算式樹狀架構 (C# 和 Visual Basic)

其他資源

LINQ (Language-Integrated Query)

遞迴 Lambda 運算式