LINQ(Language Integrated Query,語言集成查詢)提供了類似于SQL的語法,能對(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>來創(chuàng)建。而數(shù)組本身,尤其是一維數(shù)組,在遇到要求高效率的算法時(shí),則會(huì)專門被優(yōu)化以提升其效率。 一維數(shù)組也稱為向量,其性能是最佳的,在IL中使用了專門的指令來處理它們(如newarr、ldelem、ldelema、ldlen和stelem)。 從內(nè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),它省去了拆箱和裝箱帶來的開銷。 注意 由于數(shù)組本身在內(nèi)存上的特點(diǎn),因此在使用數(shù)組的過程中還應(yīng)該注意大對(duì)象的問題。所謂“大對(duì)象”,是指那些占用內(nèi)存超過85 000字節(jié)的對(duì)象,它們被分配在大對(duì)象堆里。大對(duì)象的分配和回收與小對(duì)象相比,都不太一樣,尤其是回收,大對(duì)象在回收過程中會(huì)帶來效率很低的問題。所以,不能肆意對(duì)數(shù)組指定過大的長(zhǎng)度,這會(huì)讓數(shù)組成為一個(gè)大對(duì)象。
2、多數(shù)情況下使用foreach進(jìn)行循環(huán)遍歷 采用foreach最大限度地簡(jiǎn)化了代碼。 它用于遍歷一個(gè)繼承了IEmuerable或IEmuerable<T>接口的集合元素。借助于IL代碼可以看到foreach還是本質(zhì)就是利用了迭代器來進(jìn)行集合遍歷。如下: List<object>list=new List<object>();除了代碼簡(jiǎn)潔之外,foreach還有兩個(gè)優(yōu)勢(shì)
3、foreach不能代替for foreach存在的一個(gè)問題是:它不支持循環(huán)時(shí)對(duì)集合進(jìn)行增刪操作。取而代之的方法是使用for循環(huán)。 不支持原因:
無論是for循環(huán)還是foreach循環(huán),內(nèi)部都是對(duì)該數(shù)組的訪問,而迭代器僅僅是多進(jìn)行了一次版本檢測(cè)。事實(shí)上,在循環(huán)內(nèi)部,兩者生成的IL代碼也是差不多的。 4、使用更有效的對(duì)象和集合初始化 舉例: class Program {對(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)化: 重點(diǎn):初始化設(shè)定項(xiàng)絕不僅僅是為了對(duì)象和集合初始化的方便,它更重要的作用是為L(zhǎng)INQ查詢中的匿名類型進(jìn)行屬性的初始化。由于LINQ查詢返回的集合中匿名類型的屬性都是只讀的,如果需要為匿名類型屬性賦值,或者增加屬性,只能通過初始化設(shè)定項(xiàng)來進(jìn)行。初始化設(shè)定項(xiàng)還能為屬性使用表達(dá)式。 舉例 List<Person>personList2=new List<Person>()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í)際的類型括起來,然后編譯器和運(yùn)行時(shí)會(huì)完成剩余的工作。 6、選擇正確的集合 要選擇正確的集合,首先需要了解一些數(shù)據(jù)結(jié)構(gòu)的知識(shí)。所謂數(shù)據(jù)結(jié)構(gòu),就是相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合 說明
FCL集合圖如下: 7、確保集合的線程安全 集合線程安全是指在多個(gè)線程上添加或刪除元素時(shí),線程之間必須保持同步。 泛型集合一般通過加鎖來進(jìn)行安全鎖定,如下: 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中繼承來的Finalize方法和Member-wiseClone方法除外),也就是說,實(shí)際上,繼承List<T>并沒有帶來任何繼承上的優(yōu)勢(shì),反而喪失了面向接口編程帶來的靈活性。而且,稍加不注意,隱含的Bug就會(huì)接踵而至。 9、迭代器應(yīng)該是只讀的 FCL中的迭代器只有GetEnumerator方法,沒有SetEnumerator方法。所有的集合類也沒有一個(gè)可寫的迭代器屬性。 原因有二
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))組成。匿名類型有如下的基本特性:
12、在查詢中使用Lambda表達(dá)式 LINQ實(shí)際上是基于擴(kuò)展方法和Lambda表達(dá)式的,理解了這一點(diǎn)就不難理解LINQ。任何LINQ查詢都能通過調(diào)用擴(kuò)展方法的方式來替代,如下面的代碼所示: foreach(var item in personList.Select(person=>new{PersonName= person.Name,CompanyName=person.CompanyID==0?'Micro':'Sun'}))針對(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ū)別 樣例如下: 在使用LINQ to SQL時(shí),延遲求值能夠帶來顯著的性能提升。舉個(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查詢從功能上來講實(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<>叫做謂語表達(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語句,然后在數(shù)據(jù)庫中執(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的語法來實(shí)現(xiàn)遍歷、篩選與投影集合的功能。借助于LINQ的強(qiáng)大功能,我們通過兩條語句就能實(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查詢生成匿名類型來看,相當(dāng)于可以無限為集合增加迭代需求。 有了LINQ之后,我們是否就不再需要比較器和迭代器了呢?答案是否定的。我們可以利用LINQ的強(qiáng)大功能簡(jiǎn)化自己的編碼,但是LINQ功能的實(shí)現(xiàn)本身就是借助于FCL泛型集合的比較器、迭代器、索引器的。LINQ相當(dāng)于封裝了這些功能,讓我們使用起來更加方便。在命名空間Sys-tem.Linq下存在很多靜態(tài)類,這些靜態(tài)類存在的意義就是為FCL的泛型集合提供擴(kuò)展方法
16、在LINQ查詢中避免不必要的迭代
會(huì)運(yùn)用First和Take等方法,都會(huì)讓我們避免全集掃描,大大提高效率。 總結(jié) 如有需要, 上一篇的《C#規(guī)范整理·語言要素》也可以看看! |
|
|