從一次測試提出的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--後");
}
複製程式碼
結果如下:
由上邊結果可以看出:(1)在為空set新增物件時,只走了hash方法。(2)在set中有元素的時候,再新增新元素的時候,走了hash方法後,還走了- (BOOL)isEqual:(id)object
方法。新增元素時,走hash方法是為了給元素在集合中算出一個合適的存放位置【NSSet底層是用雜湊表實現的】
。在集合中元素數量大於一的時候,再新增新元素的時候,會根據當前已有元素的存放位置【系統的雜湊表實現應該會和元素的個數有關,雜湊表中有個桶的概念,每個桶又是一個連結串列或陣列,深入探究可以去看雜湊表的相關知識】來決定是否走isEqual:
方法。我測試了幾次不同的情況,有的時候不呼叫,有的時候呼叫,但呼叫的次數不一定會等於已有元素的個數。
測試結果如下:
新增兩個自定義model後,又新增了一個字串。執行結果:
集合中已有10個元素的時候,新增一個字串:
集合中已有10個元素的時候,新增一個model:
集合中已有100個元素的時候,新增一個model:
除了addObject
方法(相當於存/寫)會呼叫hash
方法外,那取/讀的方法會不會呼叫?由於NSSet類沒有類似NSDictionary
的通過key或陣列的通過索引的方法去取某一個元素,我們把目標放到containsObject
方法上來,這個方法判斷集合中含有某個元素與否,按照我的想法,應該是通過傳入元素的hash值,找到元素在雜湊表中的位置,看這個位置上有沒有值【如果有碰撞衝突的話,通過其他方法(遍歷)找遍該位置上的值】看與傳入的值是否相等。
通過判斷有與沒有兩種情況檢視containsObject
的呼叫過程,結果如下:
isEqual
方法的呼叫就是不確定的了,推測應該是根據系統雜湊表的底層實現來的。
已有10個元素的時候:
已有22個元素的時候:
已有113個元素的時候:
從上圖結果來看,我們能肯定的得出在呼叫containsObject
方法的時候,肯定會呼叫傳入物件的hash
方法,但是isEqual
方法的呼叫就沒有什麼規律可循了,猜測的結果是:當集合中元素多的時候,呼叫isEqual
方法的次數少,可能是系統對雜湊表的實現,當表中裝入的元素多的時候,元素在表中的存放位置越接近正態分佈。當表中元素少的時候,系統為例節省空間,可能只會開闢一個桶或者很少的桶來存放元素。
除了NSSet集合類底層是用雜湊表實現的,iOS的NSDictionary底層也是用雜湊表實現的,那我們來看下,當model存放在字典或者當做字典的key時,是不是也會呼叫hash方法。
- model作為value存入字典結果:
- model作為字典的key的結果:
可以看到當model作為key的時候【要想作為key,需要遵循
<NSCopying>
協議,並實現- (id)copyWithZone:(nullable NSZone *)zone
方法】,會呼叫hash方法,而且最終存入字典的key是model的一份copy【避免之後的model更改,影響到存入時候的hash值,從而導致找不到對應的value】。
這次我也多新增了好多個值到字典中,從執行結果來看,model作為key的時候會呼叫hash方法,但是也看到了isEqual
的呼叫,而且呼叫的次數沒有什麼規律。這個我就真的有點迷惑了,如有哪位大佬看到,知道這個原因的話,還請不吝賜教!!?測試結果如下圖:
綜上:要想給自定義的類定義相等的話,需要重寫isEqual
和hash
方法,hash
值可以用標識該型別例項的屬性異或
來得出。在model存入集合或作為字典的key的時候,會呼叫model的hash
方法,而isEqual
的呼叫猜測應該是根據系統底層雜湊表的實現來的。
本人能力有限,如有理解不對的地方,還請指出,謝謝!!!
參考致謝: