virtual bool Equals(Object object)
Equals 這個方法很有趣,和 operator == 不一樣。Equals是個在物件之母上的virtual method。意味著所有物件都可以override掉,提供型別客製的相等性比較。
上個段落有提到C#官方建議,如果 reference type 要比 value equality,則應該用 .Equals()。但有趣的是,Object 型別上的 Equals 方法,其比較的方法是「Reference相等」,和 Object.ReferenceEquals() 行為相同。但Value-Type物件,已經將.Equals方法覆寫成byte-by-byte內容相等。
Value Type 型別應該總是覆寫掉Equals
因為Value Type的 Equals 會用反射去取得所有欄位,並byte-by-byte的比較,所以效率並不好,會建議覆寫掉並提供struct自己的實做。
Reference Type請三思而後行
要不要覆寫Reference Type的Equals method,這件事情其實很微妙。
C#設計的時候,把物件分成兩大類,一類是Value Type,意思是這類型的物件,內容才是重點,物件本身不是。因此只要內容相等,就可以視為兩個物件完全相同。一般的數字、enum、複數(Complexity)、單純的座標(x,y)等,都可以看成是Value Type。
另一類的物件是 Reference Type,意思是這類型的物件,比起內容,更重要的是物件的個體。就算兩個物件內容完全相同,依然會被視為兩個不同的個體。預設 operator == 比較對於 Reference Type 物件而言,就是判斷兩個變數是否參考到同一個物件。
覆寫Reference Type的Equals意味著改變語意
當你決定要替Reference Type覆寫Equals方法時,就代表你賦予了該Reference Type值相等(Value Equality)的語意,你必須要開始小心你現在講的「相等」,到底是 Reference Equality 還是 Value Equality。
你要注意當你使用 == 時,你想表達的是哪個相等?所以你要考慮是否多載operator ==
- 如果你沒有多載==,那麼依然是Reference Equality
- 如果你有多載==,而且行為和Equals(Object)相同,那麼代表Value Equality
- 如果你有多載==,而且行為和Equals(Object)不同,Shame On You。這會造成理解上的不一致。
你要注意的事情還有很多,因為你覆寫掉Equals(Object)了,假設這個Reference Type叫做T,在使用Dictionary<T, V>時,判斷內容是否包含T key的行為跟著被影響了。
假設T key是個Reference Type,其相等性的判斷是預設的Object.Equals(Object),預設的行為是不管內容,只管是否是同一個物件。現在這個行為被覆寫掉了,會改為根據你自訂的Equals行為判斷是否相同。
這個時候要不要改寫GetHashCode呢?如果不改寫GetHashCode,就會發生兩個Value Equality相同的物件,卻有不同的GetHashCode值,這在Dictionary的操作上會變得很怪,明明兩個相同的物件,但僅僅因為HashCode不同,導致Dictionary無法找到物件原本的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public static void Main(string[] args) { var lookup = new Dictionary<Person, int>(); var personA = new Person("Tom"); var personB = new Person("Tom"); lookup.Add(personA, 30); Console.WriteLine(lookup.ContainsKey(personA)); // true Console.WriteLine(lookup.ContainsKey(personB)); // false } internal class Person { private readonly string _name; public Person(string name) { _name = name; } public override bool Equals(object obj) { return Equals(obj as Person); } public bool Equals(Person other) { if (object.ReferenceEquals(other, null)) { return false; } if (object.ReferenceEquals(this, other)) { return true; } if (this.GetType() != other.GetType()) { return false; } return _name == other._name; } } |
加上GetHashCode後,
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public override int GetHashCode() { return _name.GetHashCode(); } public static void Main(string[] args) { var lookup = new Dictionary<Person, int>(); var personA = new Person("Tom"); var personB = new Person("Tom"); lookup.Add(personA, 30); Console.WriteLine(lookup.ContainsKey(personA)); // true Console.WriteLine(lookup.ContainsKey(personB)); // true } |
因此更好的作法可能是什麼都不做
如果你什麼都不做,不去多載Reference Type的Equals,因為GetHashCode和Equals必須要彼此配合,所以這兩個method都不用改寫。
你可以保持 == 的單純,代表Reference Equality。而且不管左右的型別是object還是T,結果都相同。
通常很少需要把Person這類的Reference Type放到dictioanry或hashset內,真的需要做這件事,也可以額外提供IEqualityComparer傳入,作為比較大小的規則。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public static void Main(string[] args) { var lookup = new Dictionary<Person, int>(new NameEqualityComparer()); var personA = new Person("Tom"); var personB = new Person("Tom"); lookup.Add(personA, 30); Console.WriteLine(lookup.ContainsKey(personA)); // true Console.WriteLine(lookup.ContainsKey(personB)); // true } internal class NameEqualityComparer : IEqualityComparer<Person> { public bool Equals(Person x, Person y) { if (x == null && y == null) { return true; } if (x == null || y == null) { return false; } return x.Name == y.Name; } public int GetHashCode(Person obj) { return obj.Name.GetHashCode(); } } internal class Person { public string Name { get; set; } public Person(string name) { Name = name; } } |