iOS開發(fā) 之 不要告訴我你真的懂isEqual與hash!

本文Demo的完整工程代碼, 參考這里的EqualAndHashDemo

目錄

為什么要有isEqual方法?

isEqual方法的作用大家肯定是知道的:

判斷兩個(gè)對(duì)象是否相等

但是判斷相等不是已經(jīng)有==運(yùn)算符了么, 為什么還要isEqual方法?

這是因?yàn)?

對(duì)于基本類型, ==運(yùn)算符比較的是值; 對(duì)于對(duì)象類型, ==運(yùn)算符比較的是對(duì)象的地址(即是否為同一對(duì)象)

注意: 上述==運(yùn)算符的說明適用于Objective-C和Java等不支持運(yùn)算符重載的語言, 支持運(yùn)算符重載的語言有C++

所以要理清==運(yùn)算符和isEqual方法的區(qū)別, 問題就集中在

什么叫比較對(duì)象的地址, 什么叫比較對(duì)象

我們通過下面的例子來說明這個(gè)問題

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");

打印結(jié)果如下

color1 == color2 = NO
[color1 isEqual:color2] = YES

從上面的例子可以看出, ==運(yùn)算符只是簡單地判斷是否是同一個(gè)對(duì)象, 而isEqual方法可以判斷對(duì)象是否相同, 例如UIColor對(duì)象表示的color是否相同

如何重寫自己的isEqual方法?

對(duì)于Cocoa Framework中定義的類型, 例如上面例子中的UIColor, isEqual方法已經(jīng)實(shí)現(xiàn)好了

常見類型的isEqual方法還有NSString isEqualToString / NSDate isEqualToDate / NSArray isEqualToArray / NSDictionary isEqualToDictionary / NSSet isEqualToSet, 更多參考Equality

但對(duì)于自定義類型來說, 通常需要重寫isEqual方法

通過下面的例子, 我們來看看重寫isEqual方法的正確姿勢(shì)

首先定義Person類如下

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;

@end

Person類中實(shí)現(xiàn)的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: ==運(yùn)算符判斷是否是同一對(duì)象, 因?yàn)橥粚?duì)象必然完全相同

  • Step 2: 判斷是否是同一類型, 這樣不僅可以提高判等的效率, 還可以避免隱式類型轉(zhuǎn)換帶來的潛在風(fēng)險(xiǎn)

  • Step 3: 通過封裝的isEqualToPerson方法, 提高代碼復(fù)用性

  • Step 4: 判斷person是否是nil, 做參數(shù)有效性檢查

  • Step 5: 對(duì)各個(gè)屬性分別使用默認(rèn)判等方法進(jìn)行判斷

  • Step 6: 返回所有屬性判等的與結(jié)果

isEqual的實(shí)現(xiàn)并不復(fù)雜, 但是從代碼質(zhì)量(效率, 安全, 復(fù)用)來說, 上述實(shí)現(xiàn)仍然值得仔細(xì)學(xué)習(xí)和借鑒

除了上面的最佳實(shí)踐, 還有一種最不佳實(shí)踐

@implementation NSDate (Approximate)

- (BOOL)isEqual:(id)object {
    return YES;
}

@end

這里的isEqual方法一直返回YES

NSLog(@"[self.date1 isEqual:@\"hello\"] = %@", [self.date1 isEqual:@"hello"] ? @"YES" : @"NO");

打印結(jié)果如下

[self.date1 isEqual:@"hello"] = YES

這個(gè)有趣的實(shí)驗(yàn)說明: 對(duì)象的判等可以完全由您決定, 即使兩個(gè)完全不同的對(duì)象

為什么要有hash方法?

這個(gè)問題要從Hash Table這種數(shù)據(jù)結(jié)構(gòu)說起

首先我們看下如何在數(shù)組中查找某個(gè)成員

  • Step 1: 遍歷數(shù)組中的成員

  • Step 2: 將取出的值與目標(biāo)值比較, 如果相等, 則返回該成員

在數(shù)組未排序的情況下, 查找的時(shí)間復(fù)雜度是O(array_length)

為了提高查找的速度, Hash Table出現(xiàn)了

當(dāng)成員被加入到Hash Table中時(shí), 會(huì)給它分配一個(gè)hash值, 以標(biāo)識(shí)該成員在集合中的位置

通過這個(gè)位置標(biāo)識(shí)可以將查找的時(shí)間復(fù)雜度優(yōu)化到O(1), 當(dāng)然如果多個(gè)成員都是同一個(gè)位置標(biāo)識(shí), 那么查找就不能達(dá)到O(1)了

重點(diǎn)來了:

分配的這個(gè)hash值(即用于查找集合中成員的位置標(biāo)識(shí)), 就是通過hash方法計(jì)算得來的, 且hash方法返回的hash值最好唯一

和數(shù)組相比, 基于hash值索引的Hash Table查找某個(gè)成員的過程就是

  • Step 1: 通過hash值直接找到查找目標(biāo)的位置

  • Step 2: 如果目標(biāo)位置上有多個(gè)相同hash值得成員, 此時(shí)再按照數(shù)組方式進(jìn)行查找

hash方法什么時(shí)候被調(diào)用?

帶著這個(gè)問題, 我們來看下面的例子

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方法是否被調(diào)用, 我們重寫hash方法如下

- (NSUInteger)hash {
    NSUInteger hash = [super hash];
    NSLog(@"hash = %ld", hash);
    return hash;
}

打印結(jié)果如下

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 -------------------------------

從打印結(jié)果可以看到:

hash方法只在對(duì)象被添加至NSSet和設(shè)置為NSDictionary的key時(shí)會(huì)調(diào)用

NSSet添加新成員時(shí), 需要根據(jù)hash值來快速查找成員, 以保證集合中是否已經(jīng)存在該成員

NSDictionary在查找key時(shí), 也利用了key的hash值來提高查找的效率

hash方法與判等的關(guān)系?

hash方法主要是用于在Hash Table查詢成員用的, 那么和我們要討論的isEqual()有什么關(guān)系呢?

為了優(yōu)化判等的效率, 基于hash的NSSet和NSDictionary在判斷成員是否相等時(shí), 會(huì)這樣做

  • Step 1: 集成成員的hash值是否和目標(biāo)hash值相等, 如果相同進(jìn)入Step 2, 如果不等, 直接判斷不相等

  • Step 2: hash值相同(即Step 1)的情況下, 再進(jìn)行對(duì)象判等, 作為判等的結(jié)果

簡單地說就是

hash值是對(duì)象判等的必要非充分條件

如何重寫自己的hash方法?

很多人在iOS開發(fā)中, 都是這么重寫hash方法的

- (NSUInteger)hash {
    return [super hash];
}

這樣寫有問題么? 帶著這個(gè)問題, 我們先來看下[super hash]的值到底是什么

Person *person = [[Person alloc] init];
NSLog(@"person = %ld", (NSUInteger)person);
NSLog(@"[person1 getSuperHash] = %ld", [person getSuperHash]);

打印結(jié)果如下

person = 140643147498880
[person1 getSuperHash] = 140643147498880

由此可以看出, [super hash]返回的就是該對(duì)象的內(nèi)存地址

聯(lián)想到前面對(duì)hash值唯一性的要求, 使用對(duì)象的內(nèi)存地址作為hash值不是很好么?

別急, 我們添加如下兩個(gè)對(duì)象到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);

此時(shí)打印結(jié)果如下

[person1 isEqual:person2] = YES
set count = 2

isEqual相等的兩個(gè)對(duì)象都加入到了NSSet中(set count = 2), 所以直接返回[super hash]是不正確的

那么hash方法的最佳實(shí)踐到底是什么呢?

大神Mattt ThompsonEquality中給出的結(jié)論就是

In reality, a simple XOR over the hash values of critical properties is sufficient 99% of the time(對(duì)關(guān)鍵屬性的hash值進(jìn)行位或運(yùn)算作為hash值)

對(duì)于上面Person類的hash方法實(shí)現(xiàn)如下

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}

更多關(guān)于位運(yùn)算的討論, 參考Implementing Equality and Hashing

參考

更多文章, 請(qǐng)支持我的個(gè)人博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市点弯,隨后出現(xiàn)的幾起案子扣蜻,更是在濱河造成了極大的恐慌互广,老刑警劉巖科侈,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冗栗,死亡現(xiàn)場離奇詭異泄朴,居然都是意外死亡罢吃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門宴偿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來湘捎,“玉大人,你說我怎么就攤上這事窄刘】荆” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵娩践,是天一觀的道長活翩。 經(jīng)常有香客問我,道長翻伺,這世上最難降的妖魔是什么材泄? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮吨岭,結(jié)果婚禮上拉宗,老公的妹妹穿的比我還像新娘。我一直安慰自己辣辫,他們只是感情好旦事,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著急灭,像睡著了一般姐浮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上葬馋,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天卖鲤,我揣著相機(jī)與錄音,去河邊找鬼点楼。 笑死扫尖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掠廓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼甩恼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蟀瞧!你這毒婦竟也來了沉颂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤悦污,失蹤者是張志新(化名)和其女友劉穎铸屉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體切端,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡彻坛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了踏枣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昌屉。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖茵瀑,靈堂內(nèi)的尸體忽然破棺而出间驮,到底是詐尸還是另有隱情,我是刑警寧澤马昨,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布竞帽,位于F島的核電站,受9級(jí)特大地震影響鸿捧,放射性物質(zhì)發(fā)生泄漏屹篓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一匙奴、第九天 我趴在偏房一處隱蔽的房頂上張望抱虐。 院中可真熱鬧,春花似錦饥脑、人聲如沸恳邀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谣沸。三九已至,卻和暖如春笋颤,著一層夾襖步出監(jiān)牢的瞬間乳附,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工伴澄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赋除,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓非凌,卻偏偏與公主長得像举农,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子敞嗡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容