什麼是 Dictionaries
當資料型態具有 key/value 特性時,就可以使用 Dictionary 類型的集合。它可以用來建立索引表,將任意的 key 與 value 做關連對應。.NET Framework 包含下列 Dictionary 類別:
- Hashtable :最基本的字典集合,每個項目都是儲存在 DictionaryEntry 物件中的 key/value 組。可透過 key 取得項目的 value。
- SortedList :類似 Hashtable,會自動以 key 排序集合內的項目。
- ListDictionary :當集合項目少於10個時,採用這個集合類別效率較佳。
- HybridDictionary :使用這個集合,當集合項目少於10個時,會採用 ListDictionary ,若項目變大時,會自動轉成 Hashtable 。
- OrderedDictionary :類似 Hashtable ,但集合中的項目會依加入的先後順序排列,不像 Hashtable 是依 hashcode 排列。
底下二個是比較特殊類似字典的集合
- StringDictionary :類似 Hashtable ,但 key/value 都只允許字串型別, Key 不能重複,一個 key 只能對應一個 value 。
- NameValueCollection :類似 Hashtable ,但 key/value 都只允許字串型別, Key 不能重複,一個 key 可以對應多個 value 。
下列幾點是有關於 Dictionary 的幾點特性:
- 每個項目都是儲存在 DictionaryEntry 物件中的 Key/Value。
- Key 必需唯一的。
- Value 可以是 null ,而且不一定要是唯一的。
- IDictionary 實作分為三類:唯讀、固定大小、變數大小。
- 唯讀的 IDictionary 物件無法修改。
- 固定大小的 IDictionary 物件不允許加入或移除項目,但允許修改現有項目。
- 變數大小的 IDictionary 物件允許加入、移除和修改項目。
Hashtable 類別
最基本的字典集合就是 Hashtable ,它可以達到上述 Dictionary 的特性。
下表為 IDictionary 與 Hashtable 主要的屬性與方法
//IDictionary 介面的組成成員 public interface IDictionary: ICollection, IEnumerable //properties bool IsFixedSize { get; } //取得值,指出 IDictionary 物件是否具有固定的大小。 bool IsReadOnly { get; } //取得值,指出 IDictionary 物件是否為唯讀。 Object Item[Object key] { get; set; } //取得或設定具有指定索引鍵的元素。 ICollection Keys { get; } //取得 ICollection 物件,其中包含 IDictionary 物件的索引鍵。 ICollection Values { get; } //取得 ICollection 物件,其中含有 IDictionary 物件中的值。 //method void Add(Object key,Object value) //將隨附有索引鍵和值的項目加入至 IDictionary 物件。 void Clear() //將所有項目從 IDictionary 物件移除。 bool Contains(Object key) //判斷 IDictionary 物件是否包含具有指定索引鍵的項目。 void CopyTo(Array array,int index) //從特定的 Array 索引開始,複製 ICollection 項目至 Array。 (繼承自 ICollection)。 IEnumerator GetEnumerator() //傳回會逐一查看集合的列舉程式 void Remove(Object key) //將有指定索引鍵的項目從 IDictionary 物件移除。 }
//Hashtable 介面的組成成員 (Hashtable繼承自IDictionary,所以除了IDictionary的功能外,也支援了兩個可以用來測試索引鍵與值是否存在的方法。 public class Hashtable : IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback, ICloneable { public virtual bool ContainsKey(Object key) //判斷 Hashtable 集合裡否包含特定索引鍵。 public virtual bool ContainsValue(Object value) //判斷 Hashtable 集合裡是否含有特定值。 }
下面範例將以 Hashtable 為例,示範 Dictionary 類型集合的幾種操作:
- 1. 新增/移除項目
- 2. 巡訪項目
- 3. 集合項目的 Equality (相等性)
- 4. 使用 IEqualityCompare
1. 新增/移除項目
Hashtable emailLookup = new Hashtable(); //使用 Add 方法,加入項目 emailLookup.Add("shaojay", "shaojay@contoso.com"); emailLookup.Add("oldwang", "oldwang@gmail.com"); //使用 Remove 方法,移除項目 emailLookup.Remove("oldwang@gmail.com"); //透過 key 值,取得 value 值,不能使用 indexer Console.WriteLine(emailLookup["oldwang@gmail.com"]); //直接指定某個 key 值的 value, 作用等同 Add emailLookup["peter"] = "peter@gmail.com"; //直接指定某個 key 值的 value, 但因為 key 值存在,作用等同更新 emailLookup["oldwang"] = "old.wang@gmail.com";
2. 巡訪資料項目 (Iterate)
//逐一列舉 emailLookup 的成員 foreach (object obj in emailLookup) { Console.WriteLine(obj); } //System.Collections.DictionaryEntry //System.Collections.DictionaryEntry
注意
使用 foreach 陳述式時, Hashtable 裡的每個項目都是由一組 key/value 組成,因此集合項目的型別既不是 key 的型別,也不是 value 的型別。元素真正的型別為 DictionaryEntry。 所以,上例輸出的結果為 System.Collections.DictionaryEntry ,而不是我們想要的結果,如果想要顯示所有使用者的姓名,正確做法如下:
//使用 DictionaryEntry 逐一列舉 emailLookup 的成員 //Key = shaojay, Value = shao.jay@gmail.com //Key = peter, Value = peter@gmail.com foreach (DictionaryEntry entry in emailLookup) { Console.WriteLine("Key = {0}, Value = {1}", entry.Key, entry.Value); } //使用 Value 逐一列舉 emailLookup 的成員 //shao.jay@gmail.com //peter@gmail.com foreach (object value in emailLookup.Values) { Console.WriteLine(value); } //使用 Key 逐一列舉 emailLookup 的成員 //Key = shaojay, Value = shao.jay@gmail.com //Key = peter, Value = peter@gmail.com foreach (object key in emailLookup.Keys) { Console.WriteLine("Key = {0}, Value = {1}", key, emailLookup[key]); } //使用 Enumerator 逐一列舉 emailLookup 的成員 IDictionaryEnumerator enumerator = emailLookup.GetEnumerator(); DictionaryEntry dentry; while (enumerator.MoveNext()) //列舉值一開始會位於集合中的第一個元素之前 { dentry = (DictionaryEntry)enumerator.Current; //Current:取得目前項目 Console.WriteLine("Key = {0}, Value = {1}", dentry.Key, dentry.Value); }
3. 集合項目的相等性 (Equality)
Hashtable 類別是一種特定型別的字典類別,它使用一個整數值(就是雜湊值hash)來輔助索引鍵的儲存。 Hashtable 就是使用這個 hash 值來加速集合中搜尋指定的索引鍵。 .Net中的每個物件都是繼承自 Object 類別,所以這個類別也支援 GetHashCode 方法,同時利用這個方法所回傳的整數值,用以識別每一個物件。
所以,當要新增一個項目到 Hashtable 裡的時候,它會以 key 值去取得 GetHashCode,判斷物件是否相等。 但是,如果 Hashtable 發現兩個物件有相同的 hashcode,它會再呼叫 Equals 方法來判斷這二個物件實際上是否相等。
例一:下面這個例子,雖然 obj1, obj2, obj3 是不同的執行個體,但卻發生 key 值重複問題。 這是因為這三個物件的 GetHashCode 方法都會回傳相同的值,所以 Hashtable 新增資料時,會認定 key 值重複。
Hashtable hash_table = new Hashtable(); Object obj1 = "apple"; //1657858284 Object obj2 = "apple"; //1657858284 String obj3 = "apple"; //1657858284 Console.WriteLine(obj1.GetHashCode().ToString()); Console.WriteLine(obj2.GetHashCode().ToString()); Console.WriteLine(obj3.GetHashCode().ToString()); hash_table.Add(obj1,"abe"); hash_table.Add(obj2, "abe"); //--> key 值重複失敗 hash_table.Add(obj3, "abe"); //--> key 值重複失敗
例二:下面這個例子,我們建立了二個 Employee 執行個體,而且這二個執行個體也具有相同的 id 和 name,但卻不會發生 key 值重複問題。 這是因為 Object 類別在實做 GetHashCode 方法時,對於每個執行個體都會分別建立一個唯一的 hash,所以 Hashtable 自然會認定這二個執行個體是不相等的。
class Employee { string id { get; set; } string name { get; set; } public Employee(string strID, string strName) { id = strID; name = strName; } } private void button7_Click(object sender, EventArgs e) { Hashtable hash_table = new Hashtable(); Employee e1 = new Employee("E0001", "Tom"); Employee e2 = new Employee("E0001", "Tom"); //雖然 e1, e2 具有相同的 id 和 name //但是其 GetHashCode 回傳值不相同,所以下面程式不會造成錯誤 Console.WriteLine(e1.GetHashCode().ToString()); Console.WriteLine(e2.GetHashCode().ToString()); hash_table.Add(e1, 30000); hash_table.Add(e2, 30000); }
例三:所以,若希望 Hashtable 知道它們是相等的,我們就必須覆寫 Employee 類別中的 GetHashCode 方法,讓 Hashtable 知道它們是相等的。
class Employee { string id { get; set; } string name { get; set; } public Employee(string strID, string strName) { id = strID; name = strName; } public override int GetHashCode() { return id.GetHashCode(); } }
例四:上例中,雖然e1.GetHashCode()=e2.GetHashCode(),但 Hashtable 還是不會認定它們是相等的,因為 Hashtable 若發現兩個物件有相同的 hashcode,它會再呼叫 Equals 方法來判斷這二個物件實際上是否相等。 因此我們必需再覆寫 Employee 類別中的 Equals 方法,這樣子具備相同 id 的執行個體,才會被 Hashtable 認定是相等的:
class Employee { string id { get; set; } string name { get; set; } public Employee(string strID, string strName) { id = strID; name = strName; } public override int GetHashCode() { return id.GetHashCode(); } public override bool Equals(object obj) { Employee otherEmployee = obj as Employee; if (otherEmployee == null) return false; return otherEmployee.id == id; } }
備註:關於 Object.GetHashCode
- HashCode 是在相等比較時,用來識別物件的數值。它也可以做為集合中物件的索引。
- GetHashCode 方法的預設實作,無法保證不同物件的回傳值是唯一的。
- GetHashCode 也不保證這個方法在不同版本的 .NET Framework 中都能傳回相同的值。
- 因此,這個方法的預設實作不能當做雜湊用途的唯一物件辨識項。
- 實值型別必須覆寫這個方法,以提供該型別所適用的雜湊函式
- 雜湊函式 (hash function) 是用來快速產生一個對應到 object 的值(雜湊碼)。
- 雜湊函式至少要指定 instance 的一個欄位當做輸入。
- 雜湊函式必須具有下列特性:
- 1. 兩個相等的物件必須具有相同的雜湊碼。 (但物件不相等時,這個方法沒有一定要傳回不同的值)
- 2. 對於任何一個物件,不論叫用甚麼方法,其 GetHashCode 永遠都必須返回相同的值。
- 3. 對於所有輸入,雜湊函式應在所有整數中產生一隨機分佈。
- 覆寫 GetHashCode 的衍生類別也必須覆寫 Equals,以保證視為相等的兩個物件具有相同的雜湊程式碼,否則,Hashtable 型別可能無法正確執行。
4. 使用 IEqualityComparer 介面
若使用下列二行程式碼建立 Employee,這時 Hashtable 會認定此為二個不同的物件,因為,我們覆寫的 GetHashCode,是依據 id 這個屬性來回傳 hashcode。
Employee e1 = new Employee("e0001", "Tom"); Employee e2 = new Employee("E0001", "Tom");
如果我們希望建立索引鍵時要忽略字串的大小寫,除了改寫上面例子中的 GetHashCode 和 Equals 方法外,另外也可以透過實作 IEqualityComparer 介面來達成。 IEqualityComparer 可以用來定義物件相等比較的方法,而 Hashtable 也支援以實作 IEqualityComparer 介面的物件為參數,來建立新的執行個體。 IEqualityComparer 支援兩個方法: GetHashCode 與 Equals 。這些方法可以讓比較器類別自已來處理物件的相等性,而不必仰賴物件本身支援這項功能。
//這個類別實作了 IEqualityComparer 介面,提供雜湊碼與相等性的比較。 //GetHashCode:將傳入的物件轉換成小寫字母,然後才回傳其hashcode,以便不區分大小寫。 //Equals: 以內建的 CaseInsensitiveComparer 執行真正的比較。 public class myComparer : IEqualityComparer { CaseInsensitiveComparer _comparer = new CaseInsensitiveComparer(); public int GetHashCode(object obj) { return obj.ToString().ToLowerInvariant().GetHashCode(); } public new bool Equals(object A, object B) { if (_comparer.Compare(A, B) == 0) return true; else return false; } } private void button8_Click(object sender, EventArgs e) { //以 IEqualityComparer 的執行個體當做參數,建立 Hashtable Hashtable score_table = new Hashtable(new myComparer()); score_table["vito"] = 90; score_table["tom"] = 60; score_table["frank"] = 80; score_table["VITO"] = 85; //因為建立 Hashtable 時使用了這個不分大小寫的方法來比較物件, //它將 "vito" 和 "VITO" 視為相同,所以最後集合只會出現四個項目。 Console.WriteLine(score_table.Count); //3 Student s3 = new Student("Allen", 60); Student s4 = new Student("Peter", 70); Student s5 = new Student("peter", 80); score_table.Add(s3, 60); score_table.Add(s4, 70); score_table.Add(s5, 80); //--> error : key 值重複 } class Student : IComparable { string name { get; set; } int score { get; set; } public Student(string _Name, int _Score) { name = _Name; score = _Score; } //自訂比較邏輯 public int CompareTo(object obj) { Student s = obj as Student; if (s != null) return String.Compare(s.name, this.name, true); //不分大小寫 else throw new ArgumentException("Object is not a Student"); } }
若使用泛型的 IComparable<T> ,就可以減少轉型的問題。
class Student2 : IComparable<string> { string name { get; set; } int score { get; set; } public Student2(string _Name, int _Score) { name = _Name; score = _Score; } //自定比較邏輯 public int CompareTo(string other) { if (other != null) { return string.Compare(this.name, other, true); } else throw new ArgumentException("Object is not a Student"); } }
上面這個範例中,我們在集合中加入了 key 值是以字串型別的項目,因為字串或所有的實值型別都有實作 IComparable 介面,所以物件可以進行 Compare 方法。 但是例子後半段,我們在集合中加入了自訂型別,為了讓物件能夠執行 Compare 方法,該類別中就必須實作 IComparable 介面才可以進行比較。
SortedList 類別
SortedList 也是用來表示 key/value 的集合,且這個集合中的項目會按照 key 值大小自動排序,而且可以透過 key 和 index 存取項目。
SortedList score_table = new SortedList(); score_table["vito"] = 90; score_table["tom"] = 60; score_table["apple"] = 70; score_table["frank"] = 80; score_table["VITO"] = 80; foreach (DictionaryEntry entry in score_table) { Console.WriteLine("{0} = {1}", entry.Key, entry.Value); } //輸出結果:按英文字母排序 //apple = 70 //frank = 80 //tom = 60 //vito = 90 //VITO = 85 //取得key=vito這個項目在集合中的index位置 Console.WriteLine(@"index of key[""vito""] = {0}", score_table.IndexOfKey("vito").ToString()); //取得value=80這個項目在集合中第一次出現的index位置 Console.WriteLine(@"index of value[""80""] = {0}", score_table.IndexOfValue(80).ToString()); //透過 index 取得項目的值 int i = score_table.IndexOfKey("vito"); Console.WriteLine(@"item of index {0} = {1}", i.ToString(), score_table.GetByIndex(i)); //index of key["vito"] = 3 //index of value["80"] = 1 //item of index 3 = 90 //透過 SetByIndex(index, value) 設定項目的值 score_table.SetByIndex(i, 100);
SortedList在操作增減項目時,便會自動執行排序動作。另外也支援指定一個 IComparer,以便控制排序的動作。
public class DescendingComparer : IComparer { CaseInsensitiveComparer _comparer = new CaseInsensitiveComparer(); public int Compare(object x, object y) { //將二值對調,呼叫CaseInsensitiveComparer.Compare,以比較大小 return _comparer.Compare(y, x); } } private void button9_Click(object sender, EventArgs e) { SortedList score_table = new SortedList(new DescendingComparer()); score_table["apple"] = 90; score_table["tom"] = 60; score_table["vito"] = 70; foreach (DictionaryEntry entry in score_table) { Console.WriteLine("{0} = {1}", entry.Key, entry.Value); } //輸出結果:按英文字母Desc排序 //vito = 70 //tom = 60 //apple = 90 }
特殊的 Dictionaries 集合
Hashtable 和 SortedList 是二個標準字典型態的集合,為了彌補功能上的不足或提升效率,.NET 另外提供以下三種字典集合:
- ListDictionary:使用單向連結串列 (Singly-Linked List) 實作 IDictionary。集合項目若在 10 個以下,使用這個字典集合效能最好。
- HybridDictionary :使用這個集合類型,若為小型集合時,則採用 ListDictionary 實作 IDictionary ;當它項目逐漸增加成為大型集合時,則切換以 Hashtable 實作。
- OrderedDictionary :有序的 Hashtable 。可依 key 或 index 存取集合中的項目。
補充說明OrderedDictionary
- 具有Hashtable集合的特性,但多了一些Hashtable沒有的方法。
- Insert:使用指定的索引鍵和值,將新元素插入 OrderedDictionary 集合中指定的索引處。
- RemoveAt:從 OrderedDictionary 集合移除指定之索引處的元素。
- 加入集合中的內容會按照加入的順序排列;Hashtable內容存放的順序則與加入順序無關,Hashtable 會依自己的演算方式來排序順序。
OrderedDictionary score_table = new OrderedDictionary(); score_table["apple"] = 90; score_table["tom"] = 60; score_table["vito"] = 70; score_table.Insert(0, "shao", "80"); //在最前頭插入一個項目 score_table.RemoveAt(score_table.Count - 1); //移除最後一個 foreach (DictionaryEntry entry in score_table) { Console.WriteLine("{0} = {1}", entry.Key, entry.Value); } //項目的順序,依照加入位置的先後 //shao = 80 //apple = 90 //tom = 60 //也可以使用 indexer 取得項目值 Console.WriteLine(score_table[0]); //80
沒有留言:
張貼留言