Lambda 運算式 (C# 程式設計手冊)
「Lambda 運算式」(Lambda Expression) 是一種匿名函式,它可以包含運算式和陳述式 (Statement),而且可以用來建立委派 (Delegate) 或運算式樹狀架構型別。
所有的 Lambda 運算式都會使用 Lambda 運算子 =>,意思為「移至」。 Lambda 運算子的左邊會指定輸入參數 (如果存在),右邊則包含運算式或陳述式區塊。 Lambda 運算式 x => x * x 的意思是「x 移至 x 乘以 x」。這個運算式可以指派成委派型別 (Delegate Type),如下所示:
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 運算式。 如需詳細資訊,請參閱匿名方法 (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 就像匿名方法,它不能用來建立運算式樹狀架構。
具有標準查詢運算子的 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 陣列中所有在 9 左側的元素,因為 9 是序列中第一個不符合條件的數字:
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