iOS中的isEqual和hash

一修Grace發表於2019-03-01

從一次測試提出的bug說起:有次測試給我提了個bug,說訂單列表的新載入出來的第一條和上一頁的最後一條的資料一樣,是同一個訂單。而且還能經常重現,我就挺疑惑,如果要是重複,應該整個一頁都是重複的啊,為什麼只有第一條重複,還是偶爾重現。我直接檢視後端返回資料,發現是後端偶爾會在下一頁返回上一頁的最後一條資料。後端找了一會兒原因後,說需要我這邊來去重,後端具體是什麼原因我就不是那麼清楚了,反正最後權衡說前端去重是效率最高,也是最有效的辦法。(我也很無奈?啊)。


說到去重,我腦子裡首先出現的是NSSet型別,因為這個集合型別是不能新增相同的資料的。這就引出了這篇文章的主題:怎麼去定義相同,對於值型別(int、double等資料型別)的資料可以直接通過==來判斷是否相等,而物件型別的資料你要通過==來判斷的話,就是直接比較兩個物件的地址是否相等,也就是判斷兩個指標是否是指向的同一個物件【NSString型別比較特殊,暫不做討論】

系統提供的Foundation中,有些類有自己的判斷相等的方法。

NSAttributedString -isEqualToAttributedString:
NSData -isEqualToData:
NSDate -isEqualToDate:
NSDictionary -isEqualToDictionary:
NSHashTable -isEqualToHashTable:
NSIndexSet -isEqualToIndexSet:
NSNumber -isEqualToNumber:
NSOrderedSet -isEqualToOrderedSet:
NSSet -isEqualToSet:
NSString -isEqualToString://這個應該是最常用的了吧。
NSTimeZone -isEqualToTimeZone:
NSValue -isEqualToValue:

複製程式碼

但是,對於自己定義的模型類,怎樣去定義兩個物件相等。 先上答案:

@interface EqualModel : NSObject
@property (nonatomic,copy)NSString *name;
@property (nonatomic,assign)NSInteger identifier;
- (BOOL)isEqualToModel:(EqualModel *)model;
@end

@implementation EqualModel
- (BOOL)isEqualToModel:(EqualModel *)model {
    if (!model) {
        return NO;
    }
    BOOL haveEqualNames = (!self.name && !model.name) || [self.name isEqualToString:model.name];
    BOOL haveEqualIdentifers = self.identifier == model.identifier;
    return haveEqualNames && haveEqualIdentifers;
}
- (BOOL)isEqual:(id)object {
    //NSLog(@"走了isEqual");
    if (self == object) {
        return YES;
    }
    
    if (![object isKindOfClass:[EqualModel class]]) {
        return NO;
    }
    
    return [self isEqualToModel:(EqualModel *)object];
}
- (NSUInteger)hash
{
    //NSLog(@"走了hash");
    return self.name.hash ^ self.identifier;
}
複製程式碼

以上是引用Mattt大神的文章Equality的實現。

看到實現你可能會有疑問:不是隻要實現- (BOOL)isEqual:(id)object方法就行了,為什麼要還要實現- (NSUInteger)hash?提到hash值,就得知道雜湊表/雜湊表了。

繼承於NSObject的物件都有hash方法,因為該方法是<NSObject>協議裡宣告的一個方法,hash方法的預設實現是返回該物件的地址。

既然不知道為什麼要實現hash方法,就用先看下hash方法何時呼叫。(我們知道NSSet類裡不能新增相同的物件,那我們就先從這個類入手)

EqualModel的定義見上邊。

- (void)testSet
{
    NSMutableSet *set = [NSMutableSet set];
    
    EqualModel *model0 = [[EqualModel alloc]init];
    model0.name = @"model0";
    model0.identifier = 0;
    NSLog(@"-------0--前");
    [set addObject:model0];
    NSLog(@"-------0--後");
    
    EqualModel *model1 = [[EqualModel alloc]init];
    model1.name = @"model1";
    model1.identifier = 1;
    NSLog(@"-------1--前");
    [set addObject:model1];
    NSLog(@"-------1--後");
    
}
複製程式碼

結果如下:

iOS中的isEqual和hash
由上邊結果可以看出:(1)在為空set新增物件時,只走了hash方法。(2)在set中有元素的時候,再新增新元素的時候,走了hash方法後,還走了- (BOOL)isEqual:(id)object方法。新增元素時,走hash方法是為了給元素在集合中算出一個合適的存放位置【NSSet底層是用雜湊表實現的】。在集合中元素數量大於一的時候,再新增新元素的時候,會根據當前已有元素的存放位置【系統的雜湊表實現應該會和元素的個數有關,雜湊表中有個桶的概念,每個桶又是一個連結串列或陣列,深入探究可以去看雜湊表的相關知識】來決定是否走isEqual:方法。我測試了幾次不同的情況,有的時候不呼叫,有的時候呼叫,但呼叫的次數不一定會等於已有元素的個數。 測試結果如下: 新增兩個自定義model後,又新增了一個字串。執行結果:
iOS中的isEqual和hash
集合中已有10個元素的時候,新增一個字串:
iOS中的isEqual和hash
集合中已有10個元素的時候,新增一個model:
iOS中的isEqual和hash
集合中已有100個元素的時候,新增一個model:
iOS中的isEqual和hash

除了addObject方法(相當於存/寫)會呼叫hash方法外,那取/讀的方法會不會呼叫?由於NSSet類沒有類似NSDictionary的通過key或陣列的通過索引的方法去取某一個元素,我們把目標放到containsObject方法上來,這個方法判斷集合中含有某個元素與否,按照我的想法,應該是通過傳入元素的hash值,找到元素在雜湊表中的位置,看這個位置上有沒有值【如果有碰撞衝突的話,通過其他方法(遍歷)找遍該位置上的值】看與傳入的值是否相等。

通過判斷有與沒有兩種情況檢視containsObject的呼叫過程,結果如下:

iOS中的isEqual和hash
從結果中來看: 不管是包含不包含傳入的元素,都會呼叫hash方法,但是isEqual方法的呼叫就是不確定的了,推測應該是根據系統雜湊表的底層實現來的。 已有10個元素的時候:
iOS中的isEqual和hash
已有22個元素的時候:
iOS中的isEqual和hash
已有113個元素的時候:
iOS中的isEqual和hash
從上圖結果來看,我們能肯定的得出在呼叫containsObject方法的時候,肯定會呼叫傳入物件的hash方法,但是isEqual方法的呼叫就沒有什麼規律可循了,猜測的結果是:當集合中元素多的時候,呼叫isEqual方法的次數少,可能是系統對雜湊表的實現,當表中裝入的元素多的時候,元素在表中的存放位置越接近正態分佈。當表中元素少的時候,系統為例節省空間,可能只會開闢一個桶或者很少的桶來存放元素。


除了NSSet集合類底層是用雜湊表實現的,iOS的NSDictionary底層也是用雜湊表實現的,那我們來看下,當model存放在字典或者當做字典的key時,是不是也會呼叫hash方法。

  • model作為value存入字典結果:
    iOS中的isEqual和hash
  • model作為字典的key的結果:
    iOS中的isEqual和hash
    可以看到當model作為key的時候【要想作為key,需要遵循<NSCopying>協議,並實現- (id)copyWithZone:(nullable NSZone *)zone方法】,會呼叫hash方法,而且最終存入字典的key是model的一份copy【避免之後的model更改,影響到存入時候的hash值,從而導致找不到對應的value】。

這次我也多新增了好多個值到字典中,從執行結果來看,model作為key的時候會呼叫hash方法,但是也看到了isEqual的呼叫,而且呼叫的次數沒有什麼規律。這個我就真的有點迷惑了,如有哪位大佬看到,知道這個原因的話,還請不吝賜教!!?測試結果如下圖:

iOS中的isEqual和hash

綜上:要想給自定義的類定義相等的話,需要重寫isEqualhash方法,hash值可以用標識該型別例項的屬性異或來得出。在model存入集合或作為字典的key的時候,會呼叫model的hash方法,而isEqual的呼叫猜測應該是根據系統底層雜湊表的實現來的。

本人能力有限,如有理解不對的地方,還請指出,謝謝!!!


參考致謝:

Equality中文

Equality英文

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

相關文章