在開始看本篇文章以前先容許我打斷一下各位的興致。其實這篇文章原本是沒有打算加「開篇介紹」這一小節的,後來想一想仍是有必要反饋一下讀者的意見。通過前三篇文章的詳細講解,咱們基本上對LINQ框架的構成原理有了一個根本的認識,包括對它的設計模型、對象的模型等,知道LINQ的查詢表達式實際上是C#之上的語法糖,不過這個糖確實不錯,很方便很及時,又對一系列的LINQ支撐原理進行了大片理論的介紹,不知道效果如何;程序員
在結束上一篇文章的時候,看到一個前輩評論說建議我多寫寫LINQ使用方面的,而不是講這些理論。順便藉此機會解釋一下,本人以爲LINQ的使用文章網上鋪天蓋地,實在沒有什麼必要更沒有價值去寫,網上的LINQ使用性的文章從入門到複雜的應用實在是太多了,無論是什麼級別的程序員都能找到適用的文章。我更以爲這些文章屬於使用類的,在實際項目中用到的時候稍微的查一下能用起來就好了,而重要的是能搞懂其原理纔是咱們長期所追求的,由於這些原理在任何一個應用框架的設計中都是相通的,能夠幫助咱們觸類旁通的學習,減小學習成本,不斷的提升內在的設計思想。編程
所謂設計能力體現技術層次,這句話一點都不假。同志們咱們不斷追求的應該是設計,而不是拿來就用。當你搞懂了原理以後,我想每一個人都能想出來各類不一樣的應用方向,那麼技術發展纔有意義,固然這也是最難能難得的。[王清培版權全部,轉載請給出署名]設計模式
咱們知道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>對象擴展,那麼狀況會有點小複雜,你的擴展方法中要擴展的對象必定要具體的給出對象的定義才行,若是你擴展的對象不能和繼承的對象保持一直,那麼你將斷掉全部的擴展方法。[王清培版權全部,轉載請給出署名]框架
下面咱們經過具體的例子來分析一下上面的理論,先看看經過擴展方法來擴展系統的IEnumerable<T>對象。編輯器
代碼段:Order類ide
- /// <summary>
- /// 訂單類型
- /// </summary>
- public class Order
- {
- /// <summary>
- /// 訂單名稱
- /// </summary>
- public string OrderName { get; set; }
- /// <summary>
- /// 下單時間
- /// </summary>
- public DateTime OrderTime { get; set; }
- /// <summary>
- /// 訂單編號
- /// </summary>
- public Guid OrderCode { get; set; }
- }
這是個訂單類純粹是爲了演示而用,裏面有三個屬性分別是"OrderName(訂單名稱)"、"OrderTime(下單時間)"、"OrderCode(訂單編號)",後面咱們將經過這三個屬性來配合示例的完成。工具
若是咱們是直接使用系統提供的IEnumerable<T>對象的話,只須要構建IEnumerable<T>對象的擴展方法就能實現對集合類型的擴展。我假設使用List<T>來保存一批訂單的信息,可是根據業務邏輯須要咱們要經過提供一套獨立的擴展方法來支持對訂單集合數據的處理。這一套獨立的擴展方法會跟隨着當前系統部署,不做爲公共的開發框架的一部分。這樣很方便也很靈活,徹底能夠替代分層架構中的部分Service層、BLL層的邏輯代碼段,看上去更爲優雅。性能
再發散一下思惟,咱們甚至能夠在擴展方法中作不少文章,把擴展方法歸入系統架構分析中去,採用擴展方法封裝流線型的處理邏輯,對業務的碎片化處理、驗證的鏈式處理都是很不錯的。只有這樣才能真正的讓這種技術深刻人心,才能在實際的系統開發當中去靈活的運用。
下面咱們來構建一個簡單的IEnumerable<T>擴展方法,用來處理當前集合中的數據是否能夠進行數據的插入操做。
代碼段:OrderCollectionExtent靜態類
- public static class OrderCollectionExtent
- {
- public static bool WhereOrderListAdd<T>(this IEnumerable<T> IEnumerable) where T : Order
- {
- foreach (var item in IEnumerable)
- {
- if (item.OrderCode != null && !String.IsNullOrEmpty(item.OrderName) && item.OrderTime != null)
- {
- continue;
- }
- return false;
- }
- return true;
- }
- }
OrderCollectionExtent是個簡單的擴展方法類,該類只有一個WhereOrderListAdd方法,該方法是判斷當前集合中的Order對象是否都知足了插入條件,條件判斷不是重點,僅僅知足例子的須要。這個方法須要加上Order類型泛型約束才行,這樣該擴展方法纔不會被其餘的類型所使用。
- List<Order> orderlist = new List<Order>()
- {
- new Order(){ OrderCode=Guid.NewGuid(), OrderName="水果", OrderTime=DateTime.Now},
- new Order(){ OrderCode=Guid.NewGuid(), OrderName="辦公用品",OrderTime=DateTime.Now}
- };
- if (orderlist.WhereOrderListAdd())
- {
- //執行插入
- }
若是.NET支持擴展屬性【不過微軟後期確定是會支持屬性擴展的】,就不會使用方法來作相似的判斷了。這樣咱們是否是很優雅的執行了之前BLL層處理的邏輯判斷了,並且這部分的擴展方法是能夠動態的更改的,徹底能夠創建在一個獨立的程序集當中。順便在擴展點使用思路,在目前MVVM模式中其實也能夠將V中的不少界面邏輯封裝在擴展方法中來減小VM中的耦合度和複雜度。包括如今的MVC均可以適當的採用擴展方法來達到更爲便利的使用模式。
可是大部分狀況下咱們都是針對全部的IEnunerale<T>類型進行擴展的,這樣能夠很好的結合Linq的鏈式編程。原理就這麼多,根據具體項目須要適當的採納。[王清培版權全部,轉載請給出署名]
我想大部分的狀況下咱們都是直接使用IEnumerable<T>的實現類,可是在編寫系統組件、框架的時候通常都是要本身去實現本身的迭代器類的。那麼這個時候的擴展方法還能做用於咱們繼承下來的類,這是至關方便的,不知不覺咱們本身擴展的組件將也會支持Linq的查詢。可是這個時候應該適當的控制你針對繼承下來的類的擴展,擴展方法應該是面向你內部使用的,不能污染到外部的對象。
咱們繼續看例子,該例子是針對繼承IEnumerable<T>來分析使用方式;
- public class OrderCollection : IEnumerable<Order>
- {
- List<Order> orderList;
- public OrderCollection()
- {
- orderList = new List<Order>() {
- new Order(){ OrderCode=Guid.NewGuid(),OrderName="訂單1", OrderTime=DateTime.Now},
- new Order(){ OrderCode=Guid.NewGuid(),OrderName="訂單2", OrderTime=DateTime.Now},
- new Order(){ OrderCode=Guid.NewGuid(),OrderName="訂單3", OrderTime=DateTime.Now}
- };
- }
- public IEnumerator<Order> GetEnumerator()
- {
- foreach (var order in orderList)
- {
- yield return order;
- }
- }
- System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
- {
- foreach (var order in orderList)
- {
- yield return order;
- }
- }
- }
這是個Order集合類型OrderCollection類,該類專門用來存放或處理Order類的。無論是從兼容.NET2.0或者其餘方面考慮均可能將集合的類型封裝在.NET2.0版本的程序集中,在.NET2.0之上的版本都會提供擴展版本的程序集,這個時候咱們的擴展方法要專門針對OrderCollection去編寫,不然就會形成 IEnumerable<T>對象的污染。
- public static OrderCollection GetOutOrderCollection(this OrderCollection OrderColl)
- {
- return OrderColl;
- }
這個時候會很乾淨的使用着本身的擴展方法,不會形成大面積的污染。固然通常都是依賴倒置原則都會有高層抽象,不會直接擴展實現類,這裏只是簡單的介紹。[王清培版權全部,轉載請給出署名]
這個小結主要將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進行高級應用。[王清培版權全部,轉載請給出署名]
這篇文章的重點就是講解IQueryable<T>、IQueryProvider兩個接口的,當咱們搞懂了這兩個接口以後,咱們就能夠發揮想象力的去實現任何一個數據源的查詢。IQueryable<T>、IQueryProvider兩接口仍是有不少值得咱們研究的好東西,裏面充斥大量的設計模式、數據結構的知識,下面咱們就來慢慢的分析它的美。
IQueryable<T>接口是Linq to Provider的入口,很是有意思的是它並非一個IQueryable<T>來支撐一次查詢。咱們在編寫Linq語句的時候通常都是 where什麼而後select 什麼,至少連續兩個擴展方法的映射調用,可是朋友你知道它內部是如何處理的嗎?每當Where事後緊接着Select他們是如何關聯一個完整的查詢的?IQueryable<T>並不是IEnumerable<T>對象,沒法實時的作出處理而後將結果返回給下一個方法接着執行。那麼它如何將片斷性的執行方法串成一個整的、完整的查詢?下面咱們將逐個的分析這其中要涉及到的模式、數據結構、框架原則,這些搞懂了以後代碼都是模型的表現,也就順其天然的明白了。
延遲加載的技術其實在Linq以前就已經在使用,只不過不多有人去關注它,都被隱藏在系統框架的底層。不少場合下咱們須要本身去構建延遲加載特性的功能,在IEnumerable<T>對象中構建延遲基本上是經過yield return 去構建一個狀態機,當進行迭代的時候才進行數據的返回操做。那麼在IQueryable<T>中是經過執行Provider程序來獲取數據,減小在一開始就獲取數據的性能代價。IQueryable<T>繼承自IEnumerable<T>接口,也就是能夠被foreach語法調用的,可是在GetEnumerator方法中才會去執行提供程序的代碼。咱們來分析一下IQueryable<T>接口的代碼。
- public IEnumerator<T> GetEnumerator()
- {
- return (Provider.Execute<T>(Expression) as IEnumerable<T>).GetEnumerator();
- }
- System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
- {
- return (Provider.Execute(Expression) as IEnumerable).GetEnumerator();
- }
這是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)來獲取數據。
其實這裏有一個思惟陷阱,當咱們分析源碼的時候只將焦點集中在擴展方法中的後面參數上,而沒有集中精力考慮擴展方法所擴展的對象自己,看似不一樣的方法位於不一樣的地方,其實他們來自一個地方,所在的邏輯對象是一個,可是這偏偏會形成咱們分析問題的瓶頸,這裏咱們重點的講解一下擴展方法所擴展對象。
咱們直接用源碼進行講解吧;
- public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
- {
- if (source == null)
- {
- throw Error.ArgumentNull("source");
- }
- if (selector == null)
- {
- throw Error.ArgumentNull("selector");
- }
- return source.Provider.CreateQuery<TResult>(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource), typeof(TResult) }), new Expression[] { source.Expression, Expression.Quote(selector) }));
- }
這是Queryable類中的Select擴展方法的源代碼,它擴展IQueryable<TSource>對象,在方法內部都是在使用source對象來操做,source是擴展對象的直接引用。這是問題的重點,對擴展方法、鏈式編程不熟悉的朋友很難將source能串聯到以前方法所返回的IQueryable<T>對象上。根據這裏的代碼分析,source每次都表明着IQueryable<T>實例,無論你是哪次進行方法的調用,它都表明着你當前調用方法的對象,因此無論咱們進行多少次的調用它們都是連貫的,就比如數據結構裏面的雙向鏈表同樣,這個方法處理完後,接着下一個方法都將是本對象的方法。因此要注意本次的調用將是接着上一次調用,而不是以個新的開始。理解這一點對後面的LINQ分析很關鍵。
都知道Linq的查詢是將一些關鍵字拼接起來的,行成連續的查詢語義,這其中背後的原理文章上上下下也說過不少遍,我想也應該大體的瞭解了。其實這有點像是把大問題分解成多個小問題來解決,可是又不全是爲了分解問題而這樣設計,在鏈式查詢中不少關鍵字在不一樣的查詢上下文中都是公用的,好比where能夠用在查詢,也能夠用在更新、刪除。這裏討論的問題可能已經超過LINQ,可是頗有意義,由於他們有着類似的設計模型。
根據3.2圖中的意思,咱們都已經知道擴展方法之間傳輸的對象都是來自不一樣的實例可是來自一個對象類型,那麼爲何要分段執行每一個關鍵字的操做呢?咱們仍是用圖來幫助咱們分析問題吧。
兩行代碼都引用了Where方法,都須要拼接條件,可是 Where方法所產生的條件不會影響你以前的方法。分段執行的好處就在這裏,最大粒度的脫耦才能最大程度的重用。
在使用IQueryable<T>時,咱們嘗試分析源碼,看看IQueryable內部使用原理來幫咱們生成表達式樹數據的,咱們順其天然的看到了Provider屬性,該屬性是IQueryProvider接口,根據註釋說明咱們搞懂了它是最後執行查詢的提供程序,咱們理所固然的把IQueryable<T>的開始實例當成了查詢的入口,而且在連續調用的擴展方法當中它都保持惟一的一個實例,最後它完整的獲取到了全部表達式,造成一顆表達式樹。可是IQueryable<T>卻跟咱們開了一個玩笑,它的調用到最後的返回不知道執行多少了CreateQuery了。看似一次執行卻隱藏着屢次方法調用,後臺暗暗的構建了咱們都不知道的執行模型,讓人欣喜若狂。咱們來揭開IQueryable<T>在鏈式方法中究竟是如何處理的,看看它到底藏的有多深。
- public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
- {
- if (source == null)
- {
- throw Error.ArgumentNull("source");
- }
- if (predicate == null)
- {
- throw Error.ArgumentNull("predicate");
- }
- return source.Provider.CreateQuery<TSource>(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource) }), new Expression[] { source.Expression, Expression.Quote(predicate) }));
- }
相似這段代碼的在文章的上面曾出現過,大同小異,咱們下面詳細的分析一下它的內部原理,究竟是如何構建一個動態倒是靜態的對象模型。
這個方法有一個參數,是條件表達式,而且這個方法擴展IQueryable<T>接口,任何派生着都能直接使用。方法的返回類型也是IQueryable<T>類型,返回類型和擴展類型相同就已經構成鏈式編程的最小環路。方法中有兩個判斷,第一個是判斷是不是經過擴展方法方式調用代碼的,防止咱們直接使用擴展方法,第二個判斷是肯定咱們是否提供了表達式。
那麼重點是最後一行代碼,它包裹着幾層方法調用,究竟是啥意思呢?咱們詳細分解後天然也就恍然大悟了。
因爲問題比較複雜,這裏不作全面的IQueryable<T>的上下文分析,只保證本節的完整性。經過上圖中,咱們大概能分析出IQueryable<T>對象是每次方法的調用都會產生一個新的實例,這個實例接着被下一個方法天然的接受,依次調用。
面向接口的設計追求職責分離,這裏爲何把執行和建立IQueryable<T>都放到IQueryProvider<T>中去?若是把建立IQueryable<T>提取處理造成獨立的建立接口我以爲更巧妙,固然這只是個人猜想,也許是理解錯了。