作者:天空的湛藍(lán) 鏈接:https://www.cnblogs.com/zhan520g/p/11018163.html
LINQ(Language Integrated Query,語(yǔ)言集成查詢)提供了類似于SQL的語(yǔ)法,能對(duì)集合進(jìn)行遍歷、篩選和投影。一旦掌握了LINQ,你就會(huì)發(fā)現(xiàn)在開發(fā)中再也離不開它。
前言
C#中的集合表現(xiàn)為數(shù)組和若干集合類。不管是數(shù)組還是集合類,它們都有各自的優(yōu)缺點(diǎn)。如何使用好集合是我們?cè)陂_發(fā)過程中必須掌握的技巧。
不要小看這些技巧,一旦在開發(fā)中使用了錯(cuò)誤的集合或針對(duì)集合的方法,應(yīng)用程序?qū)?huì)背離你的預(yù)想而運(yùn)行。
1、元素?cái)?shù)量可變的情況下不應(yīng)使用數(shù)組
在C#中,數(shù)組一旦被創(chuàng)建,長(zhǎng)度就不能改變。如果我們需要一個(gè)動(dòng)態(tài)且可變長(zhǎng)度的集合,就應(yīng)該使用ArrayList或List<T>來(lái)創(chuàng)建。而數(shù)組本身,尤其是一維數(shù)組,在遇到要求高效率的算法時(shí),則會(huì)專門被優(yōu)化以提升其效率。
一維數(shù)組也稱為向量,其性能是最佳的,在IL中使用了專門的指令來(lái)處理它們(如newarr、ldelem、ldelema、ldlen和stelem)。
從內(nèi)存使用的角度來(lái)講,數(shù)組在創(chuàng)建時(shí)被分配了一段固定長(zhǎng)度的內(nèi)存。如果數(shù)組的元素是值類型,則每個(gè)元素的長(zhǎng)度等于相應(yīng)的值類型的長(zhǎng)度;如果數(shù)組的元素是引用類型,則每個(gè)元素的長(zhǎng)度為該引用類型的IntPtr.Size。
數(shù)組的存儲(chǔ)結(jié)構(gòu)一旦被分配,就不能再變化。而ArrayList是數(shù)組結(jié)構(gòu),可以動(dòng)態(tài)地增減內(nèi)存空間,如果ArrayList存儲(chǔ)的是值類型,則會(huì)為每個(gè)元素增加12字節(jié)的空間,其中4字節(jié)用于對(duì)象引用,8字節(jié)是元素裝箱時(shí)引入的對(duì)象頭。
List<T>是ArrayList的泛型實(shí)現(xiàn),它省去了拆箱和裝箱帶來(lái)的開銷。
注意
由于數(shù)組本身在內(nèi)存上的特點(diǎn),因此在使用數(shù)組的過程中還應(yīng)該注意大對(duì)象的問題。所謂“大對(duì)象”,是指那些占用內(nèi)存超過85 000字節(jié)的對(duì)象,它們被分配在大對(duì)象堆里。大對(duì)象的分配和回收與小對(duì)象相比,都不太一樣,尤其是回收,大對(duì)象在回收過程中會(huì)帶來(lái)效率很低的問題。所以,不能肆意對(duì)數(shù)組指定過大的長(zhǎng)度,這會(huì)讓數(shù)組成為一個(gè)大對(duì)象。
如果一定要?jiǎng)討B(tài)改變數(shù)組的長(zhǎng)度,一種方法是將數(shù)組轉(zhuǎn)換為ArrayList或List<T>,需要擴(kuò)容時(shí),內(nèi)部數(shù)組將自動(dòng)翻倍擴(kuò)容
還有一種方法是用數(shù)組的復(fù)制功能。數(shù)組繼承自System.Array,抽象類System.Array提供了一些有用的實(shí)現(xiàn)方法,其中就包含了方法,它負(fù)責(zé)將一個(gè)數(shù)組的內(nèi)容復(fù)制到另外一個(gè)數(shù)組中。無(wú)論是哪種方法,改變數(shù)組長(zhǎng)度就相當(dāng)于重新創(chuàng)建了一個(gè)數(shù)組對(duì)象。
2、多數(shù)情況下使用foreach進(jìn)行循環(huán)遍歷
采用foreach最大限度地簡(jiǎn)化了代碼。
它用于遍歷一個(gè)繼承了IEmuerable或IEmuerable<T>接口的集合元素。借助于IL代碼可以看到foreach還是本質(zhì)就是利用了迭代器來(lái)進(jìn)行集合遍歷。如下:
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;
}
}
除了代碼簡(jiǎn)潔之外,foreach還有兩個(gè)優(yōu)勢(shì)
自動(dòng)將代碼置入try-finally塊
若類型實(shí)現(xiàn)了IDispose接口,它會(huì)在循環(huán)結(jié)束后自動(dòng)調(diào)用Dispose方法。
3、foreach不能代替for
foreach存在的一個(gè)問題是:它不支持循環(huán)時(shí)對(duì)集合進(jìn)行增刪操作。取而代之的方法是使用for循環(huán)。
不支持原因:
foreach循環(huán)使用了迭代器進(jìn)行集合的遍歷,它在FCL提供的迭代器內(nèi)部維護(hù)了一個(gè)對(duì)集合版本的控制。那么什么是集合版本?簡(jiǎn)單來(lái)說,其實(shí)它就是一個(gè)整型的變量,任何對(duì)集合的增刪操作都會(huì)使版本號(hào)加1。foreach循環(huán)會(huì)調(diào)用MoveNext方法來(lái)遍歷元素,在MoveNext方法內(nèi)部會(huì)進(jìn)行版本號(hào)的檢測(cè),一旦檢測(cè)到版本號(hào)有變動(dòng),就會(huì)拋出InvalidOperationException異常。
如果使用for循環(huán)就不會(huì)帶來(lái)這樣的問題。for直接使用索引器,它不對(duì)集合版本號(hào)進(jìn)行判斷,所以不存在因?yàn)榧系淖儎?dòng)而帶來(lái)的異常(當(dāng)然,超出索引長(zhǎng)度這種情況除外)。
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();
}
無(wú)論是for循環(huán)還是foreach循環(huán),內(nèi)部都是對(duì)該數(shù)組的訪問,而迭代器僅僅是多進(jìn)行了一次版本檢測(cè)。事實(shí)上,在循環(huán)內(nèi)部,兩者生成的IL代碼也是差不多的。
4、使用更有效的對(duì)象和集合初始化
舉例:
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;}
}
對(duì)象初始化設(shè)定項(xiàng)支持在大括號(hào)中對(duì)自動(dòng)實(shí)現(xiàn)的屬性進(jìn)行賦值。以往只能依靠構(gòu)造方法傳值進(jìn)去,或者在對(duì)象構(gòu)造完畢后對(duì)屬性進(jìn)行賦值?,F(xiàn)在這些步驟簡(jiǎn)化了,初始化設(shè)定項(xiàng)實(shí)際相當(dāng)于編譯器在對(duì)象生成后對(duì)屬性進(jìn)行了賦值。
集合初始化也同樣進(jìn)行了簡(jiǎn)化:
List<Person>personList=new List<Person>( )
{
new Person() {Name='Rose',Age=19},
mike,
null
};
重點(diǎn):初始化設(shè)定項(xiàng)絕不僅僅是為了對(duì)象和集合初始化的方便,它更重要的作用是為L(zhǎng)INQ查詢中的匿名類型進(jìn)行屬性的初始化。由于LINQ查詢返回的集合中匿名類型的屬性都是只讀的,如果需要為匿名類型屬性賦值,或者增加屬性,只能通過初始化設(shè)定項(xiàng)來(lái)進(jìn)行。初始化設(shè)定項(xiàng)還能為屬性使用表達(dá)式。
舉例
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命名空間下,對(duì)應(yīng)的泛型集合則在System.Collections.Generic命名空間下。
泛型的好處不言而喻,,如果對(duì)大型集合進(jìn)行循環(huán)訪問、轉(zhuǎn)型或拆箱和裝箱操作,使用ArrayList這樣的傳統(tǒng)集合對(duì)效率的影響會(huì)非常大。鑒于此,微軟提供了對(duì)泛型的支持。泛型使用一對(duì)<>括號(hào)將實(shí)際的類型括起來(lái),然后編譯器和運(yùn)行時(shí)會(huì)完成剩余的工作。
6、選擇正確的集合
要選擇正確的集合,首先需要了解一些數(shù)據(jù)結(jié)構(gòu)的知識(shí)。所謂數(shù)據(jù)結(jié)構(gòu),就是相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合
說明
直接存儲(chǔ)結(jié)構(gòu)的優(yōu)點(diǎn)是:向數(shù)據(jù)結(jié)構(gòu)中添加元素是很高效的,直接放在數(shù)據(jù)末尾的第一個(gè)空位上就可以了。它的缺點(diǎn)是:向集合插入元素將會(huì)變得低效,它需要給插入的元素騰出位置并順序移動(dòng)后面的元素。
如果集合的數(shù)目固定并且不涉及轉(zhuǎn)型,使用數(shù)組效率高,否則就使用List<T>(該使用數(shù)組的時(shí)候,還是要使用數(shù)組)
順序存儲(chǔ)結(jié)構(gòu),即線性表。線性表可動(dòng)態(tài)地?cái)U(kuò)大和縮小,它在一片連續(xù)的區(qū)域中存儲(chǔ)數(shù)據(jù)元素。線性表不能按照索引進(jìn)行查找,它是通過對(duì)地址的引用來(lái)搜索元素的,為了找到某個(gè)元素,它必須遍歷所有元素,直到找到對(duì)應(yīng)的元素為止。所以,線性表的優(yōu)點(diǎn)是插入和刪除數(shù)據(jù)效率高,缺點(diǎn)是查找的效率相對(duì)來(lái)說低一些。
隊(duì)列Queue<T>遵循的是先入先出的模式,它在集合末尾添加元素,在集合的起始位置刪除元素。
棧Stack<T>遵循的是后入先出的模式,它在集合末尾添加元素,同時(shí)也在集合末尾刪除元素。
字典Dictionary<TKey, TValue>存儲(chǔ)的是鍵值對(duì),值在基于鍵的散列碼的基礎(chǔ)上進(jìn)行存儲(chǔ)。字典類對(duì)象由包含集合元素的存儲(chǔ)桶組成,每一個(gè)存儲(chǔ)桶與基于該元素的鍵的哈希值關(guān)聯(lián)。如果需要根據(jù)鍵進(jìn)行值的查找,使用Dictionary<TKey, TValue>將會(huì)使搜索和檢索更快捷。
雙向鏈表LinkedList<T>是一個(gè)類型為L(zhǎng)inkedListNode的元素對(duì)象的集合。當(dāng)我們覺得在集合中插入和刪除數(shù)據(jù)很慢時(shí),就可以考慮使用鏈表。如果使用LinkedList<T>,我們會(huì)發(fā)現(xiàn)此類型并沒有其他集合普遍具有的Add方法,取而代之的是AddAfter、AddBefore、AddFirst、AddLast等方法。雙向鏈表中的每個(gè)節(jié)點(diǎn)都向前指向Previous節(jié)點(diǎn),向后指向Next節(jié)點(diǎn)。
在FCL中,非線性集合實(shí)現(xiàn)得不多。非線性集合分為層次集合和組集合。層次集合(如樹)在FCL中沒有實(shí)現(xiàn)。組集合又分為集和圖,集在FCL中實(shí)現(xiàn)為HashSet<T>,而圖在FCL中也沒有對(duì)應(yīng)的實(shí)現(xiàn)。
集的概念本意是指存放在集合中的元素是無(wú)序的且不能重復(fù)的。
除了上面提到的集合類型外,還有其他幾個(gè)要掌握的集合類型,它們是在實(shí)際應(yīng)用中發(fā)展而來(lái)的對(duì)以上基礎(chǔ)類型的擴(kuò)展:SortedList<T>、SortedDictionary<TKey, TValue>、Sorted-Set<T>。它們所擴(kuò)展的對(duì)應(yīng)類分別為L(zhǎng)ist<T>、Dictionary<TKey, TValue>、HashSet<T>,作用是將原本無(wú)序排列的元素變?yōu)橛行蚺帕小?/p>
除了排序上的需求增加了上面3個(gè)集合類外,在命名空間System.Collections.Concurrent下,還涉及幾個(gè)多線程集合類。它們主要是:
ConcurrentBag<T>對(duì)應(yīng)List<T>
ConcurrentDictionary<TKey, TValue>對(duì)應(yīng)Dictionary<TKey, TValue>
ConcurrentQueue<T>對(duì)應(yīng)Queue<T>
ConcurrentStack<T>對(duì)應(yīng)Stack<T>
FCL集合圖如下:
7、確保集合的線程安全
集合線程安全是指在多個(gè)線程上添加或刪除元素時(shí),線程之間必須保持同步。
泛型集合一般通過加鎖來(lái)進(jìn)行安全鎖定,如下:
static object sycObj=new object();
static void Main(string[]args)
{
//object sycObj=new object();
Thread t1=new Thread(()=>{
//確保等待t2開始之后才運(yùn)行下面的代碼
autoSet.WaitOne();
lock(sycObj)
{
foreach(Person item in list)
{
Console.WriteLine('t1:'+item.Name);
Thread.Sleep(1000);
}
}
}
8、避免將List<T>作為自定義集合類的基類
如果要實(shí)現(xiàn)一個(gè)自定義的集合類,不應(yīng)該以一個(gè)FCL集合類為基類,而應(yīng)該擴(kuò)展相應(yīng)的泛型接口。FCL集合類應(yīng)該以組合的形式包含至自定義的集合類,需擴(kuò)展的泛型接口通常是IEnumer-able<T>和ICollection<T>(或ICollection<T>的子接口,如IList<T>),前者規(guī)范了集合類的迭代功能,后者則規(guī)范了一個(gè)集合通常會(huì)有的操作。
List<T>基本上沒有提供可供子類使用的protected成員(從object中繼承來(lái)的Finalize方法和Member-wiseClone方法除外),也就是說,實(shí)際上,繼承List<T>并沒有帶來(lái)任何繼承上的優(yōu)勢(shì),反而喪失了面向接口編程帶來(lái)的靈活性。而且,稍加不注意,隱含的Bug就會(huì)接踵而至。
9、迭代器應(yīng)該是只讀的
FCL中的迭代器只有GetEnumerator方法,沒有SetEnumerator方法。所有的集合類也沒有一個(gè)可寫的迭代器屬性。
原因有二
這違背了設(shè)計(jì)模式中的開閉原則。被設(shè)置到集合中的迭代器可能會(huì)直接導(dǎo)致集合的行為發(fā)生異常或變動(dòng)。一旦確實(shí)需要新的迭代需求,完全可以創(chuàng)建一個(gè)新的迭代器來(lái)滿足需求,而不是為集合設(shè)置該迭代器,因?yàn)檫@樣做會(huì)直接導(dǎo)致使用到該集合對(duì)象的其他迭代場(chǎng)景發(fā)生不可知的行為。
現(xiàn)在,我們有了LINQ。使用LINQ可以不用創(chuàng)建任何新的類型就能滿足任何的迭代需求。
10、謹(jǐn)慎集合屬性的可寫操作
如果類型的屬性中有集合屬性,那么應(yīng)該保證屬性對(duì)象是由類型本身產(chǎn)生的。如果將屬性設(shè)置為可寫,則會(huì)增加拋出異常的幾率。一般情況下,如果集合屬性沒有值,則它返回的Count等于0,而不是集合屬性的值為null。
11、使用匿名類型存儲(chǔ)LINQ查詢結(jié)果(最佳搭檔)
從.NET 3.0開始,C開始支持一個(gè)新特性:匿名類型。匿名類型由var、賦值運(yùn)算符和一個(gè)非空初始值(或以new開頭的初始化項(xiàng))組成。匿名類型有如下的基本特性:
既支持簡(jiǎn)單類型也支持復(fù)雜類型。簡(jiǎn)單類型必須是一個(gè)非空初始值,復(fù)雜類型則是一個(gè)以new開頭的初始化項(xiàng);
匿名類型的屬性是只讀的,沒有屬性設(shè)置器,它一旦被初始化就不可更改;
如果兩個(gè)匿名類型的屬性值相同,那么就認(rèn)為兩個(gè)匿名類型相等;
匿名類型可以在循環(huán)中用作初始化器;
匿名類型支持智能感知;
還有一點(diǎn),雖然不常用,但是匿名類型確實(shí)也可以擁有方法。
12、在查詢中使用Lambda表達(dá)式
LINQ實(shí)際上是基于擴(kuò)展方法和Lambda表達(dá)式的,理解了這一點(diǎn)就不難理解LINQ。任何LINQ查詢都能通過調(diào)用擴(kuò)展方法的方式來(lái)替代,如下面的代碼所示:
foreach(var item in personList.Select(person=>new{PersonName= person.Name,CompanyName=person.CompanyID==0?'Micro':'Sun'}))
{
Console.WriteLine(string.Format('{0} :{1}',item.PersonName, item.CompanyName));
}
針對(duì)LINQ設(shè)計(jì)的擴(kuò)展方法大多應(yīng)用了泛型委托。System命名空間定義了泛型委托Action、Func和Predicate。
可以這樣理解這三個(gè)委托:Action用于執(zhí)行一個(gè)操作,所以它沒有返回值;Func用于執(zhí)行一個(gè)操作并返回一個(gè)值;Predicate用于定義一組條件并判斷參數(shù)是否符合條件。
Select擴(kuò)展方法接收的就是一個(gè)Func委托,而Lambda表達(dá)式其實(shí)就是一個(gè)簡(jiǎn)潔的委托,運(yùn)算符“=>”左邊代表的是方法的參數(shù),右邊的是方法體。
13、理解延遲求值和主動(dòng)求值之間的區(qū)別
樣例如下:
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時(shí),延遲求值能夠帶來(lái)顯著的性能提升。舉個(gè)例子:如果定義了兩個(gè)查詢,而且采用延遲求值,CLR則會(huì)合并兩次查詢并生成一個(gè)最終的查詢。
14、區(qū)別LINQ查詢中的IEnumerable<T>和IQueryable<T>
LINQ查詢方法一共提供了兩類擴(kuò)展方法,在System.Linq命名空間下,有兩個(gè)靜態(tài)類:Enumerable類,它針對(duì)繼承了IEnumerable<T>接口的集合類進(jìn)行擴(kuò)展;Queryable類,它針對(duì)繼承了IQueryable<T>接口的集合類進(jìn)行擴(kuò)展。
稍加觀察我們會(huì)發(fā)現(xiàn),接口IQueryable<T>實(shí)際也是繼承了IEnumerable<T>接口的,所以,致使這兩個(gè)接口的方法在很大程度上是一致的。那么,微軟為什么要設(shè)計(jì)出兩套擴(kuò)展方法呢?
我們知道,LINQ查詢從功能上來(lái)講實(shí)際上可分為三類:LINQ to OBJECTS、LINQ to SQL、LINQ to XML(本建議不討論)。設(shè)計(jì)兩套接口的原因正是為了區(qū)別對(duì)待LINQ to OBJECTS、LINQ to SQL,兩者對(duì)于查詢的處理在內(nèi)部使用的是完全不同的機(jī)制。針對(duì)LINQ to OBJECTS時(shí),使用Enumerable中的擴(kuò)展方法對(duì)本地集合進(jìn)行排序和查詢等操作,查詢參數(shù)接受的是Func<>。Func<>叫做謂語(yǔ)表達(dá)式,相當(dāng)于一個(gè)委托。針對(duì)LINQ toSQL時(shí),則使用Queryable中的擴(kuò)展方法,它接受的參數(shù)是Ex-pression<>。Expression<>用于包裝Func<>。LINQ to SQL引擎最終會(huì)將表達(dá)式樹轉(zhuǎn)化成為相應(yīng)的SQL語(yǔ)句,然后在數(shù)據(jù)庫(kù)中執(zhí)行。
那么,到底什么時(shí)候使用IQueryable<T>,什么時(shí)候使用IEnumerable<T>呢?簡(jiǎn)單表述就是:本地?cái)?shù)據(jù)源用IEnumer-able<T>,遠(yuǎn)程數(shù)據(jù)源用IQueryable<T>。
注意
在使用IQueryable<T>和IEnumerable<T>的時(shí)候還需要注意一點(diǎn),IEnumerable<T>查詢的邏輯可以直接用我們自己所定義的方法,而IQueryable<T>則不能使用自定義的方法,它必須先生成表達(dá)式樹,查詢由LINQ to SQL引擎處理。在使用IQueryable<T>查詢的時(shí)候,如果使用自定義的方法,則會(huì)拋出異常。
15、使用LINQ取代集合中的比較器和迭代器
LINQ提供了類似于SQL的語(yǔ)法來(lái)實(shí)現(xiàn)遍歷、篩選與投影集合的功能。借助于LINQ的強(qiáng)大功能,我們通過兩條語(yǔ)句就能實(shí)現(xiàn)上述的排序要求。
var orderByBonus=from s in companySalary orderby s.Bonus select s;
foreach實(shí)際會(huì)隱含調(diào)用的是集合對(duì)象的迭代器。以往,如果我們要繞開集合的Sort方法對(duì)集合元素按照一定的順序進(jìn)行迭代,則需要讓類型繼承IEnumerable接口(泛型集合是IEnumerable<T>接口),實(shí)現(xiàn)一個(gè)或多個(gè)迭代器?,F(xiàn)在從LINQ查詢生成匿名類型來(lái)看,相當(dāng)于可以無(wú)限為集合增加迭代需求。
有了LINQ之后,我們是否就不再需要比較器和迭代器了呢?答案是否定的。我們可以利用LINQ的強(qiáng)大功能簡(jiǎn)化自己的編碼,但是LINQ功能的實(shí)現(xiàn)本身就是借助于FCL泛型集合的比較器、迭代器、索引器的。LINQ相當(dāng)于封裝了這些功能,讓我們使用起來(lái)更加方便。在命名空間Sys-tem.Linq下存在很多靜態(tài)類,這些靜態(tài)類存在的意義就是為FCL的泛型集合提供擴(kuò)展方法
強(qiáng)烈建議你利用LINQ所帶來(lái)的便捷性,但我們?nèi)孕枵莆毡容^器、迭代器、索引器的原理,以便更好地理解LINQ的思想,寫出更高質(zhì)量的代碼。最好是能看懂Linq源碼。
public static IOrderedEnumerable<TSource>OrderBy<TSource,TKey>(this IEnumerable<TSource>source,Func<TSource,TKey>keySelector){ //省略}
16、在LINQ查詢中避免不必要的迭代
比如常使用First()方法,F(xiàn)irst方法實(shí)際完成的工作是:搜索到滿足條件的第一個(gè)元素,就從集合中返回。如果沒有符合條件的元素,它也會(huì)遍歷整個(gè)集合。
與First方法類似的還有Take方法,Take方法接收一個(gè)整型參數(shù),然后為我們返回該參數(shù)指定的元素個(gè)數(shù)。與First一樣,它在滿足條件以后,會(huì)從當(dāng)前的迭代過程直接返回,而不是等到整個(gè)迭代過程完畢再返回。如果一個(gè)集合包含了很多的元素,那么這種查詢會(huì)為我們帶來(lái)可觀的時(shí)間效率。
會(huì)運(yùn)用First和Take等方法,都會(huì)讓我們避免全集掃描,大大提高效率。
總結(jié)
如有需要, 上一篇的《C#規(guī)范整理·語(yǔ)言要素》也可以看看!
聯(lián)系客服