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