2012年1月16日 星期一

Dictionaries

什麼是 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 的特性。

下表為 IDictionaryHashtable 主要的屬性與方法

//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");

如果我們希望建立索引鍵時要忽略字串的大小寫,除了改寫上面例子中的 GetHashCodeEquals 方法外,另外也可以透過實作 IEqualityComparer 介面來達成。 IEqualityComparer 可以用來定義物件相等比較的方法,而 Hashtable 也支援以實作 IEqualityComparer 介面的物件為參數,來建立新的執行個體。 IEqualityComparer 支援兩個方法: GetHashCodeEquals 。這些方法可以讓比較器類別自已來處理物件的相等性,而不必仰賴物件本身支援這項功能。

//這個類別實作了 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 集合

HashtableSortedList 是二個標準字典型態的集合,為了彌補功能上的不足或提升效率,.NET 另外提供以下三種字典集合:

補充說明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

沒有留言:

張貼留言