iOS判斷對象相等 重寫isEqual、isEqualToClass、hash
相等的概念是探究哲學和數學的核心,并且對道德、公正和公共政策的問題有著深遠的影響。
從一個經驗主義者的角度來看,兩個物體不能依據一些觀測標準中分辨出來,它們就是相等的。在人文方面,平等主義者認為相等意味著要保持每個人的社會、經濟、政治和他們住地的司法系統都一致。
對程序員來說,協調好邏輯和感官能力來理解我們塑造的'相同'的語義是一項任務。'相同的問題'(的探討)太微妙,同時有太容易被忽視。對語義沒有充分的理解就直接去實現它,可能會導致沒必要的工作和不正確的結果。因此對數學和邏輯系統的深刻理解與按既定計劃實現同樣必要。
雖然所有的技術博文都是有誘惑你來讀它的標題和代碼,但請花幾分鐘時間來閱讀和理解這些文字。逐字地復制看似有用的代碼而不知道為什么這樣寫很有可能導致一些錯誤。相等性是個重要話題之一,但它仍包含了許多混亂的概念,尤其是在Objective-C中。
Equality & Identity
首先,弄清楚equality和identity的區別很重要。如果兩個物體具有相同的觀測屬性,它們是可以相互等同的。但是,這兩個對象仍然可以分辨出差異,它們各自的identity。在程序中,一個對象的identity是和它的內存地址關聯的。
NSObject對象測試和另一個對象是否相同使用 isEqual: 方法,在它的基本實現里性等性檢查本質上是對identity的檢查,如果兩個對象指向了相同的內存地址,它們被認為是相等的。
@implementation NSObject (Approximate) - (BOOL)isEqual:(id)object { return self == object; } @end
對于內置的類,像NSArray, NSDictionary, 和 NSString,進行了一個深層的相等性比較,來測試在集合中的每個元素是否相等,這是一個應該也確實非常有用的做法。
NSObject 的子類要實現它們各自的 isEqual: 方法時,應該做到以下幾點:
-
1.實現一個 isEqualTo__ClassName__: 方法來執行有意義的值比較.
2.重寫 isEqual: 方法 來作類型和對象identity檢查, 回調上述的值比較方法.
3.重寫 hash, 這個會在下一部分解釋.
這里有一個NSArray實現這個的大概的思路(這個例子忽略了類簇, 實際實現會更具體復雜):
@implementation NSArray (Approximate) - (BOOL)isEqualToArray:(NSArray *)array { if (!array || [self count] != [array count]) { return NO; } for (NSUInteger idx = 0; idx < [array count]; idx++) { if (![self[idx] isEqual:array[idx]]) { return NO; } } return YES; } - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[NSArray class]]) { return NO; } return [self isEqualToArray:(NSArray *)object]; } @end
下面的在Foudation中NSObject的子類已經自定義了判等實現,用了相關的方法:
NSAttributedString -isEqualToAttributedString: NSData -isEqualToData: NSDate -isEqualToDate: NSDictionary -isEqualToDictionary: NSHashTable -isEqualToHashTable: NSIndexSet -isEqualToIndexSet: NSNumber -isEqualToNumber: NSOrderedSet -isEqualToOrderedSet: NSSet -isEqualToSet: NSString -isEqualToString: NSTimeZone -isEqualToTimeZone: NSValue -isEqualToValue:
當比較任何這些類的兩個實例時,推薦使用它們各自的高級別的method而不是 isEqual:
然而,我們的理論實現還沒有完成,現在,讓我們把注意力轉向hash(一段插曲:先清理一下NSString的問題)。
NSString判等的奇怪案例
一個有趣的插曲,看一下這個代碼:
NSString *a = @"Hello"; NSString *b = @"Hello"; BOOL wtf = (a == b); // YES
鄭重地聲明一下正確的比較兩個NSString對象相等的方法是使用 -isEqualToString: 方法,無論如何也不能通過 == 操作符來比較兩個NSString。
那么這里是怎么回事呢?為什么 NSArray或者NSDictionary字面量相同不會這樣,而它(NSString)會這樣呢。
這都是一種被稱為'字符串駐留'的優化技術做的,因為這種優化不同的值可以對一份不可變的字符串值的備份進行拷貝。NSString類型的a指針和b指針對駐留字符串 @"Hello"進行了相同的拷貝。注意這個優化僅僅對靜態聲明的不可變字符串有效。
更有趣的是,OC的selector的名字也會被當做駐留字符串存儲在一個共用的字符串pool中。
Hashing
最日常的面向對象編程來說,對象判等最主要的用法在于決定集合成員。為了讓這一步更快一些,自定義判等實現的類應該也實現hash:
-
1.對象相等是相互的([a isEqual:b] ? [b isEqual:a])
2.如果對象相等,它們的hash值必須相等([a isEqual:b] ? [a hash] == [b hash])
但是,反過來不一定成立:如果它們的hash值相等,兩個對象不一定相等。([a hash] == [b hash] ?? [a isEqual:b])
現在快速翻看一下《計算機科學》101:
hash表式編程中的基本的數據結構,它可以使NSSet & NSDictionary 快速地(O(1))查找它的元素。我們也可以通過對比著數組很好地理解hash表:
Arrays按照有序的索引存儲元素,因此一個大小為n的數組會把元素放在索引1,2直到n-1.為了確定數組中的一個元素存在了哪里,不得不一個個檢查每個位置(除非數組碰巧已經排序好,但這是另一回事)。
Hash表使用了略微不同的方法。而不是按順序存儲元素,hash表在內存中分配了n個位置,同時用一個函數來計算在這個范圍內計算一個位置。一個hash函數是確定性的,同時一個好的hash函數使用一個相對均勻的散列來生成值,而且不會有太多的計算過程。當兩個不同的對象計算出相同的hash值時,會產生hash沖突。當沖突發生時,hash表會尋找沖突點同時把新加的對象放到第一個可用的位置。當hash表變得越來越擁擠,沖突的可能性會增加,這會導致花費更多的時間來尋找空間(這就是為什么均勻散列的hash函數不菲的原因。)
一個關于實現hash函數的錯誤共識來自于隨之發生的斷言,這個錯誤的共識認為hash值必須是不同的。這個錯誤共識會導致不必要地復雜實現,包括從Java textbooks復制過來的質數的神奇咒語。實際上,一個簡單的對關鍵屬性hash值的XOR(異或運算)對于99%的情況來說已經夠用了。
技巧就是思考對象中的哪個值是關鍵的。對NSDate來說,對參照日期的時間間隔已經夠用了:
@implementation NSDate (Approximate) - (NSUInteger)hash { return (NSUInteger)abs([self timeIntervalSinceReferenceDate]); }
對UIColor來說,移位之后的RGB值是非常方便計算的For a UIColor, a bit-shifted sum of RGB components is a convenient calculation:
@implementation UIColor (Approximate) - (NSUInteger)hash { CGFloat red, green, blue; [self getRed:&red green:&green blue:&blue alpha:nil]; return ((NSUInteger)(red * 255) << 16) + ((NSUInteger)(green * 255) << 8) + (NSUInteger)(blue * 255); } @end
在子類中實現 -isEqual: 和 hash
綜合在一起,這里有一個如何在子類重寫默認的判等實現的例子:
@interface Person @property NSString *name; @property NSDate *birthday; - (BOOL)isEqualToPerson:(Person *)person; @end @implementation Person - (BOOL)isEqualToPerson:(Person *)person { if (!person) { return NO; } BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name]; BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday]; return haveEqualNames && haveEqualBirthdays; } #pragma mark - NSObject - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self isEqualToPerson:(Person *)object]; } - (NSUInteger)hash { return [self.name hash] ^ [self.birthday hash]; }
如果想滿足好奇心或者出于學究式的研究,看一下這個 Mike Ash的文章 ,解釋了通過移位和翻轉組合值如何改善了可能產生重疊(沖突)的hash.
本文完全翻譯自: http://nshipster.com/equality/