iOS筆記:進一步認識 ==、isEqual、hash

在路上重名了啊發表於2018-11-20

最近在新接手的專案中進行物件比較,對同一個物件呼叫isEqual來比較,結果竟然是NO。猜想是物件重寫了isEqual方法。檢視程式碼如下:

iOS筆記:進一步認識 ==、isEqual、hash
果然重寫了isEqual方法,雖然方法不太嚴謹,沒有首先判斷==,程式碼看起來也什麼大問題,但是同一個物件比較也不應該返回NO啊。看了下面一堆&&判斷,難道要一個個po嗎?突然想到了二分查詢演算法(演算法基礎還是有用的)的優點,然後快速定位到放回NO的條件。

iOS筆記:進一步認識 ==、isEqual、hash
然後繼續根據二分查詢,最終找到self.releasetime比較的時候返回NO。原來releasetime欄位為nil,使用isEqualToString比較兩個為nil的物件的時候返回NO。

(lldb) po (BOOL)([self.releasetime isEqualToString:info.releasetime])
NO
(lldb) 
複製程式碼

解決辦法:

場景一:針對當前的場景,只需要在的最頂部新增下面判斷就可以了if (self == object) return YES;

場景二:但是一些其他場景,我們確實會遇到比較兩個地址不一樣,但是資料一樣(並且物件有的屬性確實為nil)的場景。這個時候我們可以為NSString等基本資料型別新增category,然後重寫isEqualToString:方法,如果要比較的兩個物件都是nil則返回YES。但是如果要比較的兩個物件都不是nil而且length也相同,難道我還要重寫一個方法進行遍歷比較嗎?能不能直接用原來的isEqualToString:方法,但是category中不能呼叫super方法,再說分類也不是子類。此時我們就需要利用runtime遍歷方法列表找到原來的方法,然後呼叫一下返回結果就可以了。後來發現,想多了,給nil傳送訊息返回的始終是NO。最後在分類中實現一個類方法判斷一下就可以了。

#import "NSString+isEqual.h"

@implementation NSString (isEqual)

+(void)load {
    NSLog(@"結果1 = %@", [nil isEqualToString:nil] ? @"YES" : @"NO");
    NSLog(@"結果2 = %@", [NSString isString:nil EqualToString:nil] ? @"YES" : @"NO");
}

+(BOOL)isString:(NSString *)aString EqualToString:(NSString *)bString {
    if (aString == nil && bString == nil ) {
        return YES;
    }
    return [aString isEqualToString:bString];
}

@end
複製程式碼

思考一:==與isEqual的區別?

  • 對於基本型別, ==運算子比較的是值;
  • 對於物件型別, ==運算子比較的是物件的地址
  • isEqual方法就是用來判斷兩個物件是否相等(自定義物件需要重寫isEqual)

思考二:isEqual的預設實現

isEqual方法是NSObject中宣告的,預設實現就是簡單的比較物件地址。

@implementation NSObject (Approximate)
- (BOOL)isEqual:(id)object {
  return self == object;
}
@end
複製程式碼

思考三:自定義繼承NSObject的子類需要重寫的幾個方法

  • 重寫-(BOOL)isEqual:(id)object
  • 重寫-(NSUInteger)hash
  • (非必須)實現NSCoping協議-(id)copyWithZone:(NSZone *)zone

注意: 使用OC自定義的類例項作為NSDictionary的key的話,需要實現NSCoping協議,如果不實現的話,向dictionary中新增資料時會報警告:Sending 'Coder *__strong to parameter of incompatible type 'id _Nonnull',在執行的時候,在setObject forKey函式這裡直接崩潰

思考四:Foundation中NSObject的子類已經定義的自己的isEqual

  • NSAttributedString -isEqualToAttributedString:
  • NSData -isEqualToData:
  • NSDate -isEqualToDate:
  • NSDictionary -isEqualToDictionary:
  • NSHashTable -isEqualToHashTable:
  • NSIndexSet -isEqualToIndexSet:
  • NSNumber -isEqualToNumber:
  • NSOrderedSet -isEqualToOrderedSet:
  • NSSet -isEqualToSet:
  • NSString -isEqualToString:
  • NSTimeZone -isEqualToTimeZone:
  • NSValue -isEqualToValue:

思考五:重寫的isEqual什麼時候呼叫

  • 1、NSArray的containsObject:removeObject:方法都是使用了isEqual來判斷成員是否相等的
  • 2、當hash方法設計不是很完美的時候,兩個物件返回一樣的hash值,就會呼叫isEqual繼續判斷是否為兩個相同物件

思考六:為什麼需要hash

目的:為了提高查詢的速度

  • 1、在陣列未排序的情況下, 查詢的時間複雜度是O(n)。
  • 2、當成員被加入到Hash Table中時, 會給它分配一個hash值, 以標識該成員在集合中的位置,通過這個位置標識可以將查詢的時間複雜度優化到O(1), 當然如果多個成員都是同一個位置標識, 那麼查詢就不能達到O(1)了。
  • 3、分配的這個hash值(即用於查詢集合中成員的位置標識), 就是通過hash方法計算得來的, 且hash方法返回的hash值最好唯一

和陣列相比, 基於hash值索引的Hash Table查詢某個成員的過程就是

  • Step 1: 通過hash值直接找到查詢目標的位置
  • Step 2: 如果目標位置上有多個相同hash值得成員, 此時再按照陣列方式進行查詢

思考七:hash什麼時候呼叫

HashTable是一種基本資料結構,NSSet和NSDictionary都是使用HashTable儲存資料的,因此可以可以確保他們查詢成員的速度為O(1)。而NSArray使用了順序表儲存資料,查詢資料的時間複雜度為O(n)。

  • 1、hash方法會在物件被新增到NSSet中的時候呼叫
Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
Coder* coder3 = [Coder initWith:@"C++" level:@"17"];
NSSet* coderSet = [NSSet setWithObjects:coder1, coder2, coder3, nil];
NSLog(@"coderSet.count = %ld", coderSet.count);
複製程式碼
  • 2、hash方法會在物件被用作NSDictionary的key的時候呼叫
Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
Coder* coder3 = [Coder initWith:@"C++" level:@"17"];
NSMutableDictionary* coderDic2 = [NSMutableDictionary dictionary];
[coderDic2 setObject:@"1" forKey:coder1];
[coderDic2 setObject:@"2" forKey:coder2];
[coderDic2 setObject:@"3" forKey:coder3];
NSLog(@"coderDic2.count = %ld", coderDic2.count);
複製程式碼

思考八:hash和isEqual的關係

  • 1、如果兩個物件相等,那麼他們hash值一定相等
  • 2、如果兩個物件hash值相等(hash演算法不完美導致),他們不一定相等,還要繼續通過isEqual進行判斷是否真的相等

思考九:重寫hash的原則

  • hash 方法不能返回一個常量。因為使用了這個值作為 hash 表的 key,會導致 hash 表 100%的碰撞。
  • 一個物件例項的 hash 計算結果應該是確定的。hash方法設計的好壞直接影響查詢的效率。

貼程式碼

.h檔案

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Coder : NSObject<NSCopying>
@property (nonatomic, strong) NSString *language;
@property (nonatomic, strong) NSString *level;
+(instancetype)initWith:(NSString*)language level:(NSString*)level;
@end

NS_ASSUME_NONNULL_END
複製程式碼

.m檔案

#import "Coder.h"

@implementation Coder
+(instancetype)initWith:(NSString*)language level:(NSString*)level {
    Coder* coder = [[Coder alloc] initWith:language level:level];
    return coder;
}
-(instancetype)initWith:(NSString*)language level:(NSString*)level {
    self.language = language;
    self.level = level;
    return self;
}
// 物件用作NSSDictionary的key必須實現
-(id)copyWithZone:(NSZone *)zone {
    Coder* coder = [[Coder allocWithZone:zone] initWith:self.language level:self.level];
    return coder;
}
-(BOOL)isEqual:(id)object {
    NSLog(@"func = %s", __func__);
    if (self == object) {
        return YES;
    }
    if (![object isKindOfClass:[self class]]) {
        // object == nil 在此返回NO
        return NO;
    }
    return [self isEqualToCoder:object];
}
-(BOOL)isEqualToCoder:(Coder*)object {
    // isEqualToString 需要使用分類重寫一下,否則 [nil isEqualToString:nil]會返回NO
    if (![self.language isEqualToString:object.language]) {
        return NO;
    }
    if (![self.level isEqualToString:object.level]) {
        return NO;
    }
    return YES;
}
-(NSUInteger)hash {
    BOOL isCareAddress = YES;
    NSUInteger hashValue = 0;
    if (isCareAddress) {
        // 如果期望對地址不同、但是內容相同的物件做區分
        hashValue = [super hash];
        // 結果:兩個地址不同,但是內容相同的物件新增到NSMutableSet中,NSMutableSet的個數返回的是2
    }
    else {
        // 不關心地址是否相同,只對內容進行區分(對關鍵屬性的hash值進行按位異或運算作為hash值)
        hashValue = [self.language hash] ^ [self.level hash];
        // 結果:兩個地址不同,但是內容相同的物件新增到NSMutableSet中,NSMutableSet的個數返回的是1
    }
    NSLog(@"func = %s, hashValue = %ld", __func__, hashValue);
    return hashValue;
}
@end
複製程式碼

呼叫

#import "HashViewController.h"
#import "Coder.h"

@interface HashViewController ()
@end

@implementation HashViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
    Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
    Coder* coder3 = [Coder initWith:@"C++" level:@"17"];

    NSArray* coderList = @[coder1, coder2, coder3];
    NSLog(@"array-containsObject-coder1-start");
    [coderList containsObject:coder1];
    NSLog(@"array-containsObject-coder1-end");
    NSLog(@"array-containsObject-coder2-start");
    [coderList containsObject:coder2];
    NSLog(@"array-containsObject-coder2-end");
    NSLog(@"array-containsObject-coder3-start");
    [coderList containsObject:coder3];
    NSLog(@"array-containsObject-coder3-end");
    NSLog(@"coderList.count = %ld", coderList.count);

    NSSet* coderSet = [NSSet setWithObjects:coder1, coder2, coder3, nil];
    NSLog(@"coderSet.count = %ld", coderSet.count);

    NSDictionary* coderDic = @{@"1":coder1, @"2":coder2, @"3":coder3};
    NSLog(@"coderDic.count = %ld", coderDic.count);
    
    NSMutableDictionary* coderDic2 = [NSMutableDictionary dictionary];
    [coderDic2 setObject:@"1" forKey:coder1];
    [coderDic2 setObject:@"2" forKey:coder2];
    [coderDic2 setObject:@"3" forKey:coder3];
    NSLog(@"coderDic2.count = %ld", coderDic2.count);
}

@end
複製程式碼

參考部落格:

Objective-C -- isEqual與hash

禪與 Objective-C 程式設計藝術

相關文章