1. 程式人生 > >.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口詳解)

.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口詳解)

統架構 ble 優雅 架構分析 bad 大致 集合類 linq查詢 語言

閱讀目錄:

  • 1.開篇介紹
  • 2.擴展Linq to Object (應用框架具有查詢功能)
    • 2.1.通過添加IEnumerable<T>對象的擴展方法
    • 2.2.通過繼承IEnumerable<T>接口
    • 2.3.詳細的對象結構圖
  • 3.實現IQueryable<T> 、IQueryProvider接口
    • 3.1.延遲加載IEnumertor<T>對象(提高系統性能)
    • 3.2.擴展方法的擴展對象之奧秘(this IQueryable<TSource> source)
    • 3.3.分段執行IQueryable<T>中的子方法(Queryable中的擴展方法)
    • 3.4.鏈式查詢方法的設計誤區(重點:一次執行程序多次處理)
    • 3.5.環路執行對象模型、碎片化執行模型(假遞歸式調用)
    • 3.6.N層對象執行模型(縱橫向對比鏈式擴展方法)
    • 3.7.LINQ查詢表達式和鏈式查詢方法其實都是空殼子
    • 3.8.詳細的對象結構圖(對象的執行原理)
    • 3.9.IQueryable<T>與IQueryProvider一對一的關系能否改成一對多的關系
  • 4.完整的自定義查詢

1】.開篇介紹

在開始看本篇文章之前先允許我打斷一下各位的興致。其實這篇文章本來是沒有打算加“開篇介紹”這一小節的,後來想想還是有必要反饋一下讀者的意見。經過前三篇文章的詳細講解,我們基本上對LINQ框架的構成原理有了一個根本的認識,包括對它的設計模型、對象的模型等,知道LINQ的查詢表達式其實是C#之上的語法糖,不過這個糖確實不錯,很方便很及時,又對一系列的LINQ支撐原理進行了大片理論的介紹,不知道效果如何;

在結束上一篇文章的時候,看到一個前輩評論說建議我多寫寫LINQ使用方面的,而不是講這些理論。順便借此機會解釋一下,本人覺得LINQ的使用文章網上鋪天蓋地,實在沒有什麽必要更沒有價值去寫,網上的LINQ使用性的文章從入門到復雜的應用實在是太多了,不管是什麽級別的程序員都能找到適用的文章。我更覺得這些文章屬於使用類的,在實際項目中用到的時候稍微的查一下能用起來就行了,而重要的是能搞懂其原理才是我們長期所追求的,因為這些原理在任何一個應用框架的設計中都是相通的,可以幫助我們舉一反三的學習,減少學習成本,不斷的提高內在的設計思想。

所謂設計能力體現技術層次,這句話一點都不假。同誌們我們不斷追求的應該是設計,而不是拿來就用。當你搞懂了原理之後,我想每個人都能想出來各種不同的應用方向,那麽技術發展才有意義,當然這也是最難能可貴的。

2】.擴展Linq to Object (應用框架具有查詢功能)

我們知道LINQ所支持的查詢範圍主要在IEnumerable<T>、IQueryable<T>這兩個方面,對於我們想要擴展LINQ的查詢能力也主要集中在這兩塊。很多時候我們在編寫應用框架的時候,都會自己去實現IEnumerble<T>對象,一般不會用系統提供的集合類,這樣為了框架的OO性,上下文連貫性,更模型化。如果應用框架具備一定的查詢能力是不是很方便些。比如你在開發一個關於數據密集性的框架,可能不是實時的持久化,但是能在外部提供某種查詢工具來查詢內存中的數據,所以這個時候需要我們能擴展LINQ的Object查詢能力。這一節我們就來學習怎麽擴展Linq to Object。

LINQ查詢Object是基於IEnumerable<T>對象的,不是集合對象有什麽好查的。對於IEnumerable<T>對象的LINQ查詢是Enumerable靜態對象在支撐著,然後通過匿名表達式來表示邏輯,這樣就能順其自然的查詢集合。那麽我們該如何下手擴展Linq to Object?其實也就是兩點可以擴展,要麽提供擴展方法來擴展IEnumerable<T>對象,當然你別企圖想讓VS支持某種關鍵字讓你對應擴展方法。還有就是繼承IEnumerable<T>對象讓我們自己的集合類型具備LINQ的強類型的查詢能力。當然具體要看我們需求,從技術角度看目前只有這兩點可以擴展。

如果我們使用擴展方法那麽只能是擴展IEnumerable<T>對象,這沒有問題。我們可以很方便的在LINQ的表達式中調用我們自己的擴展方法,讓自己的方法跟著一起鏈式查詢。如果我們從繼承IEnumerable<T>對象擴展,那麽情況會有點小復雜,你的擴展方法中要擴展的對象一定要具體的給出對象的定義才行,如果你擴展的對象不能和繼承的對象保持一直,那麽你將斷掉所有的擴展方法。

2.1】.通過添加IEnumerable<T>對象的擴展方法

下面我們通過具體的例子來分析一下上面的理論,先看看通過擴展方法來擴展系統的IEnumerable<T>對象。

代碼段:Order類

技術分享View Code

這是個訂單類純粹是為了演示而用,裏面有三個屬性分別是"OrderName(訂單名稱)"、"OrderTime(下單時間)"、"OrderCode(訂單編號)",後面我們將通過這三個屬性來配合示例的完成。

如果我們是直接使用系統提供的IEnumerable<T>對象的話,只需要構建IEnumerable<T>對象的擴展方法就能實現對集合類型的擴展。我假設使用List<T>來保存一批訂單的信息,但是根據業務邏輯需要我們要通過提供一套獨立的擴展方法來支持對訂單集合數據的處理。這一套獨立的擴展方法會跟隨著當前系統部署,不作為公共的開發框架的一部分。這樣很方便也很靈活,完全可以替代分層架構中的部分Service層、BLL層的邏輯代碼段,看上去更為優雅。

再發散一下思維,我們甚至可以在擴展方法中做很多文章,把擴展方法納入系統架構分析中去,采用擴展方法封裝流線型的處理邏輯,對業務的碎片化處理、驗證的鏈式處理都是很不錯的。只有這樣才能真正的讓這種技術深入人心,才能在實際的系統開發當中去靈活的運用。

下面我們來構建一個簡單的IEnumerable<T>擴展方法,用來處理當前集合中的數據是否可以進行數據的插入操作。

代碼段:OrderCollectionExtent靜態類

技術分享View Code

OrderCollectionExtent是個簡單的擴展方法類,該類只有一個WhereOrderListAdd方法,該方法是判斷當前集合中的Order對象是否都滿足了插入條件,條件判斷不是重點,僅僅滿足例子的需要。這個方法需要加上Order類型泛型約束才行,這樣該擴展方法才不會被其他的類型所使用。

技術分享View Code

如果.NET支持擴展屬性【不過微軟後期肯定是會支持屬性擴展的】,就不會使用方法來做類似的判斷了。這樣我們是不是很優雅的執行了以前BLL層處理的邏輯判斷了,而且這部分的擴展方法是可以動態的更改的,完全可以建立在一個獨立的程序集當中。順便在擴展點使用思路,在目前MVVM模式中其實也可以將V中的很多界面邏輯封裝在擴展方法中來減少VM中的耦合度和復雜度。包括現在的MVC都可以適當的采用擴展方法來達到更為便利的使用模式。

但是大部分情況下我們都是針對所有的IEnunerale<T>類型進行擴展的,這樣可以很好的結合Linq的鏈式編程。原理就這麽多,根據具體項目需要適當的采納。

2.2】.通過繼承IEnumerable<T>接口

我想大部分的情況下我們都是直接使用IEnumerable<T>的實現類,但是在編寫系統組件、框架的時候一般都是要自己去實現自己的叠代器類的。那麽這個時候的擴展方法還能作用於我們繼承下來的類,這是相當方便的,不知不覺我們自己擴展的組件將也會支持Linq的查詢。但是這個時候應該適當的控制你針對繼承下來的類的擴展,擴展方法應該是面向你內部使用的,不能汙染到外部的對象。

我們繼續看例子,該例子是針對繼承IEnumerable<T>來分析使用方式;

技術分享View Code

這是個Order集合類型OrderCollection類,該類專門用來存放或處理Order類的。不管是從兼容.NET2.0或者其他方面考慮都可能將集合的類型封裝在.NET2.0版本的程序集中,在.NET2.0之上的版本都會提供擴展版本的程序集,這個時候我們的擴展方法要專門針對OrderCollection去編寫,否則就會造成 IEnumerable<T>對象的汙染。

技術分享View Code

這個時候會很幹凈的使用著自己的擴展方法,不會造成大面積的汙染。當然一般都是依賴倒置原則都會有高層抽象,不會直接擴展實現類,這裏只是簡單的介紹。

2.3】.詳細的對象結構圖

這個小結主要將IEnumerable<T>及它的擴展方法包括Linq的查詢進行一個完整的結構分析,將給出詳細的對象結構導圖。

對象靜態模型、運行時導圖:

技術分享

上圖中的關鍵部分就是i==10將被封裝成表達式直接送入Where方法,而select後面的i也是表達式【(int i)=>i】,也將被送入Select方法,這裏就不畫出來了。順著數字序號理解,IEnumerable<T>是Linq to Object的數據源,而Enumerable靜態類是專門用來擴展Linq查詢表達式中的查詢方法的,所以當我們編寫Linq查詢IEnumerable<T>集合是,其實是在間接的調用這些擴展方法,只不過我們不需要那麽繁瑣的去編寫Lambda表達式,由編輯器幫我們動態生成。

小結:本節主要講解了Linq to Object的原理,其實主要的原理就是Lambda表達式傳入到Enumerable擴展方法當中,然後形成鏈式操作。Linq 只是輔助我們快速查詢的語言,並不是.NET或者C#的一部分,在任何.NET平臺上的語言中都可以使用。下面我們將重點分析Linq to Provider,這樣我們才能真正的對LINQ進行高級應用。

3.】.實現IQueryable<T> 、IQueryProvider接口

這篇文章的重點就是講解IQueryable<T>、IQueryProvider兩個接口的,當我們搞懂了這兩個接口之後,我們就可以發揮想象力的去實現任何一個數據源的查詢。IQueryable<T>、IQueryProvider兩接口還是有很多值得我們研究的好東西,裏面充斥大量的設計模式、數據結構的知識,下面我們就來慢慢的分析它的美。

IQueryable<T>接口是Linq to Provider的入口,非常有意思的是它並不是一個IQueryable<T>來支撐一次查詢。我們在編寫Linq語句的時候一般都是 where什麽然後select 什麽,至少連續兩個擴展方法的映射調用,但是朋友你知道它內部是如何處理的嗎?每當Where過後緊接著Select他們是如何關聯一個完整的查詢的?IQueryable<T>並非IEnumerable<T>對象,無法實時的做出處理然後將結果返回給下一個方法接著執行。那麽它如何將片段性的執行方法串成一個整的、完整的查詢?下面我們將逐個的分析這其中要涉及到的模式、數據結構、框架原則,這些搞懂了之後代碼都是模型的表現,也就順其自然的明白了。

3.1】.延遲加載IEnumertor<T>對象(提高系統性能)

延遲加載的技術其實在Linq之前就已經在使用,只不過很少有人去關註它,都被隱藏在系統框架的底層。很多場合下我們需要自己去構建延遲加載特性的功能,在IEnumerable<T>對象中構建延遲基本上是通過yield return 去構建一個狀態機,當進行叠代的時候才進行數據的返回操作。那麽在IQueryable<T>中是通過執行Provider程序來獲取數據,減少在一開始就獲取數據的性能代價。IQueryable<T>繼承自IEnumerable<T>接口,也就是可以被foreach語法調用的,但是在GetEnumerator方法中才會去執行提供程序的代碼。我們來分析一下IQueryable<T>接口的代碼。

技術分享View Code

這是IQueryable<T>接口中從IEnumerable<T>繼承下來的兩個返回IEnumerator接口類型的方法,在我們目前使用的Linq to Sql、Linq to Entity中都會返回強類型的集合對象,一般都不會實時的進行數據查詢操作,如果要想實時執行需要進行IQueryable<T>.Provider.Execute方法的直接調用。

我們用圖來分析一下Linq to Provider中的延遲加載的原理;

技術分享

這段代碼不會被立即執行,我們跟蹤一下各個組成部分之間的執行過程;

技術分享

這幅圖重點是IQueryable<T>對象的連續操作,大致原理是每次執行擴展方法的時候都會構造一個新的IQueryable<T>,本次的IQueryable<T>對象將包含上次執行的表達式樹,以此類推就形成了一顆龐大的表達式樹。詳細的原理在下面幾小節中具體分析。

最後Orderlist將是一個IQueryable<T>類型的對象,該對象中包含了完整的表達式樹,這個時候如果我們不進行任何的使用將不會觸發數據的查詢。這就是延遲加載的關鍵所在。如果想立即獲取orderlist中的數據可以手動執行orderlist.Provider.Execute<TB_Order>(orderlist.Expression)來獲取數據。

3.2】.擴展方法的擴展對象之奧秘(this IQueryable<TSource> source)

其實這裏有一個思維陷阱,當我們分析源碼的時候只將焦點集中在擴展方法中的後面參數上,而沒有集中精力考慮擴展方法所擴展的對象本身,看似不同的方法位於不同的地方,其實他們來自一個地方,所在的邏輯對象是一個,但是這恰恰會造成我們分析問題的瓶頸,這裏我們重點的講解一下擴展方法所擴展對象。

我們直接用源碼進行講解吧;

技術分享View Code

這是Queryable類中的Select擴展方法的源代碼,它擴展IQueryable<TSource>對象,在方法內部都是在使用source對象來操作,source是擴展對象的直接引用。這是問題的重點,對擴展方法、鏈式編程不熟悉的朋友很難將source能串聯到之前方法所返回的IQueryable<T>對象上。根據這裏的代碼分析,source每次都代表著IQueryable<T>實例,不管你是哪次進行方法的調用,它都代表著你當前調用方法的對象,所以不管我們進行多少次的調用它們都是連貫的,就好比數據結構裏面的雙向鏈表一樣,這個方法處理完後,接著下一個方法都將是本對象的方法。所以要註意本次的調用將是接著上一次調用,而不是以個新的開始。理解這一點對後面的LINQ分析很關鍵。

技術分享

3.3】.分段執行IQueryable<T>中的子方法(Queryable中的擴展方法)

都知道Linq的查詢是將一些關鍵字拼接起來的,行成連續的查詢語義,這其中背後的原理文章上上下下也說過很多遍,我想也應該大致的了解了。其實這有點像是把大問題分解成多個小問題來解決,但是又不全是為了分解問題而這樣設計,在鏈式查詢中很多關鍵字在不同的查詢上下文中都是公用的,比如where可以用在查詢,也可以用在更新、刪除。這裏討論的問題可能已經超過LINQ,但是很有意義,因為他們有著相似的設計模型。

根據3.2圖中的意思,我們都已經知道擴展方法之間傳輸的對象都是來自不同的實例但是來自一個對象類型,那麽為什麽要分段執行每個關鍵字的操作呢?我們還是用圖來幫助我們分析問題吧。

技術分享

兩行代碼都引用了Where方法,都需要拼接條件,但是 Where方法所產生的條件不會影響你之前的方法。分段執行的好處就在這裏,最大粒度的脫耦才能最大程度的重用。

3.4】.鏈式查詢方法的設計誤區(重點:一次執行程序多次處理)

在使用IQueryable<T>時,我們嘗試分析源碼,看看IQueryable內部使用原理來幫我們生成表達式樹數據的,我們順其自然的看到了Provider屬性,該屬性是IQueryProvider接口,根據註釋說明我們搞懂了它是最後執行查詢的提供程序,我們理所當然的把IQueryable<T>的開始實例當成了查詢的入口,並且在連續調用的擴展方法當中它都保持唯一的一個實例,最後它完整的獲取到了所有表達式,形成一顆表達式樹。但是IQueryable<T>卻跟我們開了一個玩笑,它的調用到最後的返回不知道執行多少了CreateQuery了。看似一次執行卻隱藏著多次方法調用,後臺暗暗的構建了我們都不知道的執行模型,讓人欣喜若狂。我們來揭開IQueryable<T>在鏈式方法中到底是如何處理的,看看它到底藏的有多深。

技術分享View Code

類似這段代碼的在文章的上面曾出現過,大同小異,我們下面詳細的分析一下它的內部原理,到底是如何構建一個動態卻是靜態的對象模型。

這個方法有一個參數,是條件表達式,並且這個方法擴展IQueryable<T>接口,任何派生著都能直接使用。方法的返回類型也是IQueryable<T>類型,返回類型和擴展類型相同就已經構成鏈式編程的最小環路。方法中有兩個判斷,第一個是判斷是否是通過擴展方法方式調用代碼的,防止我們直接使用擴展方法,第二個判斷是確定我們是否提供了表達式。

那麽重點是最後一行代碼,它包裹著幾層方法調用,到底是啥意思呢?我們詳細分解後自然也就恍然大悟了。

技術分享

由於問題比較復雜,這裏不做全面的IQueryable<T>的上下文分析,只保證本節的完整性。通過上圖中,我們大概能分析出IQueryable<T>對象是每次方法的調用都會產生一個新的實例,這個實例接著被下一個方法自然的接受,依次調用。

面向接口的設計追求職責分離,這裏為什麽把執行和創建IQueryable<T>都放到IQueryProvider<T>中去?如果把創建IQueryable<T>提取處理形成獨立的創建接口我覺得更巧妙,當然這只是我的猜測,也許是理解錯了。

.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口詳解)