閑話少說,先說本編博客的核心
iOS系統API給我們提供一個自動過濾重復元素的容器
NSMutableSet/NSSet
。我們可能經常用NSMutableSet/NSSet
過濾相同的字符串(NSSring實例)。因為NSMutableSet/NSSet
內部一些實現機制要比我們自己寫的濾重方法效率高。但是對于自定義一個類如Person,如果想利用NSMutableSet/NSSet
來過濾重復元素(如多個Person實例的uid相同),我們必須要同時實現- (BOOL)isEqual:
和- (NSUInteger)hash
兩個方法。這里先簡單介紹他們的關系:兩個相等的實例,他們的hash值一定相等。但是hash值相等的兩個實例,不一定相等。在重寫- (BOOL)isEqual:
和- (NSUInteger)hash
兩個方法 的時候,切記一定要遵循上述規則。后面我們會詳細分析只實現- (BOOL)isEqual:
會遇到一些什么問題。
如果我們對兩個實例相同或者- (BOOL)isEqual:
概念不是很清楚。可以看下博客iOS開發 之 不要告訴我你真的懂isEqual與hash!。然后再回過頭來,繼續下面的一些深入的分析。
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
能搞定嗎???帶著這個問題,我們繼續上路
我們來看下面只實現- (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應該被濾掉。為啥有時候執行結果正確,有時候不正確呢?
其實吧
NSMutableSet/NSSet
,是一個無序集合容器,不像我們上面想的那么簡單。僅僅實現- (BOOL)isEqual:
而不實現- (NSUInteger)hash
沒門。NSMutableSet/NSSet
在數據存儲和比較元素相等都和- (NSUInteger)hash
方法息息相關。內部高效濾重機制有- (NSUInteger)hash
的很大功勞。- (NSUInteger)hash
究竟有什么用???帶著一些疑問,我繼續上路。
2.2 只實現- (BOOL)isEqual:
調用默認實現- (NSUInteger)hash
下面代碼我們雖然實現了- (NSUInteger)hash
,但是我們只調用了[super hash]
并輸出了一些日志,其行為完全和系統默認實現一致。繼續完善一上面的案例,增加一些log。根據log輸出,理清- (BOOL)isEqual:
和- (NSUInteger)hash
何時會被觸發及調用順序。
///下面主要增加了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
后,可能會繼續調用- (BOOL)isEqual:
。
了解上面的一些結論,不必深入,因為上面的案例,不是正確的案例,輸出的結果,存在偶然性(有時候輸出一樣,有時候不一樣)。下面我們步入正軌,如果我們同時 - (BOOL)isEqual:
和 - (NSUInteger)hash
和上面2.2會有何不同???
2.3 同時 - (BOOL)isEqual:
和 - (NSUInteger)hash
2.1和2.2代碼都是錯誤實現,是為了對比用。下面才是正確實現!!!
///正確的測試案例
@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
繼續梳理log我們可以得出以下結論:
- 結論1:當把一個實例假設為personA添加到
NSMutableSet/NSSet
中的時候一定會調用- (NSUInteger)hash
。
- 結論2:當把一個實例假設為personA添加到
NSMutableSet/NSSet
中的時候,如果NSMutableSet/NSSet
中存在>=1個元素,那么personA調用- (NSUInteger)hash
方法后,會根據其返回值,判斷是否需要繼續調用- (BOOL)isEqual:
。
- 結論3:當把一個實例假設為personA添加到
NSMutableSet/NSSet
中的時候,如果集合中存在某個成員假設為personB的- (NSUInteger)hash
返回值和personA的- (NSUInteger)hash
返回值相等,則personA會繼續調用- (BOOL)isEqual:
,以personB為參數。否則不等, 繼續下一個元素判斷。
- 結論4:詳細判斷規則如下:
- Step1: 集成成員的某個元素假設為personB的
- (NSUInteger)hash
返回值是否和personA的- (NSUInteger)hash
返回值相等, 如果不相等則進入step2;否則進入Step3。 - Step2:
NSMutableSet/NSSet
是否存在下一個沒有比較過得元素,如果有繼續Step1;否則personA會被添加到NSMutableSet/NSSet
集合中,執行結束。 - Step3: 調用personA的
- (BOOL)isEqual:
以personB為參數,如果返回結果為NO則執行Step2;如果返回結果為Yes則NSMutableSet/NSSet
中存在和personA相同元素,personA不會被添加到集合中,執行結束。
這里就不給大家普及 isEqual與hash的的深層理論東西。具體感興趣請看下面文檔。本博客只是講解實際應用。點擊可下載測試代碼
參考文檔如下:
參考文檔1iOS開發 之 不要告訴我你真的懂isEqual與hash!
參考文檔2Equality
參考文檔3best-practices-for-overriding-isequal-and-hash
3 寫在最后
在2.2的測試中遇到一個問題無法解答,知道的請留言,不甚感激!!!
因為如果我實現hash方法只是調用系統默認實現[super hash]或者返回self地址值,如下:
- (NSUInteger)hash{
reutrn [super hash];
//reutrn self;
}
通過2.2的log輸出,我們可以看到即使hash值不相等即(內存地址不相等),那么后面一樣會調用isEqual:方法比較。這個是為什么呢???