大
部分計算機科學(xué)和編程的入門課本中都會有一章介紹集合。它們可能被稱為數(shù)組或數(shù)據(jù)結(jié)構(gòu),但概念是相同的。將正式數(shù)據(jù)對象中的一組元素與另一組元素聯(lián)系在一起的能力對現(xiàn)代編程技術(shù)來說是必需的。
在 Microsoft® .NET Framework 中,付出的很多努力都是為了創(chuàng)建強大的集合類,以滿足各種需求及樣式。這些集合使用方便、直觀,并有足夠的性能,這些都是非常重要的特征。在本月的這一期內(nèi)容中,我將著眼于 .NET 中的集合、它們的工作原理、使用它們的時機和一些最佳實踐。
使用集合的時機
開發(fā)人員面對的第一個決定好像相當(dāng)簡單:我是否該使用集合?有時答案會顯而易見。有時候就是個人喜好問題。集合的基本用途是將某些對象聯(lián)系起來構(gòu)成一個組,因為它們彼此間有著某種邏輯關(guān)系。例如,您可以有一個代表書籍的 Book 類,也可以有一個代表圖書館或圖書館中一書架書的 BookCollection 類。在這中情況下使用集合,可以在書架中快速添加或刪除書籍、移動書籍、根據(jù)標(biāo)題或 ISBN 或某些其他描述性數(shù)據(jù)查找書籍、統(tǒng)計書籍?dāng)?shù)量、遍歷書籍等。隨后我將就此詳細(xì)介紹,但在這種情況下使用集合是顯而易見的選擇。
現(xiàn)在,讓我們看一看您幾乎不會選擇使用集合的情形££有理數(shù)的情況。有理數(shù)是能表示為兩個整數(shù)之比的數(shù)。在集合描述中,我提出集合應(yīng)該與一組元素結(jié)合使用,這樣您就可以使用有兩個元素的集合,一個用做分子,另一個用作分母。但是,分子和分母的用途完全不同(與所有元素都用于相似目的的書籍示例不同)。在集合中存儲項目的一個主要原因是可以輕松遍歷所有元素,因此可以對每個元素進(jìn)行操作,而對組成有理數(shù)的整數(shù)來說這項任務(wù)幾乎沒有意義。另外,有理數(shù)的元素數(shù)量固定,而且非常少。因此,用集合存儲有理數(shù)的元素沒有任何意義;這里您應(yīng)該使用兩個單獨的整數(shù)字段。
現(xiàn)在,請設(shè)想一個可能有必要討論使用集合的情形。假設(shè)一個用于描述某個體育賽事(如棒球比賽)參賽隊的數(shù)據(jù)結(jié)構(gòu)。在比賽中需要對每個參賽隊執(zhí)行操作,而且每個隊都有相似的目標(biāo)(與有理數(shù)中的分子和分母不同)。但是,在類似棒球這樣的比賽中,只有兩個隊參賽,因此雖然循環(huán)支持可能有用,但要枚舉的項目數(shù)量卻非常少(原型實現(xiàn)中數(shù)量是固定的)。有些人可能選擇使用 TeamCollection 或 Team 實例數(shù)組描述參賽雙方,另外一些人可能選擇使用兩個單獨的 Team 字段。兩種實現(xiàn)各有其優(yōu)缺點。
然而,考慮使用集合時應(yīng)牢記一些簡單的指導(dǎo)方針。當(dāng)以下陳述至少有一個為真時,則應(yīng)該使用集合:
各個元素具有相似的用途和同樣的重要性。 元素的數(shù)量未知,或者沒有在編譯時固定。 需要支持對所有元素的遍歷。 需要支持對元素的排序。 需要公開某個庫的元素,并且預(yù)期用戶在此使用集合類型。
注意,出于本欄目之目的,我認(rèn)為數(shù)組 (System.Array) 將成為一種特殊類型的集合。
創(chuàng)建自定義集合的時機
.NET Framework 包含多種集合類型,其中最常見的類型在 System.Collections 命名空間和子命名空間下(System.Array 除外)。我強烈建議在了解可用的集合后再編寫自定義集合,因為現(xiàn)有的類型可以滿足您對集合的絕大部分需求。
您可能需要創(chuàng)建自定義集合的情況是您在嘗試實現(xiàn) Framework 中尚不存在的特殊數(shù)據(jù)結(jié)構(gòu)。腦海中出現(xiàn)的圖形、樹和其他特殊數(shù)據(jù)結(jié)構(gòu)。許多情況下它們會非常有用。如果需要現(xiàn)有集合類型的一個變體,則必須創(chuàng)建一個自定義集合。您可能需要集合只承載某些類型,并且此集合的特定實現(xiàn)取決于這些類型。您可能出于性能考慮而需要自定義集合;如果通用集合無法滿足您的特殊性能需求,編寫自定義集合可能是您的唯一選擇。例如,您可能需要一個能在其中快速插入項目的列表,同時也可以快速查找項目而不必遍歷該列表。
現(xiàn)有的集合
如果您使用現(xiàn)有的集合,則仍需要選擇合適的集合。根據(jù)構(gòu)建的內(nèi)容不同,您可以從各種不同的集合中作出選擇。首先,您需要確定集合中要存儲什么,然后決定要如何使用集合。是否會一次性添加所有元素?是否會動態(tài)添加和刪除元素?是否只需要遍歷整個集合,或者還需要查找和使用特定元素?是否通過某個公共 API 公開集合?應(yīng)用程序的性能特征和要求是什么?詢問這些問題有助于您決定使用哪個集合,如果這些選項無法滿足您的需求,可以考慮擴展集合而不是從頭開始。
泛型或非泛型
.NET Framework 2.0 引入了泛型概念。在 .NET Framework 1.x 中,大多數(shù)集合類都存儲 System.Object 引用。這意味著每次從集合中提取項目時都必須將其轉(zhuǎn)換回原來的類型(如果您建了庫,那么庫的使用者需要知道這些都是什么類型)。這個轉(zhuǎn)換對性能有影響,尤其在存儲值類型時,因為將值添加到集合時首先需要將值封裝到 System.Object 中,以后在從集合中取出值并轉(zhuǎn)換回原來的類型時需要取消封裝。
通過使用泛型集合類型(可在 System.Collections.Generic 命名空間下找到),可以避免這些問題。例如,如果您的 C# 應(yīng)用程序中需要一個字符串列表,則可以使用 System.Collections.Generic.List<String>(Visual Basic® 中為 List(Of String),C++ 中為 <String^>)。這樣可以確保提取元素時,它們只進(jìn)入 System.String 類型(意味著您不需要從 Object 轉(zhuǎn)換到 String)。如果嘗試在列表中存儲其他內(nèi)容,也可以通過提供編譯時錯誤使編譯器強制該行為。另外,如果您正在開發(fā)供其他人使用的庫,并且不知道他們要使用什么內(nèi)含類型,則可以繼續(xù)使用庫用戶隨后將定義的泛型 T。您也可以限制該 T 類型,以便對即將使用的類型有些了解(它們從一組特定的接口派生而來,它們實現(xiàn)無參數(shù)的構(gòu)造函數(shù),等等)。
如果您現(xiàn)在或?qū)硎褂?.NET Framework 2.0,非常不鼓勵使用舊的非泛型集合。需要保存它們的唯一原因可能是作為遺產(chǎn),或者您的代碼需要與舊版的 Framework 配合使用。使用舊的非泛型集合會降低代碼速度,也會降低它的可讀性。即使需要在一個集合中存儲多個類型,仍可使用泛型集合。您只需要為要存儲的對象找到一個常用的基本類型;最不濟(jì)也可以將 System.Object 用作泛型參數(shù)以便存儲任何內(nèi)容(如 List<Object>),因為每個類型都是從 System.Object 派生而來。
如果有使用非泛型集合的舊代碼,強烈建議您考慮更改代碼以便使用泛型集合。
圖 1 中的表顯示了建議的類型和應(yīng)使用的接口。此外,.NET Framework 2.0 還包含了在 .NET Framework 1.x 中沒有直接對應(yīng)項的新泛型集合,例如,LinkedList<T> 和 SortedDictionary<TKey, TValue>。.NET Framework 3.5 還包含一個 HashSet<T> 類,提供新的標(biāo)準(zhǔn)集功能(先前的版本中,通??衫?Hashtable 或 Dictionary<TKey,TValue> 來滿足對集的需求)。
性能方面的含義
需要集合的設(shè)計通常是因為需要使用大量的項目,而您使用的項目數(shù)量會影響應(yīng)用程序的性能。要確保選擇了正確的集合類型,重要的是了解預(yù)期的集合使用模式。例如,您可能只在初始化應(yīng)用程序時在集合中插入項目,或者在程序的整個生命周期中您都可能插入項目。您與集合的唯一交互可能是遍歷所有元素,或者您可能需要隨機訪問功能。了解使用配置文件有助于找到符合需求的最佳集合。
如果需要非常快地添加、刪除和查找項目,而且不關(guān)心集合中項目的順序,那么首先應(yīng)該考慮使用 System.Collections.Generic.Dictionary<TKey, TValue>(或者您正在使用 .NET Framework 1.x,可以考慮 Hashtable)。三個基本操作(添加、刪除和包含)都可快速操作,即使集合包含上百萬的項目。另一方面,利用 List<T>(或 .NET Framework 1.x 中的 ArrayList)插入和刪除項目所需的時間都可能有所不同。(List<T> 和 ArrayList 都在基礎(chǔ)數(shù)組中存儲項目,并保持順序。添加項目可能需要移動基礎(chǔ)數(shù)組中現(xiàn)有的項目以騰出空間。在末尾添加項目不需要進(jìn)行任何移動,而且速度非??欤?div style="height:15px;">
如果您的使用模式很少需要刪除和大量添加,而重要的是保持集合的順序,那么您仍然可以選擇 List<T>。雖然查找速度可能比較慢(因為在搜索目標(biāo)項目時需要遍歷基礎(chǔ)數(shù)組),但可以保證集合會保持特定的順序。另外,您可以選擇 Queue<T> 實現(xiàn)先進(jìn)先出 (FIFO) 順序或 Stack<T> 實現(xiàn)后進(jìn)先出 (LIFO) 順序。雖然 Queue<T> 和 Stack<T> 都支持枚舉集合中的所有項目,但前者只支持在末尾插入和從開頭刪除,而后者只支持從開頭插入和刪除。
如果需要在實現(xiàn)快速插入的同時保持順序,那么使用新的 LinkedList<T> 集合可幫助您提高性能。與 List<T> 不同,LinkedList<T> 是作為動態(tài)分配的對象鏈實現(xiàn)。與 List<T> 相比,在集合中間插入對象只需要更新兩個連接和添加新項目。從性能的角度來看,鏈接列表的缺點是垃圾收集器會增加其活動,因為它必須遍歷整個列表以確保沒有對象沒有被釋放。另外,由于每個節(jié)點相關(guān)的開銷以及每個節(jié)點在內(nèi)存中的位置等原因,大的鏈接列表可能會出現(xiàn)性能問題。雖然將項目插入到 LinkedList<T> 的實際操作比在 List<T> 中插入要快得多,但是找到要插入新值的特定位置仍需遍歷列表并找到正確的位置。
如前所述,Dictionary<TKey, TValue> 可能最適用于快速插入和查找項目。但是,較快的查找速度只是針對普通情況而言,某些數(shù)據(jù)集可能導(dǎo)致性能急劇下降。SortedDictionary<TKey,TValue> 是一個不同的實現(xiàn),它用平衡樹實現(xiàn)作為基礎(chǔ)數(shù)據(jù)存儲;這相對提高了查找速度并保持項目的排列順序,但插入很有可能會慢一些(隨集合中的項目數(shù)量不同而有所差異)?;蛘咭部梢杂?SortedList<Tkey,TValue>,它使用兩個獨立的數(shù)組分別保存鍵和值,并保持兩者的順序(最壞的情況是必須移動所有鍵和值)。
使用 List<T> 的天氣年鑒
以下示例是一個簡單的應(yīng)用程序,計算各城市天氣預(yù)報列表的平均溫度和濕度(請參見
圖 2)。WeatherReport 類用于存儲所選城市的單個天氣事件。List<WeatherReport> 集合用于存儲求平均值的信息。我在這個案例中選擇了 List<T> 集合,因為我的使用模式是只添加項目,而不是刪除項目。另外,我不關(guān)心排序或在中間插入項目,也不關(guān)心在列表中查找單個項目。我對列表做的唯一其他操作是遍歷項目。您可以選擇將此程序擴展為找到最熱或最冷的城市,以及增加許多天氣年鑒類型操作。
自定義集合
某些情況下,您可能發(fā)現(xiàn) Framework 中任何現(xiàn)有的集合類型都無法滿足您的全部要求??赡苣歇毺氐囊螅蛘呷栽谑褂媒?jīng)過很少改動的現(xiàn)有集合。如果決定要編寫自定義集合,首先要考慮擴展現(xiàn)有的集合類型。
通過查看 System.Collections.ObjectModel 命名空間開始。這些集合已經(jīng)實現(xiàn)了最需要的接口,并為您提供了需要的基本功能。您可以輕易地為繼承的類添加方法,從而滿足您的特定需求。
例如,假設(shè)您需要實現(xiàn)一個簡單的 System.Double 值集合,但還希望支持使用 Double.TryParse 的 Add(string) 方法,只在將值成功解析為 Double 后才添加結(jié)果。System.Collections.ObjectModel.Collection<T> 類實現(xiàn)了所有的標(biāo)準(zhǔn)集合接口,為了提供附加的 Add 重載,您的自定義集合類型從 Collection<Double> 派生即可。
class DoubleCollection : Collection<double>{public void Add(string st){Double d;if (Double.TryParse(st, out d)) base.Add(d);else throw new ArgumentException(“Cannot parse string to a double. “ +“Item was not added to collection.”);}}
隨著擴展方法的引入,.NET Framework 3.5 可以提供額外的機制來創(chuàng)建擴展現(xiàn)有類型的方法。例如,您可以在 C# 3.0 中重新編寫此方法作為擴展方法,如下所示:
class CollectionExtensions{public static void Add(this Collection<Double> c, string st){Double d;if (Double.TryParse(st, out d)) c.Add(d);else throw new ArgumentException(“Cannot parse string to a double. “ +“Item was not added to collection.”);}}
如果一個 Collection<Double> 有命名的值,則此靜態(tài)方法可通過傳統(tǒng)語法將新值添加到集合中。
CollectionExtensions.Add(values, “3.14”);
但是,因為這是一個擴展方法(顯然“this”關(guān)鍵字將第一個參數(shù)歸為靜態(tài) Add 方法),您可以按如下所示重新編寫:
values.Add(“3.14”);
如果用新類型擴展了 Collection<Double>,那么您會編寫完全相同的方法調(diào)用,但您現(xiàn)在可以將此新 Add 方法與任何 Collection<Double> 實例(或其派生的任何類型)配合使用,而不只是與自定義類型一起使用?;蛘吣梢蚤_始構(gòu)建自己的集合,不需要繼承任何現(xiàn)有的集合。這種情況下,您可以利用 .NET Framework 中提供的適當(dāng)接口。
實現(xiàn)集合接口
如果您真的決定從頭開始編寫自己的自定義集合,則應(yīng)實現(xiàn) .NET Framework 提供的所有適當(dāng)接口。這些接口公開了與集合交互的常見方式,大多數(shù)人都認(rèn)為非常直觀和易于使用。下面是可供選擇的各種接口的簡短概述。
IEnumerable 該接口允許通過返回集合枚舉器來枚舉集合。該接口公開可返回 IEnumerator 對象的單一方法 GetEnumerator。IEnumerator 對象用于快速遍歷整個集合。實現(xiàn)了此接口的任何集合都可以通過 C# 中的 foreach 語言結(jié)構(gòu)(在 Visual Basic 中是 For Each)進(jìn)行訪問。
IEnumerable<T> 此接口通過附加的 GetEnumerator 方法擴展了非泛型 IEnumerable,該方法返回 IEnumerator<T> 而不是 IEnumerator。這意味著如果計劃實現(xiàn) IEnumerable<T>,則還必須實現(xiàn) IEnumerable。不同之處是此附加的 GetEnumerator 返回泛型版本的 IEnumerator (IEnumerator<T>) 而不是非泛型版本。這是一個常見的做法,首先在 IEnumerable<T> 中提供集合的枚舉邏輯,然后使非泛型 GetEnumerator 方法委托到泛型 GetEnumerator 方法以確保兩者都返回相同的枚舉器(從而不需要再一次實現(xiàn)同一邏輯)。
IEnumerator 此接口通常不由集合自身實現(xiàn)。我在此提到它是因為它是一個有用的接口,在構(gòu)建自定義集合時應(yīng)牢記。此接口用于枚舉集合中的項目。它公開三個方法:Reset、Current 和 MoveNext。Reset 方法用于初始化枚舉。Current 方法用于獲取當(dāng)前的項目,MoveNext 用于移至下一個項目。如果能用 GetEnumerator 為集合獲取一個枚舉器,則可以實現(xiàn) foreach 循環(huán)。例如,如果 Words 是一個字符串 ArrayList,那么以下代碼將得到相同結(jié)果:
foreach (string word in Words) Console.WriteLine (word);
或
IEnumerator enumerator = Words.GetEnumerator();while(enumerator.MoveNext())Console.WriteLine(enumerator.Current);
IEnumerator <T> 泛型版本的 IEnumerator 接口通過添加返回 T 的 Current 屬性重載擴展了非泛型版本。此重載無需進(jìn)行轉(zhuǎn)換,從而使枚舉集合更簡單。
ICollection 此接口是 IEnumerable 的擴展。它還公開三個附加的屬性和一個附加的方法:Count 是一個只讀屬性,可返回集合中的元素數(shù)量;IsSynchronized 指出該集合對線程安全,因此可以在多線程應(yīng)用程序中使用它;SyncRoot 使實現(xiàn)人員能夠?qū)崿F(xiàn)對線程安全的集合(通過提供用于該目的的對象);CopyTo 用于將集合的項目按給定索引復(fù)制到數(shù)組。大體來說,我建議避免使用非泛型 ICollection 接口,因為泛型接口要有用得多。
ICollection<T> 此接口是 IEnumerable<T> 的擴展(因此也是 IEnumerable 的擴展接口)。它還公開五個新方法和兩個新屬性。方法包括 Add(用于將新元素添加到集合中)、Remove(用于刪除元素)、Clear(用于刪除所有元素)、Contains(用于確定某個項目是否在集合中)和 CopyTo(用于將項目復(fù)制到數(shù)組)。兩個只讀屬性是 Count(與非泛型 ICollection 中的相同)和 IsReadOnly,后者告訴我們集合是否為只讀,或者說無法刪除或添加項目(即使集合為只讀,集合中的可變項目仍然可以更改)。
此接口作為任何自定義集合的起點都非常有用。如果不知道要在集合中存儲什么類型的項目(或要存儲多種類型的項目),用 ICollection<Object> 即可,實際上得到的是非泛型的擴展 ICollection 接口。
IList 和 IList<T> IList 是 ICollection 的擴展。除了 ICollection(泛型或非泛型版本)中的所有內(nèi)容外,它還有 IndexOf、Insert 和 RemoveAt 方法以及一個索引器,用于獲取和設(shè)置集合中的任何項目,就如同使用數(shù)組一樣。非泛型 IList 也包含存在于泛型 ICollection 中和不存在于非泛型 ICollection 中的方法(Add、Remove、Clear 和 Contains)。創(chuàng)建自定義集合時 IList 接口非常有用,可用于自行添加和刪除項目,獲取或設(shè)置任何項目的值。RemoveAt 方法可根據(jù)索引刪除項目(而常規(guī) Remove 只能根據(jù)值刪除項目)。Insert 方法用于在列表的任何位置添加項目,而常規(guī) Add 只能在最后添加。
IDictionary 和 IDictionary<TKey, TValue> IDictionary 是 ICollection 的擴展。它支持用一個鍵在集合中存儲和查找項目的概念。除了 ICollection 支持的所有方法外,它還公開了將鍵與添加的項目相關(guān)聯(lián)的另一個 Add 重載。ContainsKey 方法將檢查是否存在特殊鍵。Keys 和 Values 屬性可返回 IDictionary 中所有鍵或值的 ICollection、基于鍵而不是值的刪除重載,以及使用該鍵的索引器。該接口的泛型版本要復(fù)雜一些,因為它允許分別指定值類型和鍵類型。
再次強調(diào)泛型或非泛型
創(chuàng)建自定義集合時,可能不必使之成為泛型。如果知道集合要存儲的對象類型,而且知道這些類型將來不會改變,則可使用固定類型的集合。您仍可以擴展泛型框架集合或使用泛型接口,但類總會存儲特定類型的對象。
如果您要構(gòu)建庫或更通用的集合(如樹或圖),那么創(chuàng)建泛型可能是一個不錯的主意。這樣此類型的使用者就能夠?qū)⑺c各種類型一起使用,而且沒有使用 Object 和來回轉(zhuǎn)換的缺點。通用有向圖類可能如下所示:
class DirectionalGraph<T> :IEnumerable<T>, ICollection<T>, IList<T>{public void Clear() { }public void Add(T item, IEnumerable<T> inNodes,IEnumerable<T> outNodes) { }public void Remove(T item) { }public bool Contains(T item) { }static public DirectionalGraph<T> EmptyGraph { get{}}// implement the interfaces...}
家族樹
下一個示例中,我將實現(xiàn)一個自定義的集合來存儲用于家譜目的的家族樹(請參見
圖 3)。為簡化起見,我將做如下假設(shè):每個人都有性別和年齡,每個人都有生父母。讓我們稱他們?yōu)槟赣H和父親。
樹是由節(jié)點構(gòu)成的特殊集合類型。每個節(jié)點(如
圖 4 中的 Person 類)都存儲著有關(guān)樹中其他節(jié)點的信息。有一個特殊的節(jié)點被稱為“根”,它是樹的基礎(chǔ)。所有其他節(jié)點都可以從樹的根節(jié)點開始遍歷訪問。我還實現(xiàn)了 IEnumerable<T>,通過 foreach 循環(huán)枚舉 FamilyTree 的所有成員。所有人員都輸入到樹中后,您應(yīng)該能夠回答如下問題:A 和 B 兩個人之間是什么關(guān)系?
注意,此示例中并沒有實現(xiàn)所有可能的關(guān)系,但您可以自己向 FindRelationship 方法添加更多的關(guān)系。另外還需要注意,為了示例的清晰起見,并沒有對所有方法都進(jìn)行充分的參數(shù)檢驗。建議您在實現(xiàn)之前確保每個方法只接受有效的參數(shù)值。
在運行
圖 5 中的示例時,應(yīng)該注意圖 6 中顯示的輸出。
圖 6 家族樹輸出示例(單擊該圖像獲得較小視圖)
圖 6 家族樹輸出示例 (單擊該圖像獲得較大視圖)
此刻您應(yīng)該對 .NET Framework 中的集合、它們是什么,以及使用它們的時機和原因有了很好的理解。集合可以是編程工具箱中的強大工具,因此了解它何時有用以及如何聰明地使用它都非常重要。如果您需要更多的信息,請參閱“更多參考資料”側(cè)欄中的資源。