C#3.0新增功能08 Lambda 表達式

Lambda 表達式是做爲對象處理的代碼塊(表達式或語句塊)。 它可做爲參數傳遞給方法,也可經過方法調用返回。 Lambda 表達式普遍用於:html

Lambda 表達式是能夠表示爲委託的代碼,或者表示爲表達式樹的代碼,它所表示的表達式樹能夠編譯爲委託。 Lambda 表達式的特定委託類型取決於其參數和返回值。 不返回值的 Lambda 表達式對應於 Action 委託,具體取決於其參數數量。 返回值的 Lambda 表達式對應於 Func 委託,具體取決於其參數數量。 例如,有 2 個參數但不返回值的 Lambda 表達式對應於 Action<T1,T2> 委託。 有 1 個參數並返回值的 Lambda 表達式對應於 Func<T,TResult> 委託。數組

Lambda 表達式使用 lambda 聲明運算符 => 從其可執行代碼中分離 lambda 參數列表。 若要建立 Lambda 表達式,須要在 lambda 運算符左側指定輸入參數(若是有),而後在另外一側輸入表達式或語句塊。 例如,單行 Lambda 表達式 x => x * x 指定名爲 x 的參數並返回 x 的平方值。 以下面的示例所示,你能夠將此表達式分配給委託類型:異步

Func<int, int> square = x => x * x; Console.WriteLine(square(5)); // 輸出: // 25

還能夠將 lambda 表達式分配給表達式樹類型:async

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x; Console.WriteLine(e); // 輸出: // x => (x * x)

或者,能夠將其直接做爲方法參數傳遞:ide

int[] numbers = { 2, 3, 4, 5 }; var squaredNumbers = numbers.Select(x => x * x); Console.WriteLine(string.Join(" ", squaredNumbers)); // 輸出: // 4 9 16 25

若是使用基於方法的語法在 System.Linq.Enumerable 類中調用 Enumerable.Select 方法(就像在 LINQ to Objects 和 LINQ to XML 中同樣),參數是委託類型 System.Func<T,TResult>。 使用 Lambda 表達式建立該委託最爲方便。 若是在 System.Linq.Queryable 類中調用 Queryable.Select 方法(就像在 LINQ to SQL 中同樣),參數類型是表達式樹類型 Expression<Func<TSource,TResult>>。 一樣,Lambda 表達式只是一種很是簡潔的構造該表達式目錄樹的方式。 儘管事實上經過 Lambda 建立的對象具備不一樣的類型,但 Lambda 使得 Select 調用看起來相似。異步編程

全部適用於匿名方法的限制也都適用於 lambda 表達式。函數

表達式 lambda
表達式位於 => 運算符右側的 lambda 表達式稱爲「表達式 lambda」。 表達式 lambda 普遍用於表達式樹的構造。 表達式 lambda 會返回表達式的結果,並採用如下基本形式:
(input-parameters) => expression

僅當 lambda 只有一個輸入參數時,括號纔是可選的;不然括號是必需的。

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

Action line = () => Console.WriteLine();

括號內的兩個或更多輸入參數使用逗號加以分隔:

Func<int, int, bool> testForEquality = (x, y) => x == y;

有時,編譯器沒法推斷輸入類型。 能夠顯式指定類型,以下面的示例所示:

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

輸入參數類型必須所有爲顯式或所有爲隱式;不然,便會生成 CS0748 編譯器錯誤。

表達式 lambda 的主體能夠包含方法調用。 不過,若要建立在 .NET 公共語言運行時的上下文以外(如在 SQL Server 中)計算的表達式樹,不得在 lambda 表達式中使用方法調用。在 .NET 公共語言運行時上下文以外,方法將沒有任何意義。

語句 lambda
語句 lambda 與表達式 lambda 表達式相似,只是語句括在大括號中:
(input-parameters) => { statement; }
語句 lambda 的主體能夠包含任意數量的語句;可是,實際上一般不會多於兩個或三個。
Action<string> greet = name => { string greeting = $"Hello {name}!"; Console.WriteLine(greeting); }; greet("World"); // 輸出: // Hello World!

像匿名方法同樣,語句 lambda 也不能用於建立表達式目錄樹。

異步 lambda
經過使用 async 和 await 關鍵字,你能夠輕鬆建立包含異步處理的 lambda 表達式和語句。例如,下面的 Windows 窗體示例包含一個調用和等待異步方法 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() { // 模擬返回異步進程的任務
        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() { // 模擬返回異步進程的任務
        await Task.Delay(1000); } }

有關如何建立和使用異步方法的詳細信息,請參閱使用 Async 和 Await 的異步編程

lambda 表達式和元組

自 C# 7.0 起,C# 語言提供對元組的內置支持。 能夠提供一個元組做爲 Lambda 表達式的參數,同時 Lambda 表達式也能夠返回元組。 在某些狀況下,C# 編譯器使用類型推理來肯定元組組件的類型。

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}"); // 輸出: // The set (2, 3, 4) doubled: (4, 6, 8)

可經過用括號括住用逗號分隔的組件列表來定義元組。 下面的示例使用包含三個組件的元組,將一系列數字傳遞給 lambda 表達式,此表達式將每一個值翻倍,而後返回包含乘法運算結果的元組(內含三個組件)。

一般,元組字段命名爲 Item1Item2 等等。可是,可使用命名組件定義元組,如如下示例所示。

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# 元組,請參閱 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> 定義包含兩個輸入參數(int 和 string)且返回類型爲 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 表達式將計算那些除以 2 時餘數爲 1 的整數的數量 (n)。

下面的示例生成一個序列,其中包含 numbers 數組中位於 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)); // 輸出: // 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)); // 輸出: // 5 4
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 表達式的類型內,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}"); } // 輸出: // 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 表達式沒法從封閉方法中直接捕獲 inref 或 out 參數。

  • lambda 表達式中的 return 語句不會致使封閉方法返回。

  • 若是相應跳轉語句的目標位於 lambda 表達式塊以外,lambda 表達式不得包含 gotobreak 或 continue 語句。 一樣,若是目標在塊內部,在 lambda 表達式塊外部使用跳轉語句也是錯誤的。

其餘技術請參考

 

相關文章
相關標籤/搜索