本文闡述C#中相等性比較,其中主要集中在下面兩個方面算法
==和!=運算符,何時它們能夠用於相等性比較,何時它們不適用,若是不使用,那麼它們的替代方式是什麼?shell
何時,須要自定一個類型的相等性比較邏輯安全
在闡述相等性比較,以及如何自定義相等性比較邏輯以前,咱們首先了解一下值類型比較和引用類型比較app
C#中的相等性比較有兩種:ide
默認狀況下,函數
實際上,值類型只能使用值相等(除非值類型進行了裝箱操做)。看一個簡單的例子(比較兩個數字),運行的結果爲True性能
int x = 5, y = 5; Console.WriteLine(x == y);
默認地,引用類型使用引用相等。好比下面的例子:返回Falseui
object x = 5, y = 5; Console.WriteLine(x == y);
若是x和y指向同一個對象,那麼將返回True:this
object x = 5, y = x; Console.WriteLine(x == y);
下面三個標準用於實現相等性比較:spa
下面咱們來分別闡述
1. ==和!=運算符
使用==和!=的緣由是它們是運算符,它們經過靜態函數實現相等性比較。所以,當你使用==或!=時,C#在編譯時就決定了所比較的類型,並且不會執行任何虛方法(Object.Equals)。這是你們所指望的相等行比較。好比在第一個數字比較的例子中,編譯器在編譯時就決定了執行==運算的類型是int類型,由於x和y都是int類型。
而第二個例子,編譯器決定執行==運算的類型是object類型,由於object是類(引用類型),所以對象的==運算符采起引用相等去比較x和y。那麼結果就返回False,這是由於x和y指向堆上不一樣的對象(被裝箱的int)
2. Object.Equals虛方法
爲了正確地比較第二個例子中的x和y,咱們可使用Equals虛方法。System.Object中定義了Equals虛方法,它適用於全部類型
object x = 5, y = 5; Console.WriteLine(x.Equals(y));
Equals在程序運行時決定比較的類型--根據對象的實際類型進行比較。在上面的例子中,會調用Int32的Euqals方法,該方法使用值相等進行比較,因此上面的例子返回True。若是x和y是引用類型,那麼調用引用相等進行比較;若是x和y是結構類型,那麼Equals會調用結構每一個成員對應類型的Equals方法進行比較。
看到這裏,你可能會想,爲何C#的設計者不把==設計成virtaul,從而使其與Equals同樣,以免上訴缺陷。這是由於:
簡而言之,複雜的設計反映了複雜的場景:相等的概念涉及到許多場景。
而Euqals方法,適用於比較兩個未知類型的對象,下面的這個方法就適用於比較任何類型的兩個對象:
public static bool AreEqual(object obj1, object obj2) { return obj1.Equals(obj2); }
可是,該函數不能處理第一個參數是null的情形,若是第一個函數是null,你會獲得NullReferenceException異常。所以咱們須要對該函數進行修改:
public static bool AreEqual(object obj1, object obj2) { if (obj1 == null) return obj2 == null; return obj1.Equals(obj2); }
object的靜態Equals方法
object類還定義了一個靜態Equals方法,它的做用與AreEquals方法同樣。
public static bool Equals(Object objA, Object objB) { if (objA==objB) { return true; } if (objA==null || objB==null) { return false; } return objA.Equals(objB); }
這樣就能夠對編譯時不知道類型的null對象進行安全地比較。
object x = 5, y = 5; Console.WriteLine(object.Equals(x, y)); // -> True x = null; Console.WriteLine(object.Equals(x, y)); // -> False y = null; Console.WriteLine(object.Equals(x, y)); // -> True Console.WriteLine(x.Equals(y)); // -> NullReferebceException, because x is null
請注意,當編寫Generic類型時,下面的代碼將不能經過編譯(除非把==或!=運算符替換成Object.Equals方法的調用):
public class Test<T> : IEqualityComparer<T> { T _value; public void SetValue(T newValue) { // Operator '!=' cannot be applied to operands of type 'T' and 'T' // it should be : if(!object.Equals(newValue, _value)) if (newValue != _value) _value = newValue; } }
object的靜態ReferenceEquals方法
有時候,你須要強行比較兩個引用是否相等。這個時候,你就須要使用object.ReferenceEquals:
internal class Widget { public string UID { get; set; } public override bool Equals(object obj) { if (obj == null) return this == null; if (!(obj is Widget)) return false; Widget w = obj as Widget; return this.UID == w.UID; } public override int GetHashCode() { return this.UID.GetHashCode(); } public static bool operator == (Widget w1, Widget w2) { return w1.Equals(w2); } public static bool operator !=(Widget w1, Widget w2) { return !w1.Equals(w2); } } static void Main(string[] args) { Widget w1 = new Widget(); Widget w2 = new Widget(); Console.WriteLine(w1==w2); // -> True Console.WriteLine(w1.Equals(w2)); // -> True Console.WriteLine(object.ReferenceEquals(w1, w2)); // -> False Console.ReadLine(); }
之因此調用ReferenceEquals方法,這是由於自定義類Widget重寫了object類的虛方法Equals;此外,該類還重寫了操做符==和!=,所以執行==時操做也返回True。因此,調用ReferenceEquals能夠確保返回引用是否相等。
3. IEquatable<T>接口
調用object.Equals方法實際上對進行比較的值類型進行了裝箱操做。在對性能有較高要求的場景,那麼就不適合使用這種方式。從C#2.0開始,經過引入IEquatable<T>接口來解決這個問題
public interface IEquatable<T> { bool Equals(T other); }
當實現IEquatable接口之口,調用接口方法就等同於調用objet的虛方法Equals,可是接口方法執行地更快(不須要類型轉換)。大多數.NET基本類型都實現了IEquatable<T>接口,你還能夠爲Generic類型添加IEquatable<T>限制
internal class Test<T> where T : IEquatable<T> { public bool IsEqual(T t1, T t2) { return t1.Equals(t2); } }
若是,咱們移除IEquatable<T>限制,Test<T>類仍能夠經過編譯,可是t1.Equals(t2)將使用object.Equals方法。
4. 當Equals結果與==的結果不一致
在前面的內容中,咱們已經提到有時候,==或equals適用於不一樣的場景。好比:
double x = double.NaN; Console.WriteLine(x == x); // False Console.WriteLine(x.Equals(x)); // True
這是由於double類型的==運算符強制NaN不等於其餘任何值,即便另一個NaN。從數學的角度來說,兩個確實不相等。而Equals方法,由於具備對稱性,因此x.Equals(x)總返回True。
集合與字典正是依賴於Equals的對稱性,不然就不能找到已經保存在集合或字典中的元素。
對於值類型而言,Equals和==不多出現不一樣的相等性。而在引用類型中,則比較常見。通常地,引用類型的建立者重寫Equals方法執行值相等比較,而保留==執行引用相等比較。好比StringBuilder類就是這樣的:
StringBuilder buffer1 = new StringBuilder("123"); StringBuilder buffer2 = new StringBuilder("123"); Console.WriteLine(buffer1 == buffer2); // False Console.WriteLine(buffer1.Equals(buffer2)); // True
回顧一下默認的比較行爲
進一步,
有時候,在建立類型時,須要重寫上述行爲,通常在下面兩種情形下須要重寫:
1)更改相等的意義
當默認的==和Equals不適用(不符合天然規則,或悖離了使用者的指望)於自定義類型時,就須要更改相等的意義。好比DateTimeOffset結構,其有兩個私有成員:一個DateTime類型的UTC,以及int類型的offset。若是是你在建立DateTimeOffset類型,那麼你極可能只要UTC字段相等便可,而不去比較Offset字段。另一個例子就是支持NaN的數字類型,好比float和double,若是你來建立這兩個類型,你可能會但願NaN也是能夠進行比較的。
而對於Class類型,不少時候,使用值比較更有意義。尤爲是一些包含較少數據的類,好比System.Uri或System.String
2)提升結構類型的比較速度
結構類型的默認比較算法相對較慢。經過重寫Equals方法能夠提升5%的性能。而重載==運算和實現IEquatable<T>能夠在不裝箱操做的狀況下實現相等性比較,這使得提升5%性能變得可能。
對於自定義相等比較,有一個特殊的情形,更改結構類型的hashing算法後,hashtable能夠得到更好的性能。這是由於hashing算法和相等性比較都發生在棧上。
3)如何重寫相等
總地來講,有下面三種方式:
I)重寫GetHashCode
object對象的虛方法GetHashCode,也就僅僅對於Hashtable類型和Dictionary<TKey,TValue>類型有益。
這兩個類型都是哈希表集合,集合中的每一個元素都是一個鍵值用於存儲元素和獲取元素。哈希表使用了一個特定的策略以有效地基於元素的鍵值分配元素。這就要求每一個鍵值都有一個Int32數(或哈希碼)。哈希碼不只對於每一個鍵值是惟一的,並且還必須有較好的性能。哈希表認爲object類定義的GetHashCode方法已經足夠了,所以這兩個類型都省略了獲取哈希碼的方法。
不管值類型仍是引用類型,都默認實現了GetHashCode方法,因此你不用重寫這個方法,除非你須要重寫Equals方法。(所以,若是你重寫了GetHashCode方法,那麼你確定是須要重寫Equals方法)。
是否須要重寫GetHashCode方法,能夠參考下面的規則:
爲了提升哈希表的性能,GetHashCode須要重寫以防止不一樣的值返回相同的哈希碼。也這就說明了爲何須要對結構類型須要重寫Equals和GetHashCode方法,所以這樣重寫比默認的哈希算法更有效率。結構類型的GetHashCode方法的默認實現是在運行時才發生,並且極可能基於結構的每一個成員而實現。
// char type public override int GetHashCode() { return (int)m_value | ((int)m_value << 16); } // int32 public override int GetHashCode() { return m_value; }
而類(class)類型,GetHashCode方法的默認實現基於內部對象標識,這個標識在CLR中對於每一個對象實例都是惟一的。
public virtual int GetHashCode() { return RuntimeHelpers.GetHashCode(this); }
II)重寫Equals
object.Equal的規定(公理)以下:
III)重載==和!=
除了可重寫Equals,還能夠重載等於和不等於運算符。
對於結構類型,基本都重載了等於和不等於運算符,若是不重載它們,那麼對於結構類型,等於和不等於將返回錯誤的結果;
而對於類(class)類型,有兩種處理方式:
第一種實現適用於大多數自定義類型,特別是可變(mutable)類型。它確保了自定義類型符合==和!=就應該執行引用相等性的比較,從而不會誤導這些自定義的使用者。再次回顧一下前面舉過的StringBuilder例子
StringBuilder buffer1 = new StringBuilder("123"); StringBuilder buffer2 = new StringBuilder("123"); Console.WriteLine(buffer1 == buffer2); // False, Reference equality Console.WriteLine(buffer1.Equals(buffer2)); // True, Value equality
而第二種實現適用於使用者永遠都不但願自定義類型執行引用相等。通常地這些都類型都是不可變(immutable)類型,好比string類型和System.Uri類型,固然也包含一些引用類型。
III)實現IEquatable<T>
爲了保持完整性,建議在重寫Equals方法時,同時實現IEquatable<T>接口。接口方法的結果應當與自定義重寫後Equals方法的結果一致。若是你已經重寫了Equals方法,那麼實現IEquatable<T>不須要額外的實現代碼(直接調用Equlas方法便可)
internal class Staff : IEquatable<Staff> { public string FirstName { get; set; } // implements IEquatable<Staff> public bool Equals(Staff other) { return this.FirstName.Equals(other.FirstName); } // override Equals public override bool Equals(object obj) { if (obj == null) return this == null; if (!(obj is Staff)) return false; Staff s = obj as Staff; return this.FirstName == s.FirstName; } // override GetHashCode public override int GetHashCode() { return this.FirstName.GetHashCode(); } }
IV)可插入的相等比較器
若是你但願一個類型在某一個特定的場景下使用不一樣的比較,那麼你可使用可插件式的IEqualityComparer。它特別適用於集合類。(後續有內容介紹)
C#類庫中,爲相等性比較設計了三個接口:IEquatable<T>,IEqualityComparer,以及IEqualityComparer<T>。
IEqualityComparer與IEqualityComparer<T>的差異很簡單,一個是非Generic的,須要把T轉換成Object,而後調用Object的Equals方法;然後者直接調用T類型實例的Equals方法。
那麼IEquatable<T>和IEqualityComparer<T>有什麼差異,分別適用於什麼場景呢?
1. IEquatable<T>用於比較與本身類型相同的另外一個對象是否相等;而IEqualityComparer<T>則用於比較兩個相同類型的實例是否相等。
2. 若是兩個實例是否相等只有一種可能,或者有幾個是否相等的比較但只有其中一個更有意義,那麼應該選擇IEquatable<T>,T類型本身實現IEquatable<T>接口。所以IEquatable<T>的實例本身就知道該如何比較本身和另一個實例。與之相反,若是須要比較的實例之間存在多個相等性比較,那麼IEqualityComparer<T>更適合這種狀況;這個接口不會由T類型實現,相反須要一個外部的類實現IEqualityComparer<T>接口。由於,當比較兩個類型實例是否相等時,由於T類型內部不知道如何比較,那麼你就須要顯示地指定一個IEqualityComparer<T>實例用於執行相等性比較從而知足特定的需求。
3. 例子
internal class Staff : IEquatable<Staff> { public string FirstName { get; set; } public string Title { get; set; } public string Dept { get; set; } public override string ToString() { return string.Format( "FirstName:{0}, Title:{1}, Dept:{2}", FirstName, Title, Dept); } // implements IEquatable<Staff> public bool Equals(Staff other) { return this.FirstName.Equals(other.FirstName); } // override Object.GetHashCode public override int GetHashCode() { return this.FirstName.GetHashCode(); } } internal class StaffTitleComparer : IEqualityComparer<Staff> { public bool Equals(Staff x, Staff y) { return x.Title == y.Title; } public int GetHashCode(Staff obj) { return obj.Title.GetHashCode(); } } internal class StaffDeptComparer : IEqualityComparer<Staff> { public bool Equals(Staff x, Staff y) { return x.Dept == y.Dept; } public int GetHashCode(Staff obj) { return obj.Dept.GetHashCode(); } } static void Main(string[] args) { IList<Staff> staffs = new List<Staff> { new Staff{FirstName="AAA", Title="Manager", Dept="Sale"}, new Staff{FirstName="BBB", Title="Accountant", Dept="Finance"}, new Staff{FirstName="BBB", Title="Accountant", Dept="Finance"}, new Staff{FirstName="AAA", Title="Sales", Dept="Sale"}, new Staff{FirstName="ABA", Title="Manager", Dept="HR"} }; Print("All Staffs", staffs); Print("No duplicated first name", staffs.Distinct()); Print("No duplicated title", staffs.Distinct(new StaffTitleComparer())); Print("No duplicated department", staffs.Distinct(new StaffDeptComparer())); Console.ReadLine(); } private static void Print(string group, IEnumerable<Staff> staffs) { Console.WriteLine(group); foreach (Staff s in staffs) Console.WriteLine(s.ToString()); Console.WriteLine(); }
--update--
最後一個例子,還能夠經過擴展IEnumeable<T>來實現DistinctBy:
public static class IEnurambleExtension { public static IEnumerable<TSource> DistinctBy<TSource, TKey> (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) { HashSet<TKey> keys = new HashSet<TKey>(); foreach (TSource element in source) if (keys.Add(keySelector(element))) yield return element; } }
能夠這樣使用
staffs.DistinctBy(s => s),注意,staff類須要實現IEquatable<T>(或重寫Equals和GetHashCode)
staffs.DistinctBy(s => s.Dept),這就省去了編寫StaffDeptComparer類
進一步,若是staff的某個字段是一個類,那麼這個類一樣須要實現IEquatable<T>(或重寫Equals和GetHashCode)
1. C# 5.0 in a Nutshell;
2. MSDN, IEquatable<T>, http://msdn.microsoft.com/en-us/library/ms131187.aspx;
3. MSDN IEqualityComparer, http://msdn.microsoft.com/en-us/library/ms132151.aspx;
4. Stackoverflow, http://stackoverflow.com/questions/9316918/what-is-the-difference-between-iequalitycomparert-and-iequatablet