程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#中的IEnumerable接口深刻研討

C#中的IEnumerable接口深刻研討

編輯:C#入門知識

C#中的IEnumerable接口深刻研討。本站提示廣大學習愛好者:(C#中的IEnumerable接口深刻研討)文章只能為提供參考,不一定能成為您想要的結果。以下是C#中的IEnumerable接口深刻研討正文


C#和VB.NET中的LINQ供給了一種與SQL查詢相似的“對象查詢”說話,關於熟習SQL說話的人來講除可以供給相似聯系關系、分組查詢的功效外,還能獲得編譯時檢討和Intellisense的支撐,應用Entity Framework更是可以或許主動為對象實體的查詢生成SQL語句,所以很受年夜中型信息體系設計者的喜愛。

IEnumerable這個接口可以說是為了這個特征“量身定制”,再加上微軟供給的擴大(Extension)辦法和Lambda表達式,給開辟者帶來了無限的方便。自己在比來的開辟任務中應用了年夜量的這類特征,同時在調試進程中還碰到了一個小成績,那末正好趁此機遇好好研討一下相干道理和完成。

先從一個實際的例子開端吧。假設我們要做一個商品檢索功效(這只是一個例子,我固然弗成能把公司的產物也營業在這裡貼出來),個中有一個檢索前提是可以指定廠家的稱號並停止隱約婚配。廠家的包含兩個稱號:注冊稱號和普通性稱號,我們只按普通性稱號停止檢索。固然你可以說直接用SQL查詢就好了,然則我們的體系是以實體對象為焦點停止設計的,廠家的數目也不會太多,年夜概1000條。為了不增長體系的龐雜性,只斟酌應用現有的數據拜訪層接口停止完成(按過濾前提獲得商品,和獲得一切廠商),這時候LINQ的便捷性就表現出來了。

借助IEnumerable接口和其幫助類,我們可以寫出以下代碼:

public GoodsListResponse GetGoodsList(GoodsListRequest request)
{
    //從數據庫中按商品種別獲得商品列表
    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這個接口,所完成的代碼遠比下面龐雜且好看。我們須要寫年夜量的foreach語句,並手工生成許多中央的 List 來赓續地挑選對象(你可以測驗考試把第二個if塊改寫成不消IEnumerable接口的情勢)。

看上去一切都很協調,然則下面的代碼有一個隱含的bug,這個bug也是明天上午困擾了我好久的一個成績。

運轉法式,當我不輸出廠商檢索前提的時刻,法式運轉是准確的。但當我輸出一個廠商的名字時,體系拋出了一個空援用的異常。咦?為何會有空援用呢?我輸出的廠商是數據庫中不存在的廠商,是以我認為成績可以出在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斷定其能否有元素,就挪用其無參數的Any辦法,這時候成績照舊:

聰慧的你確定曾經看出成績出在哪了,由於Visual Studio曾經提醒得很清晰了。但我其時還局限在“列表為空”這個框框中,是以遲遲不克不及發明緣由。失足是產生在 manufactures.Any() 這句話上,而我曾經斷定了它不為空啊,為何還會拋錯呢?

後來叫了一個同事幫我看,他說的四個字一會兒就提示了我“延遲盤算”。哦,對!我怎樣把這個特征給忘了。在最後的代碼中(就是沒有對 manufactures 為空停止斷定),失足是產生在 goods.ToList() 這句話時,而圖上的誰人代碼段失足是產生在挪用Any()辦法時(圖中的灰色部門),而我單步跟蹤到 Any() 這句話上時,失足的語句跳到 Where 子句(黃色部門),解釋曉得拜訪 Any 辦法時lambda表達式才被挪用。

那末很明顯是 Where 語句中這個 predicate 有成績:Manufacture的Name字段能夠為空(數據庫中存在如許的數據,所以招致在 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用得更多),就會對成果停止過濾,如今看來其實不是如許。

即便是應用 Where 等擴大辦法, 履行這些 predicate 的時光是在 foreach 和 ToList 的時刻才產生。

為何會如許呢?看模樣這完整不該該呀?Where子句的前往值就是一個IEnumerable的迭代器,按事理應當曾經挑選了對象啊?為了完全弄清晰這個成績,那末辦法很顯著——看 .NET 的源代碼。

Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 是它的辦法頭,在看源代碼之前,信任你曾經曉得微軟年夜概是怎樣完成的了:既然Where接收一個Func類型的拜托,而且都是在ToList 或許 foreach 的時刻盤算的,那末不言而喻完成應當是……

好了,來看下代碼吧。IEnumerable的擴大辦法都在 Enumerable 這個靜態類中,Where辦法的完成代碼以下:

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 的類型,依據分歧的類型前往分歧的 WhereXXXIterator。等等,這就意味著Where辦法前往的不是IEnumerable。從這裡我們便可以清楚地看到M$實際上是包裝了一層,那末不言而喻,應當是只記載了一個拜托。這些WhereXXXIterator都是派生自 Iterator 籠統類,這個類完成了 IEnumerable<TSource> 和 IEnumerator<TSource> 這兩個接口,如許用戶就可以鏈式地去挪用。不外, Iterator 類不是public的,所以用戶只曉得是一個  IEnumerable 的類型。如許做的利益是可以向用戶隱蔽一些底層完成的細節,顯得類庫用起來很簡略;害處是能夠會招致用戶的應用方法不公道,和一些較難懂得的成績。

我們臨時不看 Iterator 類的一些細節,持續看 WhereListIterator 的 Where 辦法。這個辦法在基類是籠統的,是以在這裡完成它:

public override IEnumerable<TSource> Where(Func<TSource, bool> predicate) {
    return new WhereListIterator<TSource>(source, CombinePredicates(this.predicate, predicate));
}

CombinePredicates是Enumerable靜態類供給的擴大辦法,不外它不是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;

如許,信任年夜家都曾經曉得了Where的任務道理,簡略地總結一下:


1.當我們創立了一個 List 後,挪用其界說在 IEnumerable 接口上的 Where 擴大辦法,體系會生成一個 WhereListIterator 的對象。這個對象把 Where 子句的 predicate 拜托保留並前往。

2.再次挪用 Where 子句時,對象其實曾經釀成 WhereListIterator類型,爾後再次挪用 Where 辦法時,會挪用 WhereListIterator.Where 辦法,這個辦法把兩個 predicate 歸並,以後前往一個新的 WhereListIterator。

3.以後的每次 Where 挪用都是履行第2步操作。

可以看出,在挪用 Where 辦法時,體系只是記載了 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;
}

在獲得迭代器的時刻要斟酌並發的成績,假如多個線程都在列舉元素,同時應用一個迭代器確定會產生凌亂。M$的完成辦法很聰慧,關於統一個線程只應用一個迭代器,當發明是另外一個線程挪用的時刻直接克隆一個。

MoveNext辦法在子類中界說,WhereListIterator的完成以下:

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 回調,前往知足前提的第一個元素。當遍歷停止後,假如迭代器完成了 IDispose 接口,就挪用其 Dispose 辦法釋放非托管資本。以後設置基類的 state 屬性為-1,如許往後就拜訪不到這個迭代器了,須要從新創立一個。

至此,終究看到只要在迭代時才停止盤算的啟事了。其他的一些Iterator年夜體上都是相似的,只是MoveNext的完成方法紛歧樣而已。至於M$為何要零丁為 List 和 Array 寫一個零丁的類,關於數組來講可以直接依據下標拜訪下一個元素,如許便可以免拜訪迭代器的 MoveNext 辦法,可以進步一點效力。但關於列表來講,其完成方法和通俗的類雷同,估量是起首想應用分歧的完成後來發明欠好吧。

其他的擴大辦法,好比Select、Repeat、Reverse、OrderBy之類的似乎也能鏈式挪用,而且可以不限次序隨意率性挪用屢次。這又是怎樣完成的呢?

我們先來看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為了兼容其他的類如許要反復寫的器械的確沒法想象。微軟把這些函數分紅了兩類,第一類是直接挪用迭代器,羅列以下:

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類型的話)。固然,拜訪集合函數或許First之類的辦法不言而喻會招致列表停止迭代,這裡不多解釋了。

第二種就是微軟停止特別處置的 Join、GroupBy、OrderBy、ThenBy。這幾個辦法是 LINQ 中的焦點,偷懶怎樣行?我曾經寫累了,信任列位看官也累了。然則求貼心怎樣會許可我們歇息呢?持續往下看吧。

先從最熟習的排序開端。OrderBy辦法最簡略的重載以下(順帶一提,辦法簽名看似異常龐雜,其實應用起來很簡略,由於Visual Studio會主動幫你婚配泛型參數,好比 goods = goods.OrderBy(g => g.GoodsName);):


public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);

哇塞,前往值終究不是IEnumerable了,這個IOrderedEnumerable很顯著也是IEnumerable繼續過去的。在完成上,OrderedEnumerable<TSource>是一個完成了該辦法的籠統類,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。這裡的排序算法只是一個簡略的疾速排序算法,因為不是重點,代碼省略。

到這裡估量有些人曾經暈了,所以須要再次停止總結。用一個例子來講明,假設我寫了以下如許的代碼,應當是怎樣任務的呢(代碼僅僅是為了解釋,沒有現實的意義)?

goods = goods.OrderBy(g => g.GoodsName);
goods.Where(g => g.GoodsName.Length < 10);

履行完第一句代碼後,類型釀成了 OrderedEnumerable ,那末又來一個 Where,情形會怎樣樣呢?

因為 OrderedEnumerable 沒有界說 Where 辦法,那末又會挪用 IEnumerable 的 Where 辦法。此時會產生甚麼呢?因為類型不是 WhereXXXIterator,那末…… 對!那末會生成一個 WhereEnumerableIterator,此時 List 這個信息就曾經喪失了。

有個疑問,我接上去再次挪用 Where,此時這個 Where 語句其實不曉得之前的一些 predicate,在接上去的迭代進程中,怎樣停止回調呢?

不要忘了,每個相似這類類型(Enumerable、Iterator),都有一個 source 字段,這個字段就是鏈式挪用的症結。OrderedEnumerable 類型對象在初始的進程中記載了 WhereListIterator 這個類型對象的援用並存入 source 字段中,在接上去的 Where 挪用裡,重生成的 WhereEnumerableIterator 類型對象中,又將 OrdredEnumerable 類型的對象存入 source 中。以後在列舉的進程中,會依照以下步調開端履行:

1.列舉時類型是 WhereEnumerableIterator,停止列舉時,起首要獲得這個對象的 Enumerator。此時體系挪用 source 字段的 GetEnumerator。恰是誰人不太好懂得的 switch 語句,已經一度被我們疏忽的 source.GetEnumerator() 在此起了主要的感化。

2.source 字段存儲的是 OrderedEnumerator 類型的對象,我們參考這個對象的 GetEnumerator 辦法(就是下面誰人帶 Buffer 的),發明它會挪用 Buffer 的結構辦法將數據填入緩沖區。Buffer 的結構辦法代碼我沒有列出,然則其確定是挪用其 source 的列舉器(現實上假如是聚集會挪用其 CopyTo)。

3.這時候 source 字段存儲的是 WhereListIterator 類型對象,這個類的行動在最開端我們剖析過:逐一回調 predicate 和 selector 並 yield return。
4.最初,後面的迭代器生成了,在 MoveNext 的進程中,起首回調 WhereEumerableIterator 的拜托,再持續取 OrderedEnumerable 的元素,直至完成。

看,一切都是如斯地“瓜熟蒂落”。都是歸功於 source 字段。至此,我們曾經簡直懂得了 IEnumerable 的全體玄機。

對了,還有 GroupBy 和 Join 沒有停止解釋。在此簡略提一下。

這兩個辦法的基本是一個稱之為 LookUp 的類。LookUp表現一個鍵到多個值的聚集(比擬Dictionary),在完成上是一個哈希表對應到可以擴容的數組。GroupBy 和 Join 借助 LookUp 完成對元素的分組與聯系關系操作。GroupBy 語句應用了 GroupEnumerator,其道理和下面所述的 OrderedEnumerator 相似,在此不再贅述。假如對 GroupBy 和 Join 的詳細完成感興致,可以自行參看源代碼。

好了,此次關於 IEnumerable 的研討總算告一段落了,我也總算是弄清了其任務道理,解答了心中的疑慮。別的可以看到,在研討的進程中要有耐煩,如許工作才會愈來愈晴明的。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved