C#規範整理·集合和Linq

LINQ(Language Integrated Query,語言集成查詢)提供了相似於SQL的語法,能對集合進行遍歷、篩選和投影。一旦掌握了LINQ,你就會發如今開發中再也離不開它。
  開始!

前言

  C#中的集合表現爲數組和若干集合類。無論是數組仍是集合類,它們都有各自的優缺點。如何使用好集合是咱們在開發過程當中必須掌握的技巧。不要小看這些技巧,一旦在開發中使用了錯誤的集合或針對集合的方法,應用程序將會背離你的預想而運行。算法

正文

1.元素數量可變的狀況下不該使用數組

  在C#中,數組一旦被建立,長度就不能改變。若是咱們須要一個動態且可變長度的集合,就應該使用ArrayList或List<T>來建立。而數組自己,尤爲是一維數組,在遇到要求高效率的算法時,則會專門被優化以提高其效率。一維數組也稱爲向量,其性能是最佳的,在IL中使用了專門的指令來處理它們(如newarr、ldelem、ldelema、ldlen和stelem)。數據庫

  從內存使用的角度來說,數組在建立時被分配了一段固定長度的內存。若是數組的元素是值類型,則每一個元素的長度等於相應的值類型的長度;若是數組的元素是引用類型,則每一個元素的長度爲該引用類型的IntPtr.Size。數組的存儲結構一旦被分配,就不能再變化。而ArrayList是數組結構,能夠動態地增減內存空間,若是ArrayList存儲的是值類型,則會爲每一個元素增長12字節的空間,其中4字節用於對象引用,8字節是元素裝箱時引入的對象頭。List<T>是ArrayList的泛型實現,它省去了拆箱和裝箱帶來的開銷。編程

注意 c#

因爲數組自己在內存上的特色,所以在使用數組的過程當中還應該注意大對象的問題。所謂「大對象」,是指那些佔用內存超過85 000字節的對象,它們被分配在大對象堆裏。大對象的分配和回收與小對象相比,都不太同樣,尤爲是回收,大對象在回收過程當中會帶來效率很低的問題。因此,不能肆意對數組指定過大的長度,這會讓數組成爲一個大對象。設計模式

  1. 若是必定要動態改變數組的長度,一種方法是將數組轉換爲ArrayList或List<T>,須要擴容時,內部數組將自動翻倍擴容
  2. 還有一種方法是用數組的複製功能。數組繼承自System.Array,抽象類System.Array提供了一些有用的實現方法,其中就包含了Copy方法,它負責將一個數組的內容複製到另一個數組中。不管是哪一種方法,改變數組長度就至關於從新建立了一個數組對象。

2.多數狀況下使用foreach進行循環遍歷

採用foreach最大限度地簡化了代碼。它用於遍歷一個繼承了IEmuerable或IEmuerable<T>接口的集合元素。藉助於IL代碼能夠看到foreach仍是本質就是利用了迭代器來進行集合遍歷。以下:數組

List<object>list=new List<object>();
 using(List<object>.Enumerator CS$5$0000=list.GetEnumerator())
 {    
    while(CS$5$0000.MoveNext()) 
   {      
      object current=CS$5$0000.Current; 
   }
}

除了代碼簡潔以外,foreach還有兩個優點安全

  • 自動將代碼置入try-finally塊
  • 若類型實現了IDispose接口,它會在循環結束後自動調用Dispose方法。

3.foreach不能代替for

  • foreach存在的一個問題是:它不支持循環時對集合進行增刪操做。 取而代之的方法是使用for循環。數據結構

    不支持緣由:多線程

  1. foreach循環使用了迭代器進行集合的遍歷,它在FCL提供的迭代器內部維護了一個對集合版本的控制。那麼什麼是集合版本?簡單來講,其實它就是一個整型的變量,任何對集合的增刪操做都會使版本號加1。foreach循環會調用MoveNext方法來遍歷元素,在MoveNext方法內部會進行版本號的檢測,一旦檢測到版本號有變更,就會拋出InvalidOperationException異常。性能

  2. 若是使用for循環就不會帶來這樣的問題。for直接使用索引器,它不對集合版本號進行判斷,因此不存在由於集合的變更而帶來的異常(固然,超出索引長度這種狀況除外)。
public bool MoveNext()
{   
  List<T>list=this.list;   
  if((this.version==list._version)&&(this.index<list._size))   
  {        
     this.current=list._items[this.index];   
     this.index++;     
     return true;    
 }    
  return this.MoveNextRare();
}

不管是for循環仍是foreach循環,內部都是對該數組的訪問,而迭代器僅僅是多進行了一次版本檢測。事實上,在循環內部,二者生成的IL代碼也是差很少的。

4.使用更有效的對象和集合初始化

舉例:

class Program {    
static void Main(string[]args)    
  {       
 Person person=new Person(){Name="Mike",Age=20};    
  }
}

class Person
{    
public string Name{get;set;} 
public int Age{get;set;}
}

對象初始化設定項支持在大括號中對自動實現的屬性進行賦值。以往只能依靠構造方法傳值進去,或者在對象構造完畢後對屬性進行賦值。如今這些步驟簡化了,初始化設定項實際至關於編譯器在對象生成後對屬性進行了賦值。

集合初始化也一樣進行了簡化:

List<Person>personList=new List<Person>( )
{   
     new Person() {Name="Rose",Age=19},    
     mike,    
     null
};

重點:初始化設定項毫不僅僅是爲了對象和集合初始化的方便,它更重要的做用是爲LINQ查詢中的匿名類型進行屬性的初始化。因爲LINQ查詢返回的集合中匿名類型的屬性都是隻讀的,若是須要爲匿名類型屬性賦值,或者增長屬性,只能經過初始化設定項來進行。初始化設定項還能爲屬性使用表達式。
舉例

List<Person>personList2=new List<Person>()
{    
   new Person(){Name="Rose",Age=19},   
   new Person(){Name="Steve",Age=45}, 
   new Person(){Name="Jessica",Age=20}
};

var pTemp=from p in personList2 
         select new {p.Name,    AgeScope=p.Age>20?"Old":"Young"};

foreach(var item in pTemp) 
{   
 Console.WriteLine(string.Format("{0}:{1}",item.Name,item.AgeScope));
}

5.使用泛型集合代替非泛型集合

注意,非泛型集合在System.Collections命名空間下,對應的泛型集合則在System.Collections.Generic命名空間下。

泛型的好處不言而喻,,若是對大型集合進行循環訪問、轉型或拆箱和裝箱操做,使用ArrayList這樣的傳統集合對效率的影響會很是大。鑑於此,微軟提供了對泛型的支持。泛型使用一對<>括號將實際的類型括起來,而後編譯器和運行時會完成剩餘的工做。

6.選擇正確的集合

要選擇正確的集合,首先須要瞭解一些數據結構的知識。所謂數據結構,就是相互之間存在一種或多種特定關係的數據元素的集合

說明

  1. 直接存儲結構的優勢是:向數據結構中添加元素是很高效的,直接放在數據末尾的第一個空位上就能夠了。它的缺點是:向集合插入元素將會變得低效,它須要給插入的元素騰出位置並順序移動後面的元素。
    若是集合的數目固定而且不涉及轉型,使用數組效率高,不然就使用List<T>(該使用數組的時候,仍是要使用數組)

  2. 順序存儲結構,即線性表。線性表可動態地擴大和縮小,它在一片連續的區域中存儲數據元素。線性表不能按照索引進行查找,它是經過對地址的引用來搜索元素的,爲了找到某個元素,它必須遍歷全部元素,直到找到對應的元素爲止。因此,線性表的優勢是插入和刪除數據效率高,缺點是查找的效率相對來講低一些。

  3. 隊列Queue<T>遵循的是先入先出的模式,它在集合末尾添加元素,在集合的起始位置刪除元素。

  4. 棧Stack<T>遵循的是後入先出的模式,它在集合末尾添加元素,同時也在集合末尾刪除元素。

  5. 字典Dictionary<TKey, TValue>存儲的是鍵值對,值在基於鍵的散列碼的基礎上進行存儲。字典類對象由包含集合元素的存儲桶組成,每個存儲桶與基於該元素的鍵的哈希值關聯。若是須要根據鍵進行值的查找,使用Dictionary<TKey, TValue>將會使搜索和檢索更快捷。

  6. 雙向鏈表LinkedList<T>是一個類型爲LinkedListNode的元素對象的集合。當咱們以爲在集合中插入和刪除數據很慢時,就能夠考慮使用鏈表。若是使用LinkedList<T>,咱們會發現此類型並無其餘集合廣泛具備的Add方法,取而代之的是AddAfter、AddBefore、AddFirst、AddLast等方法。雙向鏈表中的每一個節點都向前指向Previous節點,向後指向Next節點。

  7. 在FCL中,非線性集合實現得很少。非線性集合分爲層次集合和組集合。層次集合(如樹)在FCL中沒有實現。組集合又分爲集和圖,集在FCL中實現爲HashSet<T>,而圖在FCL中也沒有對應的實現。
    集的概念本意是指存放在集合中的元素是無序的且不能重複的。

  8. 除了上面提到的集合類型外,還有其餘幾個要掌握的集合類型,它們是在實際應用中發展而來的對以上基礎類型的擴展:SortedList<T>、SortedDictionary<TKey, TValue>、Sorted-Set<T>。它們所擴展的對應類分別爲List<T>、Dictionary<TKey, TValue>、HashSet<T>,做用是將本來無序排列的元素變爲有序排列。

  9. 除了排序上的需求增長了上面3個集合類外,在命名空間System.Collections.Concurrent下,還涉及幾個多線程集合類。它們主要是:
    • ConcurrentBag<T>對應List<T>
    • ConcurrentDictionary<TKey, TValue>對應Dictionary<TKey, TValue>
    • ConcurrentQueue<T>對應Queue<T>
    • ConcurrentStack<T>對應Stack<T>

FCL集合圖以下:

7.確保集合的線程安全

集合線程安全是指在多個線程上添加或刪除元素時,線程之間必須保持同步。
泛型集合通常經過加鎖來進行安全鎖定,以下:

static object sycObj=new object();
static void Main(string[]args)
{
//object sycObj=new object();
Thread t1=new Thread(()=>{

//確保等待t2開始以後才運行下面的代碼
autoSet.WaitOne();
lock(sycObj)
{
    foreach(Person item in list)
    {
        Console.WriteLine("t1:"+item.Name);
        Thread.Sleep(1000);
    }
}
}

8.避免將List<T>做爲自定義集合類的基類

若是要實現一個自定義的集合類,不該該以一個FCL集合類爲基類,而應該擴展相應的泛型接口。FCL集合類應該以組合的形式包含至自定義的集合類,需擴展的泛型接口一般是IEnumer-able<T>和ICollection<T>(或ICollection<T>的子接口,如IList<T>),前者規範了集合類的迭代功能,後者則規範了一個集合一般會有的操做。

List<T>基本上沒有提供可供子類使用的protected成員(從object中繼承來的Finalize方法和Member-wiseClone方法除外),也就是說,實際上,繼承List<T>並無帶來任何繼承上的優點,反而喪失了面向接口編程帶來的靈活性。並且,稍加不注意,隱含的Bug就會接踵而至。

9.迭代器應該是隻讀的

FCL中的迭代器只有GetEnumerator方法,沒有SetEnumerator方法。全部的集合類也沒有一個可寫的迭代器屬性。

緣由有二

  1. 這違背了設計模式中的開閉原則。被設置到集合中的迭代器可能會直接致使集合的行爲發生異常或變更。一旦確實須要新的迭代需求,徹底能夠建立一個新的迭代器來知足需求,而不是爲集合設置該迭代器,由於這樣作會直接致使使用到該集合對象的其餘迭代場景發生不可知的行爲。
  2. 如今,咱們有了LINQ。使用LINQ能夠不用建立任何新的類型就能知足任何的迭代需求。

10.謹慎集合屬性的可寫操做

若是類型的屬性中有集合屬性,那麼應該保證屬性對象是由類型自己產生的。若是將屬性設置爲可寫,則會增長拋出異常的概率。通常狀況下,若是集合屬性沒有值,則它返回的Count等於0,而不是集合屬性的值爲null。

11.使用匿名類型存儲LINQ查詢結果(最佳搭檔)

從.NET 3.0開始,C#開始支持一個新特性:匿名類型。匿名類型由var、賦值運算符和一個非空初始值(或以new開頭的初始化項)組成。匿名類型有以下的基本特性:

  • 既支持簡單類型也支持複雜類型。簡單類型必須是一個非空初始值,複雜類型則是一個以new開頭的初始化項;
  • 匿名類型的屬性是隻讀的,沒有屬性設置器,它一旦被初始化就不可更改;
  • 若是兩個匿名類型的屬性值相同,那麼就認爲兩個匿名類型相等;
  • 匿名類型能夠在循環中用做初始化器;
  • 匿名類型支持智能感知;
  • 還有一點,雖然不經常使用,可是匿名類型確實也能夠擁有方法。

11. 在查詢中使用Lambda表達式

LINQ其實是基於擴展方法和Lambda表達式的,理解了這一點就不難理解LINQ。任何LINQ查詢都能經過調用擴展方法的方式來替代,以下面的代碼所示:

foreach(var item in personList.Select(person=>new{PersonName= person.Name,CompanyName=person.CompanyID==0?"Micro":"Sun"}))
{  

  Console.WriteLine(string.Format("{0}\t:{1}",item.PersonName, item.CompanyName));
}

針對LINQ設計的擴展方法大多應用了泛型委託。System命名空間定義了泛型委託Action、Func和Predicate。能夠這樣理解這三個委託:Action用於執行一個操做,因此它沒有返回值;Func用於執行一個操做並返回一個值;Predicate用於定義一組條件並判斷參數是否符合條件。Select擴展方法接收的就是一個Func委託,而Lambda表達式其實就是一個簡潔的委託,運算符「=>」左邊表明的是方法的參數,右邊的是方法體。

12.理解延遲求值和主動求值之間的區別

樣例以下:

List<int>list=new List<int>(){0,1,2,3,4,5,6,7,8,9};
var temp1=from c in list where c>5 select c;
var temp2=(from c in list where c>5 select c).ToList<int>();

在使用LINQ to SQL時,延遲求值可以帶來顯著的性能提高。舉個例子:若是定義了兩個查詢,並且採用延遲求值,CLR則會合並兩次查詢並生成一個最終的查詢。

13.區別LINQ查詢中的IEnumerable<T>和IQueryable<T>

LINQ查詢方法一共提供了兩類擴展方法,在System.Linq命名空間下,有兩個靜態類:Enumerable類,它針對繼承了IEnumerable<T>接口的集合類進行擴展;Queryable類,它針對繼承了IQueryable<T>接口的集合類進行擴展。稍加觀察咱們會發現,接口IQueryable<T>實際也是繼承了IEnumerable<T>接口的,因此,導致這兩個接口的方法在很大程度上是一致的。那麼,微軟爲何要設計出兩套擴展方法呢?

咱們知道,LINQ查詢從功能上來說實際上可分爲三類:LINQ to OBJECTS、LINQ to SQL、LINQ to XML(本建議不討論)。設計兩套接口的緣由正是爲了區別對待LINQ to OBJECTS、LINQ to SQL,二者對於查詢的處理在內部使用的是徹底不一樣的機制。針對LINQ to OBJECTS時,使用Enumerable中的擴展方法對本地集合進行排序和查詢等操做,查詢參數接受的是Func<>。Func<>叫作謂語表達式,至關於一個委託。針對LINQ toSQL時,則使用Queryable中的擴展方法,它接受的參數是Ex-pression<>。Expression<>用於包裝Func<>。LINQ to SQL引擎最終會將表達式樹轉化成爲相應的SQL語句,而後在數據庫中執行。

那麼,到底何時使用IQueryable<T>,何時使用IEnumerable<T>呢?簡單表述就是:本地數據源用IEnumerable<T>,遠程數據源用IQueryable<T>。

注意

在使用IQueryable<T>和IEnumerable<T>的時候還須要注意一點,IEnumerable<T>查詢的邏輯能夠直接用咱們本身所定義的方法,而IQueryable<T>則不能使用自定義的方法,它必須先生成表達式樹,查詢由LINQ to SQL引擎處理。在使用IQueryable<T>查詢的時候,若是使用自定義的方法,則會拋出異常。

13.使用LINQ取代集合中的比較器和迭代器

LINQ提供了相似於SQL的語法來實現遍歷、篩選與投影集合的功能。藉助於LINQ的強大功能,咱們經過兩條語句就能實現上述的排序要求。

var orderByBonus=from s in companySalary orderby s.Bonus select s;

foreach實際會隱含調用的是集合對象的迭代器。以往,若是咱們要繞開集合的Sort方法對集合元素按照必定的順序進行迭代,則須要讓類型繼承IEnumerable接口(泛型集合是IEnumerable<T>接口),實現一個或多個迭代器。如今從LINQ查詢生成匿名類型來看,至關於能夠無限爲集合增長迭代需求。

有了LINQ以後,咱們是否就再也不須要比較器和迭代器了呢?答案是否認的。咱們能夠利用LINQ的強大功能簡化本身的編碼,可是LINQ功能的實現自己就是藉助於FCL泛型集合的比較器、迭代器、索引器的。LINQ至關於封裝了這些功能,讓咱們使用起來更加方便。在命名空間System.Linq下存在不少靜態類,這些靜態類存在的意義就是爲FCL的泛型集合提供擴展方法

  • 強烈建議你利用LINQ所帶來的便捷性,但咱們仍需掌握比較器、迭代器、索引器的原理,以便更好地理解LINQ的思想,寫出更高質量的代碼。最好是能看懂Linq源碼。
public static IOrderedEnumerable<TSource>OrderBy<TSource,TKey>(this    IEnumerable<TSource>source,Func<TSource,TKey>keySelector){    //省略}

14.在LINQ查詢中避免沒必要要的迭代

  1. 好比常使用First()方法,First方法實際完成的工做是:搜索到知足條件的第一個元素,就從集合中返回。若是沒有符合條件的元素,它也會遍歷整個集合。
  2. 與First方法相似的還有Take方法,Take方法接收一個整型參數,而後爲咱們返回該參數指定的元素個數。與First同樣,它在知足條件之後,會從當前的迭代過程直接返回,而不是等到整個迭代過程完畢再返回。若是一個集合包含了不少的元素,那麼這種查詢會爲咱們帶來可觀的時間效率。

    會運用First和Take等方法,都會讓咱們避免全集掃描,大大提升效率。

總結

若有須要, 上一篇的《c#規範整理·語言要素》也能夠看看!

相關文章
相關標籤/搜索