翻譯自 Mike Ash 的 Implementing Equality and Hashinghtml
對象判等是一個基本的概念,在代碼中常常會被使用到。在 Cocoa 編程中,它經過 isEqual:
方法被實現。一些比較簡單的例子像[array indexOfObject:]
會在底層使用到它,因此說對象支持判等是很是重要的。算法
在 Cocoa 編程中,它已經爲咱們在 NSObject 中提供了一個默認的判等實現。這個默認的實現只是經過對象的指針地址來進行判等。換句話說,一個對象只會與它本身相等。這個默認實現從功能上看以下面代碼所示:編程
- (BOOL)isEqual: (id)other {
return self == other;
}
複製代碼
雖然該默認的判等實現看上去過於簡單,但實際上它對於許多對象來講,是十分有用的。好比,咱們永遠不會把一個 NSView 看作跟另一個 NSView 同等。一樣的,對於不少具備該特性的類來講,這個默認的判等實現已是足夠了。這或許是個好消息,由於這意味着若是你的類具備相同的判等特性,那麼你不須要作任何事情就能夠免費獲得想要的結果。數組
有時候你須要實現更深層次的判等。這對於許多對象來講是很正常的,特別是那些被看做「值類型的對象」,他們是根據邏輯上的判等來區分。舉個例子:緩存
// 使用可變類型保證生成的是不一樣的字符串對象
NSMutableString *s1 = [NSMutableString stringWithString: @"Hello, world"];
NSMutableString *s2 = [NSMutableString stringWithString: @"%@, %@", @"Hello", @"world"];
BOOL equal = [s1 isEqual: s2]; // 返回 YES !
複製代碼
固然啦,NSMutableString 在這種狀況下已經爲你作了判等實現。可是若是你想要爲自定義的對象作一樣的操做該怎麼辦?安全
MyClass *c1 = ...;
MyClass *c2 = ...;
BOOL equal = [c1 isEqual: c2];
複製代碼
在這種狀況下你須要實現你本身版本的isEqual:
方法。bash
測試相等性在大多數狀況下是至關簡單的。把你的類中全部相關的屬性收集起來,再測試他們的相等性。若是他們當中有不相等的,那麼返回 NO ,不然返回 YES 。數據結構
有一個微妙的點就是,當你的對象所對應的類也是檢測相等性中的一個重要的屬性。去檢測 MyClass 和 NSString 的相等性是十分合理的,可是這種比較的結果永遠不會返回 YES (除非 MyClass 是 NSString 的一個子類)。測試
有一個稍微不那麼微妙的點就是,確保你測試的屬性對於判等來講是很是重要的。一些像緩存 caches 這樣的屬性對於你的對象的外部視角而言是可有可無的,那麼它就不須要被用做判等的因素。ui
好比說你的類看起來像這樣:
@interface MyClass: NSObject {
int _length;
char *_data;
NSString *_name;
NSMutableDictionary *_cache;
}
複製代碼
你的判等實現看起來會像這樣:
- (BOOL)isEqual: (id)other {
return ([other isKindOfClass: [MyClass class]] && [other length] == _length && memcmp([other data], _data, _length) == 0 && [[other name] isEqual: _name])
// 注意:沒有 _cache 的比較
}
複製代碼
哈希表是一個普通的數據結構,被用於實現 NSDictionary 和 NSSet 等。不管你往容器類中添加多少對象,都可以支持快速查找到相應的對象。
若是你已經瞭解哈希表是如何工做的,你能夠直接跳過接下來的一到兩個段落內容。
哈希表基本上能夠被看作是一個帶特殊索引的龐大數組。全部被添加到數組的對象都會有一個索引關聯着他們的哈希值。這個哈希值本質上是由對象的屬性而產生的僞隨機的數字。這種機制使得索引有足夠的隨機性,那麼兩個對象就不太可能擁有相同的哈希值了,但這是徹底可複寫的。當一個對象被插入到數組中時,它的哈希值會被用來決定它該被放到哪一個位置上。當一個對象被查找時,它的哈希值會被用來決定到哪一個位置中查找。
用更加正式的術語來說,一個對象的哈希值被定義了,若是兩個對象是相等的,那麼他們會有相同的哈希值。要注意的是,反過來講是不正確的,也不該該這樣,由於:兩個對象能夠有相同的哈希值,可是他們能夠不相等。你想要儘量的避免出現這種狀況,由於當兩個不相等的對象擁有兩個相同的哈希值(稱爲碰撞),那麼哈希表就必須採起特殊的措施去處理這種狀況,這是一個很是耗時的操做。然而,這已經被證實了要想徹底避免哈希碰撞的發生是不可能的。
在 Cocoa 編程中,哈希的實現經過哈希方法,它的方法簽名爲:
- (NSUInteger)hash;
複製代碼
跟對象判等同樣,NSObject 也爲你提供了一個默認的哈希實現,但這是經過使用對象的標識來實現的。粗略的講,它作了這些事情:
- (NSUInteger)hash {
return (NSUInteger)self;
}
複製代碼
實際返回的值可能不同,但本質的關鍵點是,這種方式是基於實際指向 self 的指針的值。跟判等方法同樣,若是基於對象標識的判等已經達到你想要的需求,那麼默認的實現對你來講已是有用的了。
由於哈希的語義,若是你重寫了isEqual:
方法,你就必須重寫哈希方法。若是你不這樣作,你會遇到兩個相同對象卻擁有不一樣的哈希值的狀況,這是十分不安全的。若是你在字典、集合或者其餘須要哈希表的地方使用到這些對象,那麼會出現問題。
由於對象哈希值的定義和相等性的關係是十分密切的,一樣的,哈希方法的實現和判等方法的實現也十分密切。
一個例外的狀況是,不須要在哈希值的定義中包含你的對象所屬的類。這主要是做爲isEqual:
方法的一個保護措施,是爲了確保跟不一樣對象之間比較時剩餘內容的檢測有意義。若是經過不一樣的數學方式去合併不一樣屬性的哈希值,那麼你的哈希值極可能跟其餘不一樣的類的哈希值相比就會很是不同。
檢測屬性的相等性一般來講是很簡單的,但計算他們的哈希值卻不老是那麼簡單。你如何計算一個屬性的哈希值取決於對象的類型是什麼。
對於數值型屬性,哈希值能夠被簡單的設定爲數字的值。
對於對象型屬性,你能夠經過調用對象的哈希方法,來使用其返回的哈希值。
對於數據型屬性,你會想要使用一些哈希算法來生成哈希值。你可使用 CRC32 ,或者重量型的 MD5 。後者的執行速度相對較慢,但便於使用,它經過把數據封裝在 NSData 中,而且獲取它的哈希值。在上面的例子中,你能夠像這樣計算出 _data 的哈希值:
[[NSData dataWithBytes: _data length: _length] hash];
複製代碼
因此你已經知道了如何爲每一個屬性生成哈希值,可是要如何將他們合併在一塊兒呢?
最簡單的方式就是將他們相加在一塊兒,或者使用按位或的特性。然而,這會破壞哈希值的獨特性,由於這些操做都是對稱性的,意味着區分不一樣對象時會出錯。舉個例子,假設一個對象有 first 和 last name 兩個屬性,它的哈希方法的實現以下:
- (NSUInteger)hash {
return [_firstName hash] ^ [_lastName hash];
}
複製代碼
如今假設你有兩個對象,一個是 「George Frederick」 ,另外一個是 「Frederick George」。即便他們很明顯是不一樣的,但他們仍是會有相同的哈希值。雖然哈希碰撞的發生是徹底不可避免的,但咱們也應該儘可能讓這種狀況不輕易出現。
如何合併哈希值是一個複雜的主題,是沒法用一個回答就能解釋的。然而,使用任何不對稱的方式去合併哈希值倒是一個很好的開始。我打算使用位移運算加上按位異或預算來合併他們。
#define NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
#define NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (NSUINT_BIT - howmuch)))
複製代碼
- (NSUInteger)hash {
return NSUINTROTATE([_firstName hash], NSUINT_BIT / 2) ^ [_firstName hash];
}
複製代碼
如今咱們能夠運用上述內容來爲前面的例子生成一個哈希值。這跟判等方法的實現同樣會遵循一些基本的格式,而且會使用上述的技術去獲取和合並每一個屬性的哈希值。
- (NSUInteger)hash {
NSUInteger dataHash = [[NSData dataWithBytes: _data length: _length] hash];
return NSUINTROTATE(dataHash, NSUINT_BIT / 2) ^ [_name hash];
}
複製代碼
若是你還有更多的屬性,你能夠添加更多的位移運算和按位或操做,並且這個流程都是相似的。你還可能會想要爲每個屬性調整位移運算來使得他們每個都是不一樣的。
你必需要注意當你子類化的是一個實現了自定義的哈希方法和判等方法的父類。尤爲是你的子類不該該暴露那些在判等方法的實現中使用到的新的屬性。若是你這樣作了,那麼該子類的實例確定與父類的實例不相等。
爲了解釋這種狀況,假設一個子類擁有 first/last name 屬性,且包含一個 birthday 屬性,並且 birthday 做爲判等計算的一部分。然而,這不能夠用在父類的實例中比較相等性,因此它的判等方法看起來像這樣:
- (BOOL)isEqual: (id)other {
// 筆者注:若是調用父類的判等實現的結果返回了 NO ,那麼不用比較新屬性(若是有)也可知道確定也不相等。
if(![super isEqual: other])
return NO;
// 若是執行到這一步,證實經過父類的判等實現的結果返回的是 YES ,接下來觀察要判斷 other 是不是子類或者子類的子類類型,若是不是,則證實要判等的兩個對象實質上是同一個父類對象。
if(![other isKindOfClass: [MyClass class]])
return YES;
// 若是執行到這一步,證實要判等的是兩個子類類型,並且對於父類中的屬性已被證實是相等的,那麼接下來繼續判斷新屬性是否相等便可。
return [[other birthday] isEqual: _birthday];
}
複製代碼
如今假設你有一個父類的實例對應 「John Smith」 ,我稱之爲 A ,和一個子類實例對應 「John Smith」,而且生日爲 5/31/1982,我稱之爲 B 。由於有了上述的判等定義,那麼結果爲,A 等於 B ,B 也等於他本身,獲得了指望的結果。
如今假設你有一個子類的實例對應 「John Smith」 ,生日爲 6/7/1994,我稱之爲 C 。那麼 C 不等於 B ,獲得咱們指望的結果。 C 等於 A ,一樣獲得指望的結果。可是如今出現了一個問題,A 等於 B 和 C ,可是 B 和 C 不相等!這打破了相等操做的傳遞性,而且會形成很是意外的後果。
一般來說這不該該是一個嚴重的問題。若是你的子類添加了會影響父類對象判等的新屬性,這是你的類層級結構中的一個明顯的設計問題。你應該去考慮如何從新設計你的類層級結構,而不是在isEqual:
方法中作一些複雜的實現。
若是你想要在 NSDictionary 中使用你的對象來做爲 key 值,你須要實現對應的哈希方法和判等方法,並且你也須要實現-copyWithZone:
方法。作這樣的技巧已經超出了本文的內容,但你應該意識到在某些狀況下你須要作更多事情。
在 Cocoa 編程中已經爲你提供了哈希方法和判等方法的默認實現,這對於許多對象而言是有用的,可是若是你想要爲你本身的對象即便在內存地址是不相同的狀況下,也想要經過判等結果返回 YES 來指明他們是相等的,那麼你就必需要作一點額外的工做。幸運的是,這實現起來並不困難,而且一旦你實現了他們,你自定義的類將能夠用在許多 Cocoa 的集合類中。