C#中的集合表現爲數組和若干集合類。無論是數組仍是集合類,它們都有各自的優缺點。如何使用好集合是咱們在開發過程當中必須掌握的技巧。不要小看這些技巧,一旦在開發中使用了錯誤的集合或針對集合的方法,應用程序將會背離你的預想而運行。算法
在C#中,數組一旦被建立,長度就不能改變。若是咱們須要一個動態且可變長度的集合,就應該使用ArrayList或List<T>來建立。而數組自己,尤爲是一維數組,在遇到要求高效率的算法時,則會專門被優化以提高其效率。一維數組也稱爲向量,其性能是最佳的,在IL中使用了專門的指令來處理它們(如newarr、ldelem、ldelema、ldlen和stelem)。數據庫
從內存使用的角度來說,數組在建立時被分配了一段固定長度的內存。若是數組的元素是值類型,則每一個元素的長度等於相應的值類型的長度;若是數組的元素是引用類型,則每一個元素的長度爲該引用類型的IntPtr.Size。數組的存儲結構一旦被分配,就不能再變化。而ArrayList是數組結構,能夠動態地增減內存空間,若是ArrayList存儲的是值類型,則會爲每一個元素增長12字節的空間,其中4字節用於對象引用,8字節是元素裝箱時引入的對象頭。List<T>是ArrayList的泛型實現,它省去了拆箱和裝箱帶來的開銷。編程
注意 c#
因爲數組自己在內存上的特色,所以在使用數組的過程當中還應該注意大對象的問題。所謂「大對象」,是指那些佔用內存超過85 000字節的對象,它們被分配在大對象堆裏。大對象的分配和回收與小對象相比,都不太同樣,尤爲是回收,大對象在回收過程當中會帶來效率很低的問題。因此,不能肆意對數組指定過大的長度,這會讓數組成爲一個大對象。設計模式
採用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還有兩個優點安全
foreach存在的一個問題是:它不支持循環時對集合進行增刪操做。 取而代之的方法是使用for循環。數據結構
不支持緣由:多線程
foreach循環使用了迭代器進行集合的遍歷,它在FCL提供的迭代器內部維護了一個對集合版本的控制。那麼什麼是集合版本?簡單來講,其實它就是一個整型的變量,任何對集合的增刪操做都會使版本號加1。foreach循環會調用MoveNext方法來遍歷元素,在MoveNext方法內部會進行版本號的檢測,一旦檢測到版本號有變更,就會拋出InvalidOperationException異常。性能
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代碼也是差很少的。
舉例:
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)); }
注意,非泛型集合在System.Collections命名空間下,對應的泛型集合則在System.Collections.Generic命名空間下。
泛型的好處不言而喻,,若是對大型集合進行循環訪問、轉型或拆箱和裝箱操做,使用ArrayList這樣的傳統集合對效率的影響會很是大。鑑於此,微軟提供了對泛型的支持。泛型使用一對<>括號將實際的類型括起來,而後編譯器和運行時會完成剩餘的工做。
要選擇正確的集合,首先須要瞭解一些數據結構的知識。所謂數據結構,就是相互之間存在一種或多種特定關係的數據元素的集合
說明
直接存儲結構的優勢是:向數據結構中添加元素是很高效的,直接放在數據末尾的第一個空位上就能夠了。它的缺點是:向集合插入元素將會變得低效,它須要給插入的元素騰出位置並順序移動後面的元素。
若是集合的數目固定而且不涉及轉型,使用數組效率高,不然就使用List<T>(該使用數組的時候,仍是要使用數組)
順序存儲結構,即線性表。線性表可動態地擴大和縮小,它在一片連續的區域中存儲數據元素。線性表不能按照索引進行查找,它是經過對地址的引用來搜索元素的,爲了找到某個元素,它必須遍歷全部元素,直到找到對應的元素爲止。因此,線性表的優勢是插入和刪除數據效率高,缺點是查找的效率相對來講低一些。
隊列Queue<T>遵循的是先入先出的模式,它在集合末尾添加元素,在集合的起始位置刪除元素。
棧Stack<T>遵循的是後入先出的模式,它在集合末尾添加元素,同時也在集合末尾刪除元素。
字典Dictionary<TKey, TValue>存儲的是鍵值對,值在基於鍵的散列碼的基礎上進行存儲。字典類對象由包含集合元素的存儲桶組成,每個存儲桶與基於該元素的鍵的哈希值關聯。若是須要根據鍵進行值的查找,使用Dictionary<TKey, TValue>將會使搜索和檢索更快捷。
雙向鏈表LinkedList<T>是一個類型爲LinkedListNode的元素對象的集合。當咱們以爲在集合中插入和刪除數據很慢時,就能夠考慮使用鏈表。若是使用LinkedList<T>,咱們會發現此類型並無其餘集合廣泛具備的Add方法,取而代之的是AddAfter、AddBefore、AddFirst、AddLast等方法。雙向鏈表中的每一個節點都向前指向Previous節點,向後指向Next節點。
在FCL中,非線性集合實現得很少。非線性集合分爲層次集合和組集合。層次集合(如樹)在FCL中沒有實現。組集合又分爲集和圖,集在FCL中實現爲HashSet<T>,而圖在FCL中也沒有對應的實現。
集的概念本意是指存放在集合中的元素是無序的且不能重複的。
除了上面提到的集合類型外,還有其餘幾個要掌握的集合類型,它們是在實際應用中發展而來的對以上基礎類型的擴展:SortedList<T>、SortedDictionary<TKey, TValue>、Sorted-Set<T>。它們所擴展的對應類分別爲List<T>、Dictionary<TKey, TValue>、HashSet<T>,做用是將本來無序排列的元素變爲有序排列。
FCL集合圖以下:
集合線程安全是指在多個線程上添加或刪除元素時,線程之間必須保持同步。
泛型集合通常經過加鎖來進行安全鎖定,以下:
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); } } }
若是要實現一個自定義的集合類,不該該以一個FCL集合類爲基類,而應該擴展相應的泛型接口。FCL集合類應該以組合的形式包含至自定義的集合類,需擴展的泛型接口一般是IEnumer-able<T>和ICollection<T>(或ICollection<T>的子接口,如IList<T>),前者規範了集合類的迭代功能,後者則規範了一個集合一般會有的操做。
List<T>基本上沒有提供可供子類使用的protected成員(從object中繼承來的Finalize方法和Member-wiseClone方法除外),也就是說,實際上,繼承List<T>並無帶來任何繼承上的優點,反而喪失了面向接口編程帶來的靈活性。並且,稍加不注意,隱含的Bug就會接踵而至。
FCL中的迭代器只有GetEnumerator方法,沒有SetEnumerator方法。全部的集合類也沒有一個可寫的迭代器屬性。
緣由有二
若是類型的屬性中有集合屬性,那麼應該保證屬性對象是由類型自己產生的。若是將屬性設置爲可寫,則會增長拋出異常的概率。通常狀況下,若是集合屬性沒有值,則它返回的Count等於0,而不是集合屬性的值爲null。
從.NET 3.0開始,C#開始支持一個新特性:匿名類型。匿名類型由var、賦值運算符和一個非空初始值(或以new開頭的初始化項)組成。匿名類型有以下的基本特性:
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表達式其實就是一個簡潔的委託,運算符「=>」左邊表明的是方法的參數,右邊的是方法體。
樣例以下:
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則會合並兩次查詢並生成一個最終的查詢。
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>查詢的時候,若是使用自定義的方法,則會拋出異常。
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的泛型集合提供擴展方法
public static IOrderedEnumerable<TSource>OrderBy<TSource,TKey>(this IEnumerable<TSource>source,Func<TSource,TKey>keySelector){ //省略}
與First方法相似的還有Take方法,Take方法接收一個整型參數,而後爲咱們返回該參數指定的元素個數。與First同樣,它在知足條件之後,會從當前的迭代過程直接返回,而不是等到整個迭代過程完畢再返回。若是一個集合包含了不少的元素,那麼這種查詢會爲咱們帶來可觀的時間效率。
會運用First和Take等方法,都會讓咱們避免全集掃描,大大提升效率。
若有須要, 上一篇的《c#規範整理·語言要素》也能夠看看!