iOS開發 之 不要告訴我你真的懂isEqual與hash!
http://www.jianshu.com/p/915356e280fc
目錄
為什麼要有isEqual方法?
isEqual方法的作用大家肯定是知道的:
判斷兩個物件是否相等
但是判斷相等不是已經有==運算子了麼, 為什麼還要isEqual方法?
這是因為:
對於基本型別, ==運算子比較的是值; 對於物件型別, ==運算子比較的是物件的地址(即是否為同一物件)
注意: 上述==運算子的說明適用於Objective-C和Java等不支援運算子過載的語言, 支援運算子過載的語言有C++
所以要理清==運算子和isEqual方法的區別, 問題就集中在
什麼叫比較物件的地址, 什麼叫比較物件
我們通過下面的例子來說明這個問題
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSLog(@"color1 == color2 = %@" , color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1 isEqual:color2] = %@", [color1 isEqual:color2] ? @"YES" : @"NO");
列印結果如下
color1 == color2 = NO
[color1 isEqual:color2] = YES
從上面的例子可以看出, ==運算子只是簡單地判斷是否是同一個物件, 而isEqual方法可以判斷物件是否相同, 例如UIColor物件表示的color是否相同
如何重寫自己的isEqual方法?
對於Cocoa Framework中定義的型別, 例如上面例子中的UIColor, isEqual方法已經實現好了
常見型別的isEqual方法還有NSString isEqualToString / NSDate isEqualToDate / NSArray isEqualToArray / NSDictionary isEqualToDictionary / NSSet isEqualToSet, 更多參考Equality
但對於自定義型別來說, 通常需要重寫isEqual方法
通過下面的例子, 我們來看看重寫isEqual方法的正確姿勢
<!--more-->
首先定義Person類如下
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@end
Person類中實現的isEqual方法如下
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}
- (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;
}
上述程式碼主要步驟如下
-
Step 1: ==運算子判斷是否是同一物件, 因為同一物件必然完全相同
-
Step 2: 判斷是否是同一型別, 這樣不僅可以提高判等的效率, 還可以避免隱式型別轉換帶來的潛在風險
-
Step 3: 通過封裝的isEqualToPerson方法, 提高程式碼複用性
-
Step 4: 判斷person是否是nil, 做引數有效性檢查
-
Step 5: 對各個屬性分別使用預設判等方法進行判斷
-
Step 6: 返回所有屬性判等的與結果
isEqual的實現並不複雜, 但是從程式碼質量(效率, 安全, 複用)來說, 上述實現仍然值得仔細學習和借鑑
除了上面的最佳實踐, 還有一種最不佳實踐
@implementation NSDate (Approximate)
- (BOOL)isEqual:(id)object {
return YES;
}
@end
這裡的isEqual方法一直返回YES
NSLog(@"[self.date1 isEqual:@\"hello\"] = %@", [self.date1 isEqual:@"hello"] ? @"YES" : @"NO");
列印結果如下
[self.date1 isEqual:@"hello"] = YES
這個有趣的實驗說明: 物件的判等可以完全由您決定, 即使兩個完全不同的物件
為什麼要有hash方法?
這個問題要從Hash Table這種資料結構說起
首先我們看下如何在陣列中查詢某個成員
-
Step 1: 遍歷陣列中的成員
-
Step 2: 將取出的值與目標值比較, 如果相等, 則返回該成員
在陣列未排序的情況下, 查詢的時間複雜度是O(array_length)
為了提高查詢的速度, Hash Table出現了
當成員被加入到Hash Table中時, 會給它分配一個hash值, 以標識該成員在集合中的位置
通過這個位置標識可以將查詢的時間複雜度優化到O(1), 當然如果多個成員都是同一個位置標識, 那麼查詢就不能達到O(1)了
重點來了:
分配的這個hash值(即用於查詢集合中成員的位置標識), 就是通過hash方法計算得來的, 且hash方法返回的hash值最好唯一
和陣列相比, 基於hash值索引的Hash Table查詢某個成員的過程就是
-
Step 1: 通過hash值直接找到查詢目標的位置
-
Step 2: 如果目標位置上有多個相同hash值得成員, 此時再按照陣列方式進行查詢
hash方法什麼時候被呼叫?
帶著這個問題, 我們來看下面的例子
Person *person1 = [Person personWithName:kName1 birthday:self.date1];
Person *person2 = [Person personWithName:kName2 birthday:self.date2];
NSMutableArray *array1 = [NSMutableArray array];
[array1 addObject:person1];
NSMutableArray *array2 = [NSMutableArray array];
[array2 addObject:person2];
NSLog(@"array end -------------------------------");
NSMutableSet *set1 = [NSMutableSet set];
[set1 addObject:person1];
NSMutableSet *set2 = [NSMutableSet set];
[set2 addObject:person2];
NSLog(@"set end -------------------------------");
NSMutableDictionary *dictionaryValue1 = [NSMutableDictionary dictionary];
[dictionaryValue1 setObject:person1 forKey:kKey1];
NSMutableDictionary *dictionaryValue2 = [NSMutableDictionary dictionary];
[dictionaryValue2 setObject:person2 forKey:kKey2];
NSLog(@"dictionary value end -------------------------------");
NSMutableDictionary *dictionaryKey1 = [NSMutableDictionary dictionary];
[dictionaryKey1 setObject:kValue1 forKey:person1];
NSMutableDictionary *dictionaryKey2 = [NSMutableDictionary dictionary];
[dictionaryKey2 setObject:kValue2 forKey:person2];
NSLog(@"dictionary key end -------------------------------");
為了看清楚hash方法是否被呼叫, 我們重寫hash方法如下
- (NSUInteger)hash {
NSUInteger hash = [super hash];
NSLog(@"hash = %ld", hash);
return hash;
}
列印結果如下
person1 == person2 = NO
[person1 isEqual:person2] = NO
isEqual end -------------------------------
array end -------------------------------
hash = 7809196951631946839
hash = 7809196951631946839
hash = 7809191961023760480
hash = 7809191961023760480
set end -------------------------------
dictionary value end -------------------------------
hash = 7809196951631946839
hash = 7809196951631946839
hash = 7809191961023760480
hash = 7809191961023760480
dictionary key end -------------------------------
從列印結果可以看到:
hash方法只在物件被新增至NSSet和設定為NSDictionary的key時會呼叫
NSSet新增新成員時, 需要根據hash值來快速查詢成員, 以保證集合中是否已經存在該成員
NSDictionary在查詢key時, 也利用了key的hash值來提高查詢的效率
hash方法與判等的關係?
hash方法主要是用於在Hash Table查詢成員用的, 那麼和我們要討論的isEqual()有什麼關係呢?
為了優化判等的效率, 基於hash的NSSet和NSDictionary在判斷成員是否相等時, 會這樣做
-
Step 1: 整合成員的hash值是否和目標hash值相等, 如果相同進入Step 2, 如果不等, 直接判斷不相等
-
Step 2: hash值相同(即Step 1)的情況下, 再進行物件判等, 作為判等的結果
簡單地說就是
hash值是物件判等的必要非充分條件
如何重寫自己的hash方法?
很多人在iOS開發中, 都是這麼重寫hash方法的
- (NSUInteger)hash {
return [super hash];
}
這樣寫有問題麼? 帶著這個問題, 我們先來看下[super hash]的值到底是什麼
Person *person = [[Person alloc] init];
NSLog(@"person = %ld", (NSUInteger)person);
NSLog(@"[person1 getSuperHash] = %ld", [person getSuperHash]);
列印結果如下
person = 140643147498880
[person1 getSuperHash] = 140643147498880
由此可以看出, [super hash]返回的就是該物件的記憶體地址
聯想到前面對hash值唯一性的要求, 使用物件的記憶體地址作為hash值不是很好麼?
別急, 我們新增如下兩個物件到NSSet中試試
Person *person1 = [Person personWithName:kName1 birthday:self.date1];
Person *person2 = [Person personWithName:kName1 birthday:self.date1];
NSLog(@"[person1 isEqual:person2] = %@", [person1 isEqual:person2] ? @"YES" : @"NO");
NSMutableSet *set = [NSMutableSet set];
[set addObject:person1];
[set addObject:person2];
NSLog(@"set count = %ld", set.count);
此時列印結果如下
[person1 isEqual:person2] = YES
set count = 2
isEqual相等的兩個物件都加入到了NSSet中(set count = 2), 所以直接返回[super hash]是不正確的
那麼hash方法的最佳實踐到底是什麼呢?
大神Mattt Thompson在Equality中給出的結論就是
In reality, a simple XOR over the hash values of critical properties is sufficient 99% of the time(對關鍵屬性的hash值進行位或運算作為hash值)
對於上面Person類的hash方法實現如下
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash];
}
更多關於位運算的討論, 參考Implementing Equality and Hashing