前言
NSObject
給我們提供了-isEqual
和-hash
方法,下面我們具體介紹一下這兩個方法主要功能是什么,會在什么時候被調用,如何根據自己定制化的需求進行重寫.
isEqual
我們先查看一下方法的聲明- (BOOL)isEqual:(id)object;
,拿另一個對象與當前object
進行對比,返回一個布爾值,來確認這兩個對象是否相等
.
在進一步解釋這個方法之前,我們先看一下相等
的定義.
什么是相等
我們知道==
運算符和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
假設我們有一個Person
類
@interface Person
@property(nonatomic, copy) NSString *firstName;
@property(nonatomic, copy) NSString *secondName
@property(nonatomic, strong) NSDate *birthday;
@end
我們先自定義一下相等
的概念,這里我們舉例,如果firstName
和secondName
相等,就視為person
相等.
下面要做的就是先增加自定義的相等方法:- (BOOL)isEqualToPerson:(Person *)person;
在.m
的實現如下
@implementation Person
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL isFirstNameEqual = (!self.firstName && !person.firstName) || [self.firstName isEqualToString:person.firstName];
BOOL isSecondNameEqual = (!self.secondName && !person.secondName) || [self.secondName isEqualToString:person.secondName];
return isFirstNameEqual && isSecondNameEqual;
}
#pragma mark - NSObject
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}
- (NSUInteger)hash {
return [self.firstName hash << 8] ^ [self.secondName hash];
}
下面分步解析一下:
- 重寫父類的
isEqual
方法,首先判斷是否內存地址相等self == object
- 判斷
![object isKindOfClass:[Person class]]
如果Class
不相等則直接返回NO
- 調用我們自定義的判等方法
- (BOOL)isEqualToPerson:(Person *)person
.
isEqual
什么時候會被調用
- 我們可以直接調用
isEqual
方法來判斷兩個對象是否相等 -
NSArray
的containObject:
方法,會遍歷數組的元素,并通過isEqual
來判斷是否相等 -
NSSet
的containObject:
方法,會先調用-hash
,如果-hash
不相等,直接返回false,如果hash
相等,則會再調用isEqual
說到這里問題來了,什么是-hash
方法,它的作用是什么?
hash
方法
- (NSUInteger)hash
返回一個整數,這個數代表的就是當前對象的哈希值
有一個很重要的規范 : 如果兩個對象相等
,他們的hash
值必須相等, 如果某個類自定義了isEqual
方法,并且這個類的實例有可能會被加入到集合中,一點要確保hash
方法被重新定義
和數組把元素存儲在一系列連續的地址中不同,哈希算法使得 NSSet
和 NSDictionary
能夠非常快速地(O(1))
進行元素查找,哈希表會在內存中分配n
個位置,然后使用一個函數來計算出位置范圍之內的某個具體位置.
在數組和hash
表中要判斷一個元素是不是存在的算法和效率是不一樣的,數組需要對數組中每個元素的位置都進行檢查,hash
有一個更快速的查找方式.
一個好的 hash
函數在不需要太多計算量的情況下,可以使得生成的位置分布接近于均勻分布,當兩個不同的對象計算出相同的散列值時,我們稱其為發生了 哈希碰撞 。當出現碰撞時,哈希表會從碰撞產生的位置開始向后尋找,把新的元素放在第一個可供放置的位置,隨著哈希表變得越來越致密,發生碰撞的可能性也會隨之增加,導致查找可用位置花費的時間也會增加(這也是為什么我們希望哈希函數的結果分布更接近于均勻分布).
大家對于哈希碰撞
和哈希算法
有一個基本的概念就可以,這一塊之后會單獨拿出來進行分析,敬請期待.好了,我們我們繼續針對我們上面的isEqual
需求進行講解.
自定義-hash
方法
如果兩個對象相等
,他們的hash
值必須相等, 如果某個類自定義了isEqual
方法,并且這個類的實例有可能會被加入到集合中,一點要確保hash
方法被重新定義
自定義了兩個對象相等的規則,那么hash
要做的是保證在規則下了個對象的hash
值要相等.
我們可以通過
- (NSUInteger)hash {
//不完善的示例
return [self.firstName hash] ^ [self.secondName hash];
}
來實現,但是為什么要對firstName
進行位移呢?
上面我們說過,hash
的設計是為了快速查找,要盡可能的避免hash
沖突,也就是不滿足isEqueal
的兩個元素,盡量hash
不相等,在設計hash
的時候要考慮,是否會比較輕易的使得兩個不等的對象hash
值相等,如果是,那么hash
算法就要重新設計.
對于上面的示例來說,john smith
和smith john
就會有問題,雖然無法避免hash
沖突,但是不應該這么輕易沖突,為了解決這個易見的hash
沖突,可以使用以下
- (NSUInteger)hash {
return [self.firstName hash << 8] ^ [self.secondName hash];
}
總結
通過以上的講解和示例,我們已經可以實現自定義isEqual
和hash
方法了.
對于hash
方法,我們提到我們希望哈希函數的結果分布更接近于均勻分布,也就是在避免顯而易見的哈希沖突前提下,使得哈希算法在我們現有的范圍內有一定的沖突,目的是為了快速查找,這一塊內容對于本篇來說有一點超綱,如果你感興趣,可以繼續關注我之后的文章,我會針對哈希沖突
進行一個比較全面的分析.