2013年7月26日 星期五

Identity And Equality

先定義兩個名詞:
Identity - 兩個變數是否參考到同一個物件
Equality - 兩個變數參考到的物件的值是不是相等

System.Object.Equals() 基本上實做的是 Identity,他的 code 大概長這樣:

public class Object {
    public virtual Boolean Equals(Object rhs) {
        if(this == rhs) return true;
        return false;
    }
}

由於子代有可能 override System.Object.Equals() 以及各種比較算符 (== 或 !=),因此若要判斷兩個變數是否參考到同一個物件 (Identity),請不要使用 Equals() 或 ==,請使用 Object.ReferenceEquals(),他的 code 大概長這樣:

public class Object {
    public static Boolean ReferenceEquals(Object lhs, Object, rhs) {
        return (lhs == rhs);
    }
}

System.ValueType 是 System.Object 的子代,它 override 了 Equals() 這個 method,讓它變成實作 Equality。但由於 System.ValueType.Equals() 是靠 reflection 來實作 Equals(),因此它的效率很差。因此當程式設計師設計了自己的 Value Type,而且該 ValueType 有比較的需求時,最好提供一個 override 的 Equals() method。

大部份的 class 都沒有比較 Equality 的需求,因此不太需要 override Equals()。但 Value Type 通常會有 override Equals() 的需求。當您要 override Equals() 時,請注意下列事項:

* Reflexive -> x.Equals(x) 必須是 true
* Symmetric -> 若 x.Equals(y), 則 y.Equals(x) 必須為 true
* Transitive -> 若 x.Equals(y) 且 y.Equals(z),則 x.Equals(z) 必須為 true
* Consistent -> 只要物件沒被改變,兩次 Equals() 的結果應該要一樣。

以下是通常要做的額外項目:
* 您應該順便繼承及實作 System.IEquatable<T>
* 您應該要 override GetHashCode(),並且保證:
    # 若兩個物件 Equals(),他們的 GetHashCode() 的回傳值必須相同。
    # 只要物件未被更改,兩次 GetHashCode() 的回傳值必須相同。
* 您應該順便實作 == 算符與 != 算符
* 如果您的 type 可以被排序:
    # 您應該繼承及實做 System.IComparable
    # 您應該繼承及實做 System.IComparable<T>
    # 您應該實做所有的比大小算符,包含 <, <=, >, >=

以下是一個 override Equals 的範例. 為求簡單, 我只專注在 Equals 以及 GetHashCode 這兩個 method 上, 請務必仔細研讀!!!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {
    class Program {
        class Base {
            private Int32 val;
            public Base(Int32 val) { this.val = val; }
            // 因為父類別是 Object, 他的 Equals 其實是檢查 identity
            // 所以在這裡不應該呼叫
            public override bool Equals(object obj) {
                if(obj == null) return false;
                if(Object.ReferenceEquals(this, obj)) return true;
                if(this.GetType() != obj.GetType()) return false;
                if(this.val != ((Base)obj).val) return false;
                return true;
            }
            // 因為父類別是 Object, 
            // 他的 GetHashCode 其實是透過 this 指標的位址求得,
            // 故在這裡不應該呼叫
            public override int GetHashCode() {
                return this.val.GetHashCode();
            }
        };
        class Drived : Base {
            private Int32 val;
            public Drived(Int32 valOfDrived, Int32 valOfBase) : base(valOfBase) {
                this.val = valOfDrived;
            }
            // 請注意!!! 父類別是否是 System.Object 會影響到 Equals 的實作方式
            public override bool Equals(object obj) {
                if(!base.Equals(obj)) return false;
                if(this.val != ((Drived)obj).val) return false;
                return true;
            }
            // 請注意!!! 父類別是否是 System.Object 會影響到 GetHashCode 的實作方式
            public override int GetHashCode() {
                return base.GetHashCode() ^ this.val.GetHashCode();
            }
        }
        static void Main(string[] args) {
            Base b = new Base(1);
            Console.WriteLine(b.Equals(new Base(1)));
            Console.WriteLine(b.Equals(new Base(2)));
            Console.WriteLine(b.Equals(new Drived(2, 1)));
            Console.WriteLine(b.Equals(b));
            Console.WriteLine(b.Equals(null));
            Console.WriteLine("------------------------------");
            Drived d = new Drived(2, 1);
            Console.WriteLine(d.Equals(new Base(1)));
            Console.WriteLine(d.Equals(new Drived(1, 1)));
            Console.WriteLine(d.Equals(new Drived(2, 1)));
            Console.WriteLine(d.Equals(d));
            Console.WriteLine(d.Equals(null));
            Console.ReadKey();
        }
    }
}


出處: CLR via C# 第四版第五章

沒有留言:

張貼留言