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

最近在新接手的項目中進行對象比較,對同一個對象調用isEqual來比較,結果居然是NO。猜測是對象重寫了isEqual方法。查看代碼以下:算法

果真重寫了 isEqual方法,雖然方法不太嚴謹,沒有首先判斷==,代碼看起來也什麼大問題,可是同一個對象比較也不該該返回NO啊。看了下面一堆&&判斷,難道要一個個po嗎?忽然想到了 二分查找算法(算法基礎仍是有用的)的優勢,而後快速定位到放回NO的條件。

而後繼續根據二分查找,最終找到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。最後在分類中實現一個類方法判斷一下就能夠了。bash

#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中聲明的,默認實現就是簡單的比較對象地址。markdown

@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何時調用

  • 一、NSArray的containsObject:removeObject:方法都是使用了isEqual來判斷成員是否相等的
  • 二、當hash方法設計不是很完美的時候,兩個對象返回同樣的hash值,就會調用isEqual繼續判斷是否爲兩個相同對象

思考六:爲何須要hash

目的:爲了提升查找的速度函數

  • 一、在數組未排序的狀況下, 查找的時間複雜度是O(n)。
  • 二、當成員被加入到Hash Table中時, 會給它分配一個hash值, 以標識該成員在集合中的位置,經過這個位置標識能夠將查找的時間複雜度優化到O(1), 固然若是多個成員都是同一個位置標識, 那麼查找就不能達到O(1)了。
  • 三、分配的這個hash值(即用於查找集合中成員的位置標識), 就是經過hash方法計算得來的, 且hash方法返回的hash值最好惟一

和數組相比, 基於hash值索引的Hash Table查找某個成員的過程就是oop

  • Step 1: 經過hash值直接找到查找目標的位置
  • Step 2: 若是目標位置上有多個相同hash值得成員, 此時再按照數組方式進行查找

思考七:hash何時調用

HashTable是一種基本數據結構,NSSet和NSDictionary都是使用HashTable存儲數據的,所以能夠能夠確保他們查詢成員的速度爲O(1)。而NSArray使用了順序表存儲數據,查詢數據的時間複雜度爲O(n)。優化

  • 一、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);
複製代碼
  • 二、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的關係

  • 一、若是兩個對象相等,那麼他們hash值必定相等
  • 二、若是兩個對象hash值相等(hash算法不完美致使),他們不必定相等,還要繼續經過isEqual進行判斷是否真的相等

思考九:重寫hash的原則

  • hash 方法不能返回一個常量。由於使用了這個值做爲 hash 表的 key,會致使 hash 表 100%的碰撞。
  • 一個對象實例的 hash 計算結果應該是肯定的。hash方法設計的好壞直接影響查找的效率。

貼代碼

.h文件atom

#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 編程藝術

相關文章
相關標籤/搜索