isEqual & hash

閑話少說,先說本編博客的核心

iOS系統API給我們提供一個自動過濾重復元素的容器 NSMutableSet/NSSet亭病。我們可能經常用NSMutableSet/NSSet過濾相同的字符串(NSSring實例)癌别。因為NSMutableSet/NSSet內部一些實現機制要比我們自己寫的濾重方法效率高茧球。但是對于自定義一個類如Person均践,如果想利用NSMutableSet/NSSet來過濾重復元素(如多個Person實例的uid相同)椒袍,我們必須要同時實現- (BOOL)isEqual:- (NSUInteger)hash兩個方法。這里先簡單介紹他們的關系:兩個相等的實例寇损,他們的hash值一定相等。但是hash值相等的兩個實例裳食,不一定相等矛市。在重寫- (BOOL)isEqual:- (NSUInteger)hash兩個方法 的時候,切記一定要遵循上述規(guī)則诲祸。后面我們會詳細分析只實現- (BOOL)isEqual:會遇到一些什么問題浊吏。

如果我們對兩個實例相同或者- (BOOL)isEqual:概念不是很清楚【嚷龋可以看下博客iOS開發(fā) 之 不要告訴我你真的懂isEqual與hash!找田。然后再回過頭來,繼續(xù)下面的一些深入的分析着憨。

1 用NSMutableSet/NSSet 過濾相同字符串

下面就是利用NSMutableSet/NSSet 過濾相同字符串的代碼實現墩衙。對于一些系統類如NSString,NSData等已經默認支持NSMutableSet/NSSet濾重 。

self.mutSet = [NSMutableSet set];
[self.mutSet addObject:@"123"];
[self.mutSet addObject:@"1234"];
[self.mutSet addObject:@"123"];
NSArray *filterArr = self.mutSet.allObjects;
//fiterArr:只包含@"123",@"1234"兩個元素甲抖。

2 用NSMutableSet/NSSet 過濾自定義類的相同實例

更多的情況下我們是想利用NSMutableSet/NSSet來過濾自定義類(如Person)相同實例漆改。別再問我為什么不自己實現一個過濾相同值的方法,因為前面已經說過NSMutableSet/NSSet內部實現機制要比我們自己寫的效率高准谚。那么我們需要做什么呢挫剑?很簡單,上面已經說過柱衔。

必須同時實現- (BOOL)isEqual:- (NSUInteger)hash兩個方法

下面先簡單介紹下- (BOOL)isEqual:樊破。這個從字面上方法很好理解:就是比較兩個值相等不相等愉棱。具體何為相等,我們可以根據需求決定(如uid相等就認為相等或者uid和name同時相等才相等)哲戚。要想過濾相同元素羽氮,那必須提供一個比較兩個元素是否相等的函數,那就是- (BOOL)isEqual:惫恼。

有人會說“如果讓我自己實現一個過濾相同元素的功能档押,一個- (BOOL)isEqual:方法就夠我用了"。是的,如果按下面的濾重算法去實現:“弄一個數組祈纯,先不考慮性能問題令宿,每addObject之前都調用- (BOOL)isEqual:判斷是否和數組某個元素值相等,如果都不相等調用addObject腕窥,否則不做處理”粒没。一個- (BOOL)isEqual:確實能搞定。但是如果只實現- (BOOL)isEqual:簇爆,NSMutableSet/NSSet能搞定嗎癞松??入蛆?帶著這個問題响蓉,我們繼續(xù)上路

我們來看下面只實現- (BOOL)isEqual:沒有實現- (NSUInteger)hash的代碼。本博客測試代碼(同時推薦EqualAndHashDemo

2.1 只實現- (BOOL)isEqual:沒有實現- (NSUInteger)hash

如下代碼:
///聲明:這個是不完善的實現案例哨毁。用于對比用
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}
@end

上面定義了一個Person類只實現了- (BOOL)isEqual:枫甲。這里- (NSString *)description只是用于log輸出。后面我們看具體調用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    [self.mutSet addObject:person1];
    NSLog(@"add %@",person1);
    [self.mutSet addObject:person2];
    NSLog(@"add %@",person2);
    NSLog(@"count = %ld",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    [self.mutSet addObject:person3];
    NSLog(@"add %@",person3);
    NSLog(@"count = %d",self.mutSet.count);
 }

下面是一個輸出log:
運行第X遍輸出:

add 0x60800002bb40(1,nihao)
0x60800002bb40(1,nihao) compare with 0x60800002bde0(2,nihao2) result = NO Equal
add 0x60800002bde0(2,nihao2)
count = 2
add 0x60800002be00(1,nihao)
count = 3

運行第Y遍輸出:

add 0x61000003c160(1,nihao)
0x61000003c160(1,nihao) compare with 0x61000003c520(2,nihao2) result = NO Equal
add 0x61000003c520(2,nihao2)
count = 2
0x61000003c160(1,nihao) compare with 0x60000003d7c0(1,nihao) result = Equal
add 0x60000003d7c0(1,nihao)
count = 2

同樣的代碼扼褪,運行結果竟然不一致(一定要多次測試想幻,輸出的結果有時候正確有時候不正確)。根據測試案例person3 和 person1 顯然是相同的话浇,正常情況下person3應該被濾掉脏毯。為啥有時候執(zhí)行結果正確,有時候不正確呢幔崖?

其實吧NSMutableSet/NSSet食店,是一個無序集合容器,不像我們上面想的那么簡單岖瑰。僅僅實現- (BOOL)isEqual:而不實現- (NSUInteger)hash沒門叛买。 NSMutableSet/NSSet在數據存儲和比較元素相等都和- (NSUInteger)hash方法息息相關。內部高效濾重機制有- (NSUInteger)hash的很大功勞蹋订。- (NSUInteger)hash究竟有什么用率挣??露戒?帶著一些疑問椒功,我繼續(xù)上路捶箱。

2.2 只實現- (BOOL)isEqual:調用默認實現- (NSUInteger)hash

下面代碼我們雖然實現了- (NSUInteger)hash,但是我們只調用了[super hash]并輸出了一些日志动漾,其行為完全和系統默認實現一致丁屎。繼續(xù)完善一上面的案例,增加一些log旱眯。根據log輸出晨川,理清- (BOOL)isEqual:- (NSUInteger)hash何時會被觸發(fā)及調用順序。

///下面主要增加了log輸出删豺。我重寫了hash共虑,但是只調用[super hash],增加log輸出呀页。實際功能和上面代碼完全一致妈拌。
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}

- (NSUInteger)hash{
    NSUInteger hashValue = [super hash];
    NSLog(@"hash = %lu,addressValue = %lu,address = %p",(NSUInteger)hashValue,(NSUInteger)self,self);
    return hashValue;
}
@end

同時調用處我也添加了一些log。幫助分析- (BOOL)isEqual:- (NSUInteger)hash如何默契協調工作的蓬蝶。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    NSLog(@"begin add %@",person1);
    [self.mutSet addObject:person1];
    NSLog(@"after add %@",person1);
    
    NSLog(@"begin add %@",person2);
    [self.mutSet addObject:person2];
    NSLog(@"after add %@",person2);
    
    NSLog(@"count = %d",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    NSLog(@"begin add %@",person3);
    [self.mutSet addObject:person3];
    NSLog(@"after add %@",person3);
    
    NSLog(@"count = %d",self.mutSet.count);
}

運行第X遍輸出:

begin add 0x60000003efc0(1,nihao)
hash = 105553116524480,addressValue = 105553116524480,address = 0x60000003efc0
hash = 105553116524480,addressValue = 105553116524480,address = 0x60000003efc0
after add 0x60000003efc0(1,nihao)
begin add 0x60000003f1a0(2,nihao2)
hash = 105553116524960,addressValue = 105553116524960,address = 0x60000003f1a0
0x60000003efc0(1,nihao) compare with 0x60000003f1a0(2,nihao2) result = NO Equal
after add 0x60000003f1a0(2,nihao2)
count = 2
begin add 0x61800003f9c0(1,nihao)
hash = 107202383968704,addressValue = 107202383968704,address = 0x61800003f9c0
0x60000003f1a0(2,nihao2) compare with 0x61800003f9c0(1,nihao) result = NO Equal
after add 0x61800003f9c0(1,nihao)
count = 3

運行第Y遍輸出:

begin add 0x600000023520(1,nihao)
hash = 105553116411168,addressValue = 105553116411168,address = 0x600000023520
hash = 105553116411168,addressValue = 105553116411168,address = 0x600000023520
after add 0x600000023520(1,nihao)
begin add 0x600000023620(2,nihao2)
hash = 105553116411424,addressValue = 105553116411424,address = 0x600000023620
after add 0x600000023620(2,nihao2)
count = 2
begin add 0x610000023a20(1,nihao)
hash = 106652628040224,addressValue = 106652628040224,address = 0x610000023a20
0x600000023520(1,nihao) compare with 0x610000023a20(1,nihao) result = Equal
after add 0x610000023a20(1,nihao)
count = 2

Person繼承自NSObject尘分。2.1代碼中Person自然也就繼承(NSObject)- (NSUInteger)hash實現。2.2代碼雖然重寫hash但是調用的是[super hash]丸氛,其他log輸出可以忽略培愁。所以2.1代碼和2.2代碼,實現功能完全一致雪位。梳理下log我們可以得出以下結論:

  • [super hash]是系統默認實現竭钝,其返回值和實例所在內存地址值完全一致(注意十六進制和十進制轉換后相等)。
  • 當把一個實例假設為personA添加到NSMutableSet/NSSet中的時候一定會調用- (NSUInteger)hash雹洗。
  • 當把一個實例假設為personA添加到NSMutableSet/NSSet中的時候,如果mutSet中存在>=1個元素卧波,調用- (NSUInteger)hash后时肿,可能會繼續(xù)調用- (BOOL)isEqual:

了解上面的一些結論港粱,不必深入螃成,因為上面的案例,不是正確的案例查坪,輸出的結果寸宏,存在偶然性(有時候輸出一樣,有時候不一樣)偿曙。下面我們步入正軌氮凝,如果我們同時 - (BOOL)isEqual:- (NSUInteger)hash和上面2.2會有何不同?望忆?罩阵?

2.3 同時 - (BOOL)isEqual:- (NSUInteger)hash

2.1和2.2代碼都是錯誤實現竿秆,是為了對比用。下面才是正確實現8灞凇S母帧!

///正確的測試案例
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}

- (NSUInteger)hash{
    NSUInteger hashValue = self.uid; //在這里只需要比較uid就行傅是。這樣的話就滿足如果兩個實例相等匪燕,那么他們的hash一定相等,但反過來hash值相等喧笔,那么兩個實例不一定相等帽驯。但是在Person這個實例中,hash值相等那么實例一定相等溃斋。(不考慮繼承之類的)
    NSLog(@"hash = %lu,addressValue = %lu,address = %p",(NSUInteger)hashValue,(NSUInteger)self,self);
    return hashValue;
}
@end

調用代碼

//調用重寫hash后的方法
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    NSLog(@"begin add %@",person1);
    [self.mutSet addObject:person1];
    NSLog(@"after add %@",person1);
    
    NSLog(@"begin add %@",person2);
    [self.mutSet addObject:person2];
    NSLog(@"after add %@",person2);
    
    NSLog(@"count = %d",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    NSLog(@"begin add %@",person3);
    [self.mutSet addObject:person3];
    NSLog(@"after add %@",person3);
    
    NSLog(@"count = %d",self.mutSet.count);
}

在這里無論運行多少次界拦,最終結果都是一樣(不考慮內存地址及比較順序),這就是我們想要的梗劫。

begin add 0x60000003b000(1,nihao)
hash = 1,addressValue = 105553116508160,address = 0x60000003b000
hash = 1,addressValue = 105553116508160,address = 0x60000003b000
after add 0x60000003b000(1,nihao)
begin add 0x60000003b100(2,nihao2)
hash = 2,addressValue = 105553116508416,address = 0x60000003b100
after add 0x60000003b100(2,nihao2)
count = 2
begin add 0x60000003b0e0(1,nihao)
hash = 1,addressValue = 105553116508384,address = 0x60000003b0e0
0x60000003b000(1,nihao) compare with 0x60000003b0e0(1,nihao) result = Equal
after add 0x60000003b0e0(1,nihao)
count = 2

繼續(xù)梳理log我們可以得出以下結論:

  • 結論1:當把一個實例假設為personA添加到NSMutableSet/NSSet中的時候一定會調用- (NSUInteger)hash享甸。
  • 結論2:當把一個實例假設為personA添加到NSMutableSet/NSSet中的時候,如果NSMutableSet/NSSet中存在>=1個元素梳侨,那么personA調用- (NSUInteger)hash方法后蛉威,會根據其返回值,判斷是否需要繼續(xù)調用- (BOOL)isEqual:走哺。
  • 結論3:當把一個實例假設為personA添加到NSMutableSet/NSSet中的時候,如果集合中存在某個成員假設為personB的- (NSUInteger)hash返回值和personA的- (NSUInteger)hash返回值相等,則personA會繼續(xù)調用- (BOOL)isEqual:蚯嫌,以personB為參數。否則不等, 繼續(xù)下一個元素判斷丙躏。
  • 結論4:詳細判斷規(guī)則如下:
  • Step1: 集成成員的某個元素假設為personB的- (NSUInteger)hash返回值是否和personA的- (NSUInteger)hash返回值相等, 如果不相等則進入step2择示;否則進入Step3。
  • Step2: NSMutableSet/NSSet是否存在下一個沒有比較過得元素晒旅,如果有繼續(xù)Step1栅盲;否則personA會被添加到NSMutableSet/NSSet集合中,執(zhí)行結束废恋。
  • Step3: 調用personA的- (BOOL)isEqual: 以personB為參數谈秫,如果返回結果為NO則執(zhí)行Step2;如果返回結果為Yes則NSMutableSet/NSSet中存在和personA相同元素鱼鼓,personA不會被添加到集合中拟烫,執(zhí)行結束。

這里就不給大家普及 isEqual與hash的的深層理論東西迄本。具體感興趣請看下面文檔硕淑。本博客只是講解實際應用。點擊可下載測試代碼

參考文檔如下:
參考文檔1iOS開發(fā) 之 不要告訴我你真的懂isEqual與hash!
參考文檔2Equality
參考文檔3best-practices-for-overriding-isequal-and-hash

3 寫在最后

在2.2的測試中遇到一個問題無法解答,知道的請留言喜颁,不甚感激3淼!半开!

因為如果我實現hash方法只是調用系統默認實現[super hash]或者返回self地址值,如下:

- (NSUInteger)hash{
    reutrn [super hash];
    //reutrn self;
}

通過2.2的log輸出隔披,我們可以看到即使hash值不相等即(內存地址不相等),那么后面一樣會調用isEqual:方法比較寂拆。這個是為什么呢奢米??纠永?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末鬓长,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子尝江,更是在濱河造成了極大的恐慌涉波,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炭序,死亡現場離奇詭異啤覆,居然都是意外死亡,警方通過查閱死者的電腦和手機惭聂,發(fā)現死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門窗声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辜纲,你說我怎么就攤上這事笨觅。” “怎么了耕腾?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵见剩,是天一觀的道長。 經常有香客問我扫俺,道長炮温,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任牵舵,我火速辦了婚禮,結果婚禮上倦挂,老公的妹妹穿的比我還像新娘畸颅。我一直安慰自己,他們只是感情好方援,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布没炒。 她就那樣靜靜地躺著,像睡著了一般犯戏。 火紅的嫁衣襯著肌膚如雪送火。 梳的紋絲不亂的頭發(fā)上拳话,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天,我揣著相機與錄音种吸,去河邊找鬼弃衍。 笑死,一個胖子當著我的面吹牛坚俗,可吹牛的內容都是我干的镜盯。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼猖败,長吁一口氣:“原來是場噩夢啊……” “哼速缆!你這毒婦竟也來了?” 一聲冷哼從身側響起恩闻,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤艺糜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后幢尚,有當地人在樹林里發(fā)現了一具尸體破停,經...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年侠草,在試婚紗的時候發(fā)現自己被綠了辱挥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡边涕,死狀恐怖晤碘,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情功蜓,我是刑警寧澤园爷,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站式撼,受9級特大地震影響童社,放射性物質發(fā)生泄漏。R本人自食惡果不足惜著隆,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一扰楼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧美浦,春花似錦弦赖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春币厕,著一層夾襖步出監(jiān)牢的瞬間列另,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工旦装, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留页衙,地道東北人。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓同辣,卻偏偏與公主長得像拷姿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子旱函,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內容