最近在新接手的專案中進行物件比較,對同一個物件呼叫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。最後在分類中實現一個類方法判斷一下就可以了。
#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
複製程式碼