C#中的Lambda表達式和表達式樹


在C# 2.0中,通過方法組轉換和匿名方法,使委托的實現得到了極大的簡化。但是,匿名方法仍然有些臃腫,而且當代碼中充滿了匿名方法的時候,可讀性可能就會受到影響。C# 3.0中出現的Lambda表達式在不犧牲可讀性的前提下,進一步簡化了委托。

LINQ的基本功能就是創建操作管道,以及這些操作需要的任何狀態。這些操作表示了各種關於數據的邏輯,例如數據篩選,數據排序等等。通常這些操作都是用委托來表示。Lambda表達式是對LINQ數據操作的一種符合語言習慣的表示方式。

Lambda表達式不僅可以用來創建委托實例,C#編譯器也能夠將他們轉換成表達式樹。

下面我們就先看看Lambda表達式。

作為委托的Lambda表達式

Lambda表達式可以看作是C# 2.0的匿名方法的進一步演變,所以匿名方法能做的幾乎一切事情都可以用Lambda表達式來完成(注意,匿名方法可以忽略參數,Lambda表達式不具備這個特性)。

跟匿名方法類似,Lambda表達式有特殊的轉換規則:表達式的類型本身並非委托類型,但它可以通過隱式或顯式的發那個是轉換為一個委托實例。匿名函數這個術語同時涵蓋了匿名方法和Lambda表達式。

下面看看使用Lambda表達式獲得字符串長度的例子,通過Lambda將得到更見簡潔、易讀的代碼:

“=>”是C# 3.0新增的,告訴編譯器我們正在使用Lambda表達式。”=>”可以讀作”goes to”,所以例子中的Lambda表達式可以讀作”str goes to str.Length”。從例子中還可以看到,根據Lambda使用的特殊情況,我們可以進一步簡化Lambda表達式。

Lambda表達式大多數時候都是和一個返回非void的委托類型配合使用(例如Func<TResult>)。在C# 1.0中,委托一般用於事件,很少會返回什么結果。在LINQ中,委托通常被視為數據管道的一部分,接受輸入並返回結果,或者判斷某項是否符合當前的篩選 器等等。

Lambda表達式本質

通過ILSpy查看上面的例子,可以發現Lambda表達式就是匿名方法,是編譯器幫我們進行了轉換工作,使我們可以直接使用Lambda表達式來進一步簡化創建委托實例的代碼。

在List<T>中使用Lambda表達式

前面簡單的介紹了什么是Lambda表達式,下面通過一個例子進一步了解Lambda表達式。

在前面的文章中,我們也提到了一下List<T>的方法,例如FindAll方法,參數是Predicate<T>類型的 委托,返回結果是一個篩選后的新列表;Foreach方法獲取一個Action<T>類型的委托,然后對每個元素設置行為。下面就看看在 List<T>中使用Lambda表達式:

從上面例子可以看到,當我們要經常使用一個操作的時候,我們最好創建一個委托實例,然后反復調用,而不是每次使用的時候都使用Lambda表達式(例如例子中的printer委托實例)。

相比C# 1.0中的委托或者C# 2.0的匿名函數,結合Lambda表達式,對List<T>中的數據操作變得簡單,易讀。

表達式樹

表達式樹也稱表達式目錄樹,將代碼以一種抽象的方式表示成一個對象樹,樹中每個節點本身都是一個表達式。表達式樹不是可執行代碼,它是一種數據結構。

下面我們看看怎么通過C#代碼建立一個表達式樹。

構建表達式樹

System.Linq.Expressions命名空間中包含了代表表達式的各個類,所有類都從Expression派生,我們可以通過這些類中的靜態方法來創建表達式類的實例。Expression類包括兩個重要屬性:

  • Type屬性代表求值表達式的.NET類型,可以把它視為一個返回類型
  • NodeType屬性返回所代表的表達式的類型

下面看一個構建表達式樹的簡單例子:

代碼的輸出為:

通過例子可以看到,我們構建了一個(6+3)的表達式樹,並且查看了各個節點的Type和NodeType屬性。

Expression有很多派生類,有很多節點類型。例如,BinaryExpression就代表了具有兩個操作樹的任意操作。這正是NodeType屬性重要的地方,它能區分由相同的類表示的不同種類的表達式。其他的節點類型就不介紹了,有興趣可以參考MSDN。

對於上面的例子,可以用下圖描述生成的表達式樹,值得注意的是,”葉子”表達式在代碼中是最先創建的,,表達式是自下而上構建的。表達式是不易變的,所有可以緩存和重用表達式。

將表達式編譯成委托

LambdaExpression是從Expression派生的類型之一。泛型類型Expression<TDelegate>又是從LambdaExpress派生的。

Expression和Expression<TDelegate>的區別在於,泛型類以靜態類型的方式標志了它是什么種類的表達式,也就是說,它確定了返回類型和參數。例如上面的加法例子,返回值是一個int類型,沒有參數,所以我們可以使用簽名Func<int>與之匹配,所以可以用Expression<Func<int>>以靜態類型的方式來表示該表達式

這樣做的目的在於,LambdaExpression有一個Compile方法,該方法能創建一個恰當類型的委托。 Expression<TDelegate>也有一個同名方法,該方法可以返回TDelegate類型的委托。獲得了委托之后,我們就可以使 用普通委托實例調用的方式來執行這個表達式。

接着上面加法的例子,我們把上面的加法表達式樹轉換成委托,然后執行委托:

從這個例子中我們看到怎么構建一個表達式樹,然后把這個對象樹編譯成真正的代碼。在.NET 3.5中的表達式樹只能是單一的表達式,不能表示完整的類、方法。這在.NET 4.0中得到了一定的改進,表達式樹可以支持動態類型,我們可以創建塊,為表達式賦值等等。

將Lambda表達式轉換為表達式樹

Lambda表達式不僅可以創建委托實例,C# 3.0對於將Lambda表達式轉換成表達式樹提供了內建的支持。我們可以通過編譯器把Lambda表達式轉換成一個表達式樹,並創建一個Expression<TDelegate>的一個實例。

下面的例子中我們將一個Lambda表達式轉換成一個表達式樹,並通過代碼查看表達式樹的各個部分:

代碼的輸出為:

表達式樹的用途

前面看到,通過Expression的派生類中的各種節點類型,我們可以構建表達式樹;然后可以把表達式樹轉換成相應的委托類型實例,最后執行委托實例的代碼。但是,我們不會繞這么大的彎子來執行委托實例的代碼。

表達式樹主要在LINQ to SQL中使用,我們需要將LINQ to SQL查詢表達式(返回IQueryable類型)轉換成表達式樹。之所以需要轉換是因為LINQ to SQL查詢表達式不是在C#代碼中執行的,LINQ to SQL查詢表達式被轉換成SQL,通過網絡發送,最后在數據庫服務器上執行。

這里只做個簡單的介紹,后續會介紹LINQ to SQL相關的內容。

編譯器對Lambda表達式的處理

前面我們了解到,Lambda可以用來創建委托實例,也可以用來生成表達式樹,這些都是編譯器幫我們完成的。

編譯器如何決定生成可執行的IL還是一個表達式樹:

  • 當Lambda表達式賦予一個委托類型的變量時,編譯器生成與匿名方法同樣的IL(可執行的委托實例)
  • 當Lambda表達式賦予一個Expression類型的變量時,編譯器就將它轉換成一個表達式樹

下圖展示了LINQ to Object和LINQ to SQL中Lambda表達式的不同處理方式:

總結

本文中介紹了Lambda表達式,在匿名方法的基礎上進一步簡化了委托實例的創建,編寫更加簡潔、易讀的代碼。匿名函數不等於匿名方法,匿名函數包含了匿名方法和lambda表達式這兩種概念

Lambda不僅可以創建委托實例,還可以由編譯器轉換成表達式樹,使代碼可以在程序之外執行(參考LINQ to SQL)。

2 收藏 2 評論
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM