《深入理解C#》整理8-超越集合的LINQ

一、用IQueryable和IQueryProvider進行轉換

在LINQ to SQL中的所有查詢表達式中,數據源都是Table。不過,如果你看一下Table,你就會發現它沒有Where、Select和Join方法,或任何其他的標準查詢操作符。但是,它利用了和LINQ to Objects同樣的技巧——LINQ to Objects中的數據源總是實現IEnumerable(可能在調用Cast或OfType之後) ,然後使用Enumerable中的擴展方法,而Table實現了IQueryable並使用Queryable的擴展方法。

1、IQueryable和相關接口的介紹

IQueryable從IEnumerable和非泛型的IQueryable繼承而來,而IQueryable又繼承於非泛型的IEnumerable。IQueryable僅有3個屬性:Query Provider、ElementType和Expression。QueryProvider屬性是IQueryProvider類型——另一個需要考慮的新接口

image-20201028203820528

理解IQueryable的最簡單方式就是,把它看作一個查詢,在執行的時候,將會生成結果序列。從LINQ的角度看,由於是通過IQueryable的Expression屬性返回結果,所以查詢的詳細信息就保存於表達式樹中。一個查詢進行執行,就是開始遍歷IQueryable的過程(換句話說,即調用GetEnumerator方法,然後對其結果調用MoveNext方法),或者調用IQueryProvider上的Execute方法並傳遞表達式樹。給定任何IQueryable查詢後,你可通過執行如下步驟來創建新的查詢:

(1) 請求現有查詢的查詢表達式樹(使用Expression屬性);

(2) 構建一個新的表達式樹,包含最初的表達式和你想要的額外功能(例如,過濾、投影或排序);

(3) 請求現有查詢的查詢提供器(使用Provider屬性);

(4) 調用提供器的CreateQuery方法,傳遞新表達式樹。

2、模擬接口實現來記錄調用

這裏我們會編寫IQueryable和IQueryProvider的實現,FakeQueryProvider和FakeQuery類型。實現如下:

image-20201028211643024

無參數的構造函數,主程序用它爲查詢創建普通“數據源”;而FakeQueryProvider則會調用另一個構造函數並傳入當前的查詢表達式。Expression.Constant(this)用作初始數據源表達式只是爲了展示查詢表示原始的對象。(例如,假設某個實現表示一個數據表——如果不使用任何查詢操作符,查詢將返回整個數據表。)

image-20201028211915742

上述CreateQuery方法不執行真正的處理,而是作爲查詢的工廠方法。Execute重載方法只是在記錄調用日誌後返回空結果。通常情況下,在這裏應該完成大量的分析工作,以及對Web服務、數據庫或任何目標平臺的實際調用。

3、把表達式粘合在一起:Queryable的擴展方法

正如Enumerable類型包含着關於IEnumerable的擴展方法來實現LINQ標準查詢操作符一樣,Queryable類型包含着關於IQueryable的擴展方法。IEnumerable和Queryable的實現之間有兩個巨大的區別:

①Enumerable的方法都使用委託作爲參數,而對於Queryable來說則需要表達式樹(Lambda表達式既可以被轉換爲委託實例,也可以轉換爲表達式樹);

image-20201028212824152

②Enumerable的擴展方法會完成與對應查詢操作符相關的實際工作(至少會構建完成這些工作的迭代器)。而Queryable中的查詢操作符的“實現”做的事情非常少:它們僅僅基於參數創建一個新的查詢,或在查詢提供器上調用Execute。換句話說,它們只用來構建查詢和要執行的請求——不包含操作符背後的邏輯。這意味着,它們可用於任何使用表達式樹的LINQ提供器,但是它們單獨使用時沒有任何意義。它們是代碼和提供器細節之間的黏合劑。

4、模擬實際運行的查詢提供器

image-20201028213637539

GetEnumerator只在最後才調用,而不在任何中間查詢中調用,並且在GetEnumerator被調用的時候,我們已經有了出現在原始查詢表達式中的所有信息。到目前爲止,單一的表達式樹已經捕獲了所有的信息。實際的表達式樹可能非常深且複雜,特別是在Where子句包含額外方法調用的時候。LINQ to SQL檢查表達式樹以算出應該執行什麼樣的查詢。當調用CreateQuery時,LINQ提供器能夠構建它們自己的查詢(以它們需要的任何形式),不過,當調用GetEnumerator的時候,看一下最後的表達式樹,可知道它們通常都比較簡單,這是因爲所有需要的信息都已經保存在同一個地方了。

二、LINQ友好的API和LINQ to XML

1、LINQ to XML中的核心類型

LINQ to XML位於System.Xml.Linq程序集,並且大多數類型都位於System.Xml.Linq命名空間。下圖展示了最常用的一些類型:

image-20201028214425626

  • XName表示元素和特性的名稱。
  • XNamespace表示XML命名空間,通常是一個URI。
  • XObject是XNode和XAttribute的共同父類:與在DOM API中不同,在LINQ to XML中特性不是節點。如果某方法返回子節點的元素,這裏面是不包含特性的
  • XNode表示XML樹中的節點。它定義了各種用於操作和查詢樹的成員。
  • XAttribute表示包含名/值對的特性。
  • XContainer是XML樹中可以包含子內容(主要爲元素或文檔)的節點。
  • XText表示文本節點,其派生類XCData表示CDATA文本節點。
  • XElement表示元素。它和XAttribute是LINQ to XML中最常用的類。與在DOM API中不同,在創建一個XElement時,不需要創建包含它的文檔。
  • XDocument表示文檔。可以通過Root屬性訪問其根元素,相當於XmlDocument.Document Element。

2、聲明式構造

①在DOM API中,我們通常創建一個元素,然後向其中添加內容。在LINQ to XML中我們也可以這樣做,使用繼承自XContainer的Add方法,但這並不是LINQ to XML的慣用法。不過還是有必要看一下XContainer.Add的簽名,因爲它使用了內容模型。你也許會認爲其簽名爲Add(XNode)或Add(XObject),但事實上它只是Add(object)。XElement(和XDocument)的構造函數簽名也使用了同樣的模式。在名稱之後,你可以什麼都不指定(創建空元素),也可以指定一個對象(創建包含單個子節點的元素),或對象數組(創建包含多個子節點的元素)。在創建多個子節點的時候,使用了參數數組(C#中的params關鍵字),這意味着編譯器將爲我們創建數組,我們只需要不斷列出參數即可。

②在創建內容時,不管是通過構造函數還是Add方法,都要考慮以下幾點:

  • 空引用會被忽略
  • XNode和XAttribute實例可以直接添加。如果它們已經有了父元素,將會被複制,但除此之外不需要任何轉換
  • 字符串、數字、日期、時間等將使用標準XML格式轉換爲XText
  • 如果參數實現了IEnumerable,Add方法將迭代其內容,並添加各個值,必要的時候會使用遞歸
  • 其他沒有特殊處理的對象將調用ToString()將其轉換爲文本

image-20201028215926652

3、查詢單個節點

XElement包含很多軸方法,可用於查詢資源,每個方法都返回適當的IEnumerable,這意味着可以在查詢出一些元素之後,使用普通的LINQ to Objects方法。除了這些返回序列的方法,有些方法還返回單個結果——其中最重要的是Attribute和Element,分別返回已命名的特性和具備指定名稱的第一個子元素。

image-20201028220630645

4、合併查詢操作符

查詢的部分結果往往爲另一個序列,而在LINQ to XML中則通常爲元素的序列。如何找出各個項目中的某一個元素呢?這時我們需要對各個元素執行另一個查詢,然後合併這些結果。LINQ to Objects已經提供了SelectMany操作符來實現該功能,但對於XML來說並非是理想的。LINQ to XML提供了一些擴展方法(位於System.Xml.Linq.Extensions類中),有的針對特殊的序列類型,有的是包含強制類型參數的泛型方法,以應對C# 4之前缺乏泛型接口協變性的問題。上節中提到的軸方法大多可作爲擴展方法的形式使用。

因而下面的查詢可以進行如下轉變:

image-20201028221648472

5、與LINQ和諧共處

LINQ to XML使用瞭如下三種方式與其他LINQ相適應:

  • 在構造函數中消費序列。LINQ是刻意聲明式語言,LINQ to XML支持聲明式地創建XML結構。
  • 在查詢方法中返回序列。這大概是數據訪問API必須遵循的最爲明顯的步驟:查詢結果應該輕而易舉地返回IEumerable或實現了該接口的類。
  • 擴展了可以對XML類型的序列所作的查詢,這樣可以讓它們看上去更像是統一的查詢API,儘管有些查詢必須用於XML。

三、用並行LINQ代替LINQ to Objects

並行LINQ的背後理念是,某個LINQ to Objects查詢需要執行很長的時間,而使用多線程利用多核優勢進行查詢則可以運行得很快,並且改動也很少。

1、在單線程中繪製曼德博羅特集

我們遍歷每一行以及每行中的每一列,計算相關像素的索引。如下:

image-20201029192201651

2、ParallelEnumerable、ParallelQuery和AsParallel

並行LINQ帶來了一些新的類型,它們位於System.Linq命名空間,ParallelEnumerable是一個靜態類,與Enumerable類似。它裏面幾乎全部是擴展方法,其中大多數都擴展了ParallelQuery這個類型。該類型包含泛型和非泛型形式(ParallelQuery和ParallelQuery),我們大多數情況下都會使用其泛型形式,就像IEnumerable要比IEnumerable常用。此外,還有一個OrderedParallelQuery類,它是IOrderedEnumerable的並行版本。這些類型之間的關係如圖所示:

image-20201029192437882

如何以並行查詢開始呢?答案是調用AsParallel,它是ParallelEnumerable中的擴展方法,擴展了IEnumerable。該查詢確實可以並行運行——但結果並不完全符合我們的要求:它的順序與我們處理每行的順序並不相同,爲了改善性能,PLINQ默認使用無序查詢。

image-20201029192742012

3、調整並行查詢

你只需要使用AsOrdered擴展方法,強制對查詢排序即可。它比無序查詢要略慢,但仍明顯快於單線程版本

image-20201029193112035

四、使用LINQ to Rx反轉查詢模型

當數據由消費者掌管,即當新數據可用的時候,由數據消費者進行響應,這就是所謂的“推”。有一個有趣的程序集叫做System.Interactive,它包含各種額外的LINQ to Objects方法;System.Reactive實現了各種推操作。

1、IObservable和IObserver

LINQ to Rx的數據模型與普通IEnumerable的模型在數學上是對偶的。它不向迭代器發出請求,而是提供一個觀察者。然後,它也不請求下一個項,而是通知你的代碼是否準備好了一個項、是否有錯誤發生、是否到達了數據末端。以下是涉及的兩個接口的聲明:

image-20201029194054799

這兩個接口屬於.NET 4(位於System命名空間),但LINQ to Rx的其餘部分則是需要單獨下載的。實際上,在.NET 4中這兩個接口是IObservable和IObserver,分別表示IObservable是協變的,IObserver是逆變的。

LINQ to Rx與我們熟悉的事件十分類似。調用一個可觀察對象(observable)的Subscribe,就像是對事件使用+=來註冊處理程序一樣。Subscribe返回的可處置(disposable)值會記住傳入的觀察者(observer):處置它就像對同一個處理程序使用-=一樣。通常,觀察者將重複調用OnNext方法,並最終調用OnCompleted——這期間如果出現了某種錯誤,就用OnError代替。在序列結束或發生錯誤之後不會再調用其他方法。

image-20201029194652999

2、簡單的開始

這裏我們使用Observable.Range來創建一個可觀察的範圍,而不再使用Enumerable.Range。每當一個觀察者訂閱這個範圍時,使用OnNext把數字發送給該觀察者,最後將調用OnCompleted。Range方法返回的是一個冷可觀察對象(cold observable)。它處於休眠狀態,直到某個觀察者訂閱了它,它纔會向該觀察者發送值。如果其他觀察者也訂閱了該對象,將會得到該範圍的一個副本。這與點擊按鈕這種普通的事件不太相同,對於後者,多個觀察者可以同時訂閱同一個實際的值序列——並且即便沒有任何觀察者,也會有效地產生值。這種序列稱爲熱可觀察對象(hot observable)

image-20201029195121128

3、查詢可觀察對象

相關示例如下:

image-20201029195611777

我們在LINQ to Objects中處理分組時常常要嵌套foreach循環,因此在LINQ to Rx中要嵌套訂閱。在進行分組操作,LINQ to Objects在返回之前把整個分組收集在一起,這意味着要對結果進行緩衝,直到序列的末尾。在某些情況下,在LINQ to Objects中所需的大量的數據緩衝操作,都可以用LINQ to Rx更高效地實現。

4、意義何在

Rx提供了一種優雅的方式來思考各種異步處理——如普通.NET事件(可以使用Observable. FromEvent將其視爲可觀察對象)、異步I/O和調用We b服務。它提供了一種有效的方式來管理複雜性和併發。

五、擴展LINQ to Objects

1、設計和實現指南

當爲LINQ進行相關查詢操作符的擴展時,應該注意以下幾點:

  • 單元測試:不要忘記測試個別情況,如空序列和無效參數等;
  • 檢查參數:好的方法會檢查傳入的參數。但這對LINQ操作符來說有一個問題。很多操作符都返回一個序列,而實現這種功能最簡單的方式就是迭代器塊。但你應該在調用方法的同時執行參數檢查,而不應該等到調用者決定迭代其結果的時候。如果打算使用迭代器塊,就把方法分成兩部分:在公共方法中執行參數檢查,然後調用一個私有方法進行迭代。
  • 優化:IEnumerable本身所支持的操作十分有限,但你所操作的序列的執行時類型可能具備更多的功能。
  • 文檔:在文檔中指明代碼對輸入的處理和操作符的預期性能是十分重要的。
  • 儘量只迭代一次:可以對IEnumerable進行多次迭代——實際上對於同一個序列,你可以同時擁有多個活動的迭代器。但對於一個查詢操作符來說,這樣做可不是什麼好主意。
  • 釋放迭代器:在大多數情況下,我們可以使用foreach語句來迭代數據源。在這種情況下,要爲迭代器使用using塊
  • 自定義比較器:很多LINQ操作符都包含可以指定適當IEqualityComparer或IComparer的重載。通常簡單的重載只需要調用複雜的重載,傳入EqualityComparer.Default或Comparer.Default作爲比較器

2、示例擴展:選擇隨機元素

image-20201029202038040

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章