本體性 和 相等性:(摘自Equality)
相等性:當(dāng)兩個物體有一系列相同的可觀測的屬性時,兩個物體可能是互相相等或者等價的。但這兩個物體仍然是不同的,他們各自有自己的本體。
本體性:在編程中,一個對象的本體和它的內(nèi)存地址是相互關(guān)聯(lián)的。關(guān)聯(lián)的內(nèi)存地址相同則具有本體性。
對象比較:
比較方式:
1、==:對于基本數(shù)據(jù)類型比較的是值,對于對象則是本體比較,也就是直接比較對象的指針地址
2、isEqual:
有了==后,為什么還要有 isEqual:,這里要搞清楚兩個概念:
對象比較和對象地址比較:
這里也就回到了文章開始提到的相等性,有時我們比較對象,并不是為了比較對象的地址是否相同,而是只要是對象的屬性,內(nèi)容等相同我們就會認為對象相同。(這也是為什么我們會自定義isEqual:函數(shù))
OC對象比較一般來說是比較“本體”,而本體的比較比的是對象的內(nèi)存地址,(即:只要是地址相同,則被認為是相同的對象)但是當(dāng)我們重寫了isEqual:后,動機就是為了做相等性比較。
重寫hash函數(shù)
哈希表的查找原理:
說到hash我們先來簡單了解下Hash Table這種數(shù)據(jù)結(jié)構(gòu):
1、數(shù)組中查找一個元素的過程:
1)遍歷整個數(shù)組、
2)取出數(shù)組中每一個值,并將取出的值同目標值進行比較。若一致則返回該成員。
如果數(shù)組未經(jīng)過排序,查找的時間復(fù)雜度是O(length).
2、而當(dāng)將一個元素加入到Hash Table中時,會給這個元素分配一個hash值,用來表示這個元素在hash表中的位置。(hash值的生成就是通過hash函數(shù))。
通過位置標識,hash表的查找時間復(fù)雜度為O(1)。但是**多個成員的hash值相同時即:出現(xiàn)hash沖突。這是時間復(fù)雜度就會降低。過程總結(jié)如下:
1)通過hash值定位到元素所在的位置
2)如果該位置有多個hash值相同的成員,則對該位置上的hash值相同的元素以數(shù)組方式進行查找。
通常為了避免情況2的出現(xiàn),有一個規(guī)范:加入到hash表中的元素應(yīng)盡量保證其hash值唯一。
iOS中關(guān)于hash方法的重寫:
3、iOS中NSSet、NSDictionary都是基于hash table實現(xiàn)的。所以當(dāng)我們自定義的類重寫了isEqual方法,且該對象有可能被加入到集合中時,要保證重寫hash方法。
原因如下:
1、為了保證效率,基于散列表實現(xiàn)的NSSet、NSDictionary在對成員判斷是否相等時,會:
1)想判斷連個對象的hash值是否相同,如果相同則進行第二步處理,反之,判定為不相等。
2)在基于第一步的條件下,再調(diào)用isEqual:(isEqualXXX:)來進行判斷。
也就是說:hash值相同,對象也有可能不相同。但是我們一般約定:如果對象相等,hash值一定要保證相等。
2、既然重寫了isEqual:函數(shù),說明我們想要做的是“相等性”比較,而不是“本體性”比較,而默認的hash函數(shù)返回值則是對象的內(nèi)存地址。既然是做“相等性”比較,那就應(yīng)該讓hash返回值也符合“相等性”比較行為,而不是返回對象內(nèi)存地址。
來看一段代碼:
person.m文件
@implementation person
- (instancetype)initWithUserName:(NSString *)userName {
self = [super init];
if(self) {
self.userName = userName;
}
return self;
}
- (BOOL)isEqual:(id)object {
NSLog(@"===isEqual:self:%@,object:%@",self,object);
// return [super isEqual:object];
if(self == object) {
return YES;
}
else {
if([self.userName isEqualToString:((person *)object).userName]) {
return YES;
}
return NO;
}
}
- (NSUInteger)hash {
NSLog(@"=====hash");
return [super hash];
}
//重寫后直接調(diào)super和不重寫hash方法作用是一致的。
otherClass.m
person *pp = [[person alloc] initWithUserName:@"1111"];
person *pp11 = [[person alloc] initWithUserName:@"1111"];
NSMutableSet *set = [NSMutableSet set];
[set addObject:pp];
[set addObject:pp11];
NSLog(@"=====%@",set);
期望輸出:set中置于一個元素,因為我們重定義了isEqual,只要是userName相同,我們就認為對象是相同的。所以pp和pp11在這里是相等的,不應(yīng)該被他添加到集合中。
實際輸出:2個元素都被加入了集合中。
原因分析:在添加第二個元素時,因為hash值返回的是每個對象的內(nèi)存地址,所以被判斷為不相等,沒有執(zhí)行isEqual:函數(shù)。幸而直接被添加進set中。
疑問:如果把上面的代碼作如下改動:
//添加代碼
person *pp22 = [[person alloc] initWithUserName:@"1111"];
[set addObject:pp22];
NSLog(@"=====%@",set);
會發(fā)現(xiàn)pp22沒有被添加到集合中,打印pp22 的hash值發(fā)現(xiàn)同pp、pp11不相同,而且這里卻執(zhí)行了isEqual:函數(shù)。所以不明白為什么沒有添加進去? 如果有同學(xué)有好的理解,請在評論區(qū)跟我分享。
如何重寫hash函數(shù):
直接說結(jié)論:
將對象關(guān)鍵屬性的hash值進行位或運算,將運算結(jié)果作為對象的hash值。
這里只是提供了一種還算不錯的實現(xiàn)方式,諸多開源庫其實都有很好的實踐。大家可自行參閱。
代碼示例:
- (NSUInteger)hash {
return [self.userName hash] ^ [self.lastName hash];
}
hash的設(shè)計是為了快速查找,要盡可能的避免hash沖突,也就是不滿足isEqueal的兩個元素,盡量hash不相等,在設(shè)計hash的時候要考慮,是否會比較輕易的出現(xiàn)兩個不等的對象hash值相等的情況。如果是,那就需要重新設(shè)計hash函數(shù)的實現(xiàn)。
以上面的實現(xiàn)為例。(例如有人曾給出這個例子)john smith 和 smith john結(jié)果是一樣的。
所以這里比較好的實踐為:
- (NSUInteger)hash {
return [self.firstName hash << 8] ^ [self.secondName hash];
}
添加進集合后,保證對象的hash值不可變:
如果重寫了對象的hash函數(shù),而且把對象作為 基于“哈希表”實現(xiàn)的集合(NSSet、NSDictionary、NSMapTable、NSHashTable)中的key時,需要保證在集合內(nèi)的期間,對象的hash不變。
看下面代碼具體解釋下:
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setObject:@"hhhhh" forKey:pp];
NSLog(@"====%@",[dic objectForKey:pp]);
結(jié)果:輸出hhh
但是我們稍加改造,如下:
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setObject:@"hhhhh" forKey:pp];
NSLog(@"====%@",[dic objectForKey:pp]);
pp.userName = @"test";
NSLog(@"====%@",[dic objectForKey:pp]);
結(jié)果:
TestIsE&Hash[7399:1002659] ====hhhhh
TestIsE&Hash[7399:1002659] ====(null)
當(dāng)對象在集合內(nèi)期間,如果改變了對象的hash值,會導(dǎo)致hash表結(jié)構(gòu)的結(jié)合無法正確查找的問題。
添加isEqualXXX:函數(shù):
NSObject子類重寫了isEqual:后,需要做一下三方面的工作:
1、實現(xiàn)一個新的 isEqualTo__ClassName__ 方法,進行實際意義上的值的比較。
2、重載 isEqual: 方法進行類和對象的本體性檢查,如果失敗則回退到上面提到的值(相等性)比較方法。
3、重載 hash 方法。
參考:
isEqual & hash
不懂isEqual
解析和重寫NSObjetc的isEqual和Hash
Equality