C#中的IEnumerable接口深入研究
C#和VB.NET中的LINQ提供了一種與SQL查詢類似的“對象查詢”語言,對于熟悉SQL語言的人來說除了可以提供類似關聯(lián)、分組查詢的功能外,還能獲取編譯時檢查和Intellisense的支持,使用Entity Framework更是能夠自動為對象實體的查詢生成SQL語句,所以很受大中型信息系統(tǒng)設計者的青睞。
IEnumerable這個接口可以說是為了這個特性“量身定制”,再加上微軟提供的擴展(Extension)方法和Lambda表達式,給開發(fā)者帶來了無窮的便利。本人在最近的開發(fā)工作中使用了大量的這種特性,同時在調試過程中還遇到了一個小問題,那么正好趁此機會好好研究一下相關原理和實現(xiàn)。
先從一個現(xiàn)實的例子開始吧。假如我們要做一個商品檢索功能(這只是一個例子,我當然不可能把公司的產品也業(yè)務在這里貼出來),其中有一個檢索條件是可以指定廠家的名稱并進行模糊匹配。廠家的包括兩個名稱:注冊名稱和一般性名稱,我們只按一般性名稱進行檢索。當然你可以說直接用SQL查詢就行了,但是我們的系統(tǒng)是以實體對象為核心進行設計的,廠家的數(shù)量也不會太多,大概1000條。為了不增加系統(tǒng)的復雜性,只考慮使用現(xiàn)有的數(shù)據(jù)訪問層接口進行實現(xiàn)(按過濾條件獲取商品,以及獲取所有廠商),這時LINQ的便捷性就體現(xiàn)出來了。
借助IEnumerable接口和其輔助類,我們可以寫出以下代碼:
public GoodsListResponse GetGoodsList(GoodsListRequest request)
{
//從數(shù)據(jù)庫中按商品類別獲取商品列表
IEnumerable<Goods> goods = GoodsInformation.GetGoodsByCategory(request.CategoryId);
//用戶指定了商品名檢索字段,進行模糊匹配
//如果沒有指定,則不對商品名進行過濾
if (!String.IsNullOrWhiteSpace(request.GoodsName))
{
request.GoodsName = request.GoodsName.Trim().ToUpper();
//按商品名對 goods 中的對象進行過濾
//生成一個新的 IEnumerable<Goods> 類型的迭代器
goods = goods.Where(g => g.GoodsName.ToUpper().Contains(request.GoodsName));
}
//如果用戶指定的廠商的檢索字段,進行模糊匹配
if (!String.IsNullOrWhiteSpace(request.ManufactureName))
{
request.ManufactureName = request.ManufactureName.Trim().ToUpper();
//只提供了獲取所有廠商的列表方法
//取出所有廠商,篩選包含關鍵字的廠商
IEnumerable<Manufacture> manufactures = ManufactureInformation.GetAll();
manufactures = manufactures.Where(m => m.Name.GeneralName.ToUpper()
.Contains(request.ManufactureName));
//取出任何符合所匹配廠商的商品
goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId));
}
GoodsListResponse response = new GoodsListResponse();
//將 goods 放到一個 List<Goods> 對象中,并返回給客戶端
response.GoodsList = goods.ToList();
return response;
}
假如不使用IEnumerable這個接口,所實現(xiàn)的代碼遠比上面復雜且難看。我們需要寫大量的foreach語句,并手工生成很多中間的 List 來不斷地篩選對象(你可以嘗試把第二個if塊改寫成不用IEnumerable接口的形式)。
看上去一切都很和諧,但是上面的代碼有一個隱含的bug,這個bug也是今天上午困擾了我許久的一個問題。
運行程序,當我不輸入廠商檢索條件的時候,程序運行是正確的。但當我輸入一個廠商的名字時,系統(tǒng)拋出了一個空引用的異常。咦?為什么會有空引用呢?我輸入的廠商是數(shù)據(jù)庫中不存在的廠商,因此我覺得問題可以出在goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId)) 這句話上。既然manufactures是空的,那么是不是意味著我不能調用其 Any 方法呢(lambda表達式中的部分)。于是我改寫成以下形式:
if (manufactures != null)
//取出任何符合所匹配廠商的商品
goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId));
還是不行,那么我對manufactures判斷其是否有元素,就調用其無參數(shù)的Any方法,這時問題依舊:
聰明的你肯定已經(jīng)看出問題出在哪了,因為Visual Studio已經(jīng)提示得很清楚了。但我當時還局限在“列表為空”這個框框中,因此遲遲不能發(fā)現(xiàn)原因。出錯是發(fā)生在 manufactures.Any() 這句話上,而我已經(jīng)判斷了它不為空啊,為什么還會拋錯呢?
后來叫了一個同事幫我看,他說的四個字一下子就提醒了我“延遲計算”。哦,對!我怎么把這個特性給忘了。在最初的代碼中(就是沒有對 manufactures 為空進行判斷),出錯是發(fā)生在 goods.ToList() 這句話時,而圖上的那個代碼段出錯是發(fā)生在調用Any()方法時(圖中的灰色部分),而我單步跟蹤到 Any() 這句話上時,出錯的語句跳到 Where 子句(黃色部分),說明知道訪問 Any 方法時lambda表達式才被調用。
那么很顯然是 Where 語句中這個 predicate 有問題:Manufacture的Name字段可能為空(數(shù)據(jù)庫中存在這樣的數(shù)據(jù),所以導致在 translate 的時候Name字段為空),那么改寫成以下形式就能解決問題,當然我們不用對 manufactures 列表進行為空的判斷:
manufactures = manufactures.Where(m => m.Name != null &&
m.Name.GeneralName.ToUpper().Contains(request.ManufactureName));
在此要感謝那位同事看出了問題所在,否則我不知道還得郁悶多久。
我之前在使用 LINQ 語句的時候知道它的延遲計算特性,但是沒有想到從根本上自 IEnumerable 的擴展方法就有這個特性。那么很顯然,C#的編譯器只是把 LINQ 語句改寫成類似于調用 Where、Select之類的擴展方法,延遲計算這種特性是 IEnumerable 的擴展方法就支持的!我之前一直以為我每調用一次 Where 或者 Select(其實我SelectMany用得更多),就會對結果進行過濾,現(xiàn)在看來并不是這樣。
即使是使用 Where 等擴展方法, 執(zhí)行這些 predicate 的時間是在 foreach 和 ToList 的時候才發(fā)生。
為什么會這樣呢?看樣子這完全不應該呀?Where子句的返回值就是一個IEnumerable的迭代器,按道理應該已經(jīng)篩選了對象???為了徹底搞清楚這個問題,那么方法很明顯——看 .NET 的源代碼。
Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 是它的方法頭,在看源代碼之前,相信你已經(jīng)知道微軟大概是怎么實現(xiàn)的了:既然Where接受一個Func類型的委托,并且都是在ToList 或者 foreach 的時候計算的,那么顯而易見實現(xiàn)應該是……
好了,來看下代碼吧。IEnumerable的擴展方法都在 Enumerable 這個靜態(tài)類中,Where方法的實現(xiàn)代碼如下:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
if (source == null) throw Error.ArgumentNull("source");
if (predicate == null) throw Error.ArgumentNull("predicate");
if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate);
if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate);
if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate);
return new WhereEnumerableIterator<TSource>(source, predicate);
}
很顯然,M$會用到 source 的類型,根據(jù)不同的類型返回不同的 WhereXXXIterator。等等,這就意味著Where方法返回的不是IEnumerable。從這里我們就可以清晰地看到M$其實是包裝了一層,那么顯而易見,應該是只記錄了一個委托。這些WhereXXXIterator都是派生自 Iterator 抽象類,這個類實現(xiàn)了 IEnumerable<TSource> 和 IEnumerator<TSource> 這兩個接口,這樣用戶就能鏈式地去調用。不過, Iterator 類不是public的,所以用戶只知道是一個 IEnumerable 的類型。這樣做的好處是可以向用戶隱藏一些底層實現(xiàn)的細節(jié),顯得類庫用起來很簡單;壞處是可能會導致用戶的使用方式不合理,以及一些較難理解的問題。
我們暫時不看 Iterator 類的一些細節(jié),繼續(xù)看 WhereListIterator 的 Where 方法。這個方法在基類是抽象的,因此在這里實現(xiàn)它:
public override IEnumerable<TSource> Where(Func<TSource, bool> predicate) {
return new WhereListIterator<TSource>(source, CombinePredicates(this.predicate, predicate));
}
CombinePredicates是Enumerable靜態(tài)類提供的擴展方法,不過它不是public的,只有在內部才能訪問:
static Func<TSource, bool> CombinePredicates<TSource>(Func<TSource, bool> predicate1, Func<TSource, bool> predicate2) {
return x => predicate1(x) && predicate2(x);
}
自然,WhereListIterator 有幾個字段:
List<TSource> source;
Func<TSource, bool> predicate;
List<TSource>.Enumerator enumerator;
這樣,相信大家都已經(jīng)知道了Where的工作原理,簡單地總結一下:
1.當我們創(chuàng)建了一個 List 后,調用其定義在 IEnumerable 接口上的 Where 擴展方法,系統(tǒng)會生成一個 WhereListIterator 的對象。這個對象把 Where 子句的 predicate 委托保存并返回。
2.再次調用 Where 子句時,對象其實已經(jīng)變成 WhereListIterator類型,此后再次調用 Where 方法時,會調用 WhereListIterator.Where 方法,這個方法把兩個 predicate 合并,之后返回一個新的 WhereListIterator。
3.之后的每一次 Where 調用都是執(zhí)行第2步操作。
可以看出,在調用 Where 方法時,系統(tǒng)只是記錄了 predicate 委托,并沒有回調這些委托,所以此時自然而然就不會產生新的列表。
當遇到foreach語句時,會需要生成一個 IEnumerator 類型的對象以便枚舉,此時就開始調用 Iterator 的 GetEnumerator 方法。這個方法只有在基類中定義:
public IEnumerator<TSource> GetEnumerator() {
if (threadId == Thread.CurrentThread.ManagedThreadId && state == 0) {
state = 1;
return this;
}
Iterator<TSource> duplicate = Clone();
duplicate.state = 1;
return duplicate;
}
在獲取迭代器的時候要考慮并發(fā)的問題,如果多個線程都在枚舉元素,同時使用一個迭代器肯定會發(fā)生混亂。M$的實現(xiàn)方法很聰明,對于同一個線程只使用一個迭代器,當發(fā)現(xiàn)是另一個線程調用的時候直接克隆一個。
MoveNext方法在子類中定義,WhereListIterator的實現(xiàn)如下:
public override bool MoveNext() {
switch (state) {
case 1:
enumerator = source.GetEnumerator();
state = 2;
goto case 2;
case 2:
while (enumerator.MoveNext()) {
TSource item = enumerator.Current;
if (predicate(item)) {
current = item;
return true;
}
}
Dispose();
break;
}
return false;
}
switch語句寫得不容易看懂。在獲取迭代器后,逐個進行 predicate 回調,返回滿足條件的第一個元素。當遍歷結束后,如果迭代器實現(xiàn)了 IDispose 接口,就調用其 Dispose 方法釋放非托管資源。之后設置基類的 state 屬性為-1,這樣今后就訪問不到這個迭代器了,需要重新創(chuàng)建一個。
至此,終于看到只有在迭代時才進行計算的緣由了。其他的一些Iterator大體上都是類似的,只是MoveNext的實現(xiàn)方式不一樣罷了。至于M$為什么要單獨為 List 和 Array 寫一個單獨的類,對于數(shù)組來說可以直接根據(jù)下標訪問下一個元素,這樣就可以避免訪問迭代器的 MoveNext 方法,可以提高一點效率。但對于列表來說,其實現(xiàn)方式和普通的類相同,估計是首先想使用不同的實現(xiàn)后來發(fā)現(xiàn)不好吧。
其他的擴展方法,比如Select、Repeat、Reverse、OrderBy之類的好像也能鏈式調用,并且可以不限順序任意調用多次。這又是怎么實現(xiàn)的呢?
我們先來看Select方法。類似Where方法,Select也定義了對應的三個Iterator:WhereSelectListIterator、WhereSelectArrayIterator和WhereSelectEnumerableIterator。每一種都定義了Select和Where方法:
public override IEnumerable<TResult2> Select<TResult2>(Func<TResult, TResult2> selector) {
return new WhereSelectListIterator<TSource, TResult2>(source, predicate, CombineSelectors(this.selector, selector));
}
public override IEnumerable<TResult> Where(Func<TResult, bool> predicate) {
return new WhereEnumerableIterator<TResult>(this, predicate);
}
CombineSelectors的代碼如下:
static Func<TSource, TResult> CombineSelectors<TSource, TMiddle, TResult>(Func<TSource, TMiddle> selector1, Func<TMiddle, TResult> selector2) {
return x => selector2(selector1(x));
}
這樣子就把Select和Where連起來了。本質上,運行時的類型在WhereXXXIterator和WhereSelectXXXIterator之間進行變換,每次都產生一個新的類型。
你可能會覺得對于每一種方法,M$都定義了一個專門的類,比如OrderByIterator等。但這樣做會引起類的爆炸,同時每一種Iterator為了兼容其他的類這樣要重復寫的東西簡直無法想象。微軟把這些函數(shù)分成了兩類,第一類是直接調用迭代器,列舉如下:
1.Reverse:生成一個Buffer對象,倒序輸入后返回 IEnumerable 類型的迭代器。
2.Cast:以object類型取迭代器中的元素并轉型yield return。
3.Union、Ditinct:生成一個Set類型的對象,這個對象會訪問迭代器。
4.Concat、Zip、Take、TakeWhile、Skip、SkipWhile:yield return。
很顯然,調用這些方法會導致訪問迭代器,這樣 predicate 和 selector 就會開始進行回調(如果是WhereXXXIterator或WhereSelectXXXIterator類型的話)。當然,訪問聚集函數(shù)或者First之類的方法顯而易見會導致列表進行迭代,這里不多說明了。
第二種就是微軟進行特殊處理的 Join、GroupBy、OrderBy、ThenBy。這幾個方法是 LINQ 中的核心,偷懶怎么行?我已經(jīng)寫累了,相信各位看官也累了。但是求知心怎么會允許我們休息呢?繼續(xù)往下看吧。
先從最熟悉的排序開始。OrderBy方法最簡單的重載如下(順帶一提,方法簽名看似非常復雜,其實使用起來很簡單,因為Visual Studio會自動幫你匹配泛型參數(shù),比如 goods = goods.OrderBy(g => g.GoodsName);):
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
哇塞,返回值終于不是IEnumerable了,這個IOrderedEnumerable很明顯也是IEnumerable繼承過來的。在實現(xiàn)上,OrderedEnumerable<TSource>是一個實現(xiàn)了該方法的抽象類,OrderedEnumerable<TSource, TKey>繼承自此類,這兩個類都不對外公開。但微軟又公開了接口,這不是很奇怪么?難道是可以讓用戶自行擴展?這點暫時不深究了。
OrderBy擴展方法會返回一個OrderedEnumerable類型的對象,這個類對外公開了 GetEnumerator 方法:
public IEnumerator<TElement> GetEnumerator() {
Buffer<TElement> buffer = new Buffer<TElement>(source);
if (buffer.count > 0) {
EnumerableSorter<TElement> sorter = GetEnumerableSorter(null);
int[] map = sorter.Sort(buffer.items, buffer.count);
sorter = null;
for (int i = 0; i < buffer.count; i++) yield return buffer.items[map[i]];
}
}
OK,重點來了:OrderBy也是進行延時操作!也就是說直到調用 GetEnumerator 之前,還是不會回調前面的 predicate 和 selector。這里的排序算法只是一個簡單的快速排序算法,由于不是重點,代碼省略。
到這里估計有些人已經(jīng)暈了,所以需要再次進行總結。用一個例子來說明,假如我寫了如下這樣的代碼,應該是怎么工作的呢(代碼僅僅是為了說明,沒有實際的意義)?
goods = goods.OrderBy(g => g.GoodsName);
goods.Where(g => g.GoodsName.Length < 10);
執(zhí)行完第一句代碼后,類型變成了 OrderedEnumerable ,那么又來一個 Where,情況會怎么樣呢?
由于 OrderedEnumerable 沒有定義 Where 方法,那么又會調用 IEnumerable 的 Where 方法。此時會發(fā)生什么呢?由于類型不是 WhereXXXIterator,那么…… 對!那么會生成一個 WhereEnumerableIterator,此時 List 這個信息就已經(jīng)丟失了。
有個疑問,我接下來再次調用 Where,此時這個 Where 語句并不知道之前的一些 predicate,在接下來的迭代過程中,怎么進行回調呢?
不要忘了,每一個類似這種類型(Enumerable、Iterator),都有一個 source 字段,這個字段就是鏈式調用的關鍵。OrderedEnumerable 類型對象在初始的過程中記錄了 WhereListIterator 這個類型對象的引用并存入 source 字段中,在接下來的 Where 調用里,新生成的 WhereEnumerableIterator 類型對象中,又將 OrdredEnumerable 類型的對象存入 source 中。之后在枚舉的過程中,會按照如下步驟開始執(zhí)行:
1.枚舉時類型是 WhereEnumerableIterator,進行枚舉時,首先要得到這個對象的 Enumerator。此時系統(tǒng)調用 source 字段的 GetEnumerator。正是那個不太好理解的 switch 語句,曾經(jīng)一度被我們忽略的 source.GetEnumerator() 在此起了重要的作用。
2.source 字段存儲的是 OrderedEnumerator 類型的對象,我們參考這個對象的 GetEnumerator 方法(就是上面那個帶 Buffer 的),發(fā)現(xiàn)它會調用 Buffer 的構造方法將數(shù)據(jù)填入緩沖區(qū)。Buffer 的構造方法代碼我沒有列出,但是其肯定是調用其 source 的枚舉器(事實上如果是集合會調用其 CopyTo)。
3.這時 source 字段存儲的是 WhereListIterator 類型對象,這個類的行為在最開始我們分析過:逐個回調 predicate 和 selector 并 yield return。
4.最后,前面的迭代器生成了,在 MoveNext 的過程中,首先回調 WhereEumerableIterator 的委托,再繼續(xù)取 OrderedEnumerable 的元素,直至完成。
看,一切都是如此地“順理成章”。都是歸功于 source 字段。至此,我們已經(jīng)幾乎了解了 IEnumerable 的全部玄機。
對了,還有 GroupBy 和 Join 沒有進行說明。在此簡單提一下。
這兩個方法的基礎是一個稱之為 LookUp 的類。LookUp表示一個鍵到多個值的集合(比較Dictionary),在實現(xiàn)上是一個哈希表對應到可以擴容的數(shù)組。GroupBy 和 Join 借助 LookUp 實現(xiàn)對元素的分組與關聯(lián)操作。GroupBy 語句使用了 GroupEnumerator,其原理和上面所述的 OrderedEnumerator 類似,在此不再贅述。如果對 GroupBy 和 Join 的具體實現(xiàn)感興趣,可以自行參看源代碼。
好了,這次關于 IEnumerable 的研究總算告一段落了,我也總算是弄清了其工作原理,解答了心中的疑慮。另外可以看到,在研究的過程中要有耐心,這樣事情才會越來越明朗的。
相關文章
C#實現(xiàn)按數(shù)據(jù)庫郵件列表發(fā)送郵件的方法
這篇文章主要介紹了C#實現(xiàn)按數(shù)據(jù)庫郵件列表發(fā)送郵件的方法,涉及C#讀取數(shù)據(jù)庫及通過自定義函數(shù)發(fā)送郵件的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-07-07c#使用FreeSql生產環(huán)境時自動升級備份數(shù)據(jù)庫
使用FreeSql,包含所有的ORM數(shù)據(jù)庫,都會存在這樣的問題。在codefirst模式下,根據(jù)代碼自動更新數(shù)據(jù)庫,都建議不要在生產環(huán)境使用。因為容易丟失數(shù)據(jù),本文提供一種自動更新數(shù)據(jù)庫的解決的思路:在判斷需要升級時,才自動升級,同時升級前先備份數(shù)據(jù)庫2021-06-06c#生成excel示例sql數(shù)據(jù)庫導出excel
這篇文章主要介紹了c#操作excel的示例,里面的方法可以直接導出數(shù)據(jù)到excel,大家參考使用吧2014-01-01