NSObject子類重寫isEqual:函數和hash函數實踐

本體性 和 相等性:(摘自Equality)

相等性:當兩個物體有一系列相同的可觀測的屬性時,兩個物體可能是互相相等或者等價的。但這兩個物體仍然是不同的,他們各自有自己的本體。
本體性:在編程中,一個對象的本體和它的內存地址是相互關聯的。關聯的內存地址相同則具有本體性。

對象比較:

比較方式:
1、==:對于基本數據類型比較的是值,對于對象則是本體比較,也就是直接比較對象的指針地址
2、isEqual:
有了==后,為什么還要有 isEqual:,這里要搞清楚兩個概念:
對象比較和對象地址比較:
這里也就回到了文章開始提到的相等性,有時我們比較對象,并不是為了比較對象的地址是否相同,而是只要是對象的屬性,內容等相同我們就會認為對象相同。(這也是為什么我們會自定義isEqual:函數
OC對象比較一般來說是比較“本體”,而本體的比較比的是對象的內存地址,(即:只要是地址相同,則被認為是相同的對象)但是當我們重寫了isEqual:后,動機就是為了做相等性比較。

重寫hash函數

哈希表的查找原理:

說到hash我們先來簡單了解下Hash Table這種數據結構:
1、數組中查找一個元素的過程:
1)遍歷整個數組、
2)取出數組中每一個值,并將取出的值同目標值進行比較。若一致則返回該成員。
如果數組未經過排序,查找的時間復雜度是O(length).
2、而當將一個元素加入到Hash Table中時,會給這個元素分配一個hash值,用來表示這個元素在hash表中的位置。(hash值的生成就是通過hash函數)。
通過位置標識,hash表的查找時間復雜度為O(1)。但是**多個成員的hash值相同時即:出現hash沖突。這是時間復雜度就會降低。過程總結如下:
1)通過hash值定位到元素所在的位置
2)如果該位置有多個hash值相同的成員,則對該位置上的hash值相同的元素以數組方式進行查找。
通常為了避免情況2的出現,有一個規范:加入到hash表中的元素應盡量保證其hash值唯一。

iOS中關于hash方法的重寫:

3、iOS中NSSet、NSDictionary都是基于hash table實現的。所以當我們自定義的類重寫了isEqual方法,且該對象有可能被加入到集合中時,要保證重寫hash方法。
原因如下:
1、為了保證效率,基于散列表實現的NSSet、NSDictionary在對成員判斷是否相等時,會:
1)想判斷連個對象的hash值是否相同,如果相同則進行第二步處理,反之,判定為不相等。
2)在基于第一步的條件下,再調用isEqual:(isEqualXXX:)來進行判斷。
也就是說:hash值相同,對象也有可能不相同。但是我們一般約定:如果對象相等,hash值一定要保證相等。

2、既然重寫了isEqual:函數,說明我們想要做的是“相等性”比較,而不是“本體性”比較,而默認的hash函數返回值則是對象的內存地址。既然是做“相等性”比較,那就應該讓hash返回值也符合“相等性”比較行為,而不是返回對象內存地址。

來看一段代碼:

person.m文件

@implementation person

- (instancetype)initWithUserName:(NSString *)userName {
    self = [super init];
    if(self) {
        self.userName = userName;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    NSLog(@"===isEqual:self:%@,object:%@",self,object);
//    return [super isEqual:object];
    if(self == object) {
        return YES;
    }
    else {
        if([self.userName isEqualToString:((person *)object).userName]) {
            return YES;
        }
        return NO;
    }
}

- (NSUInteger)hash {
    NSLog(@"=====hash");
    return [super hash];
}

//重寫后直接調super和不重寫hash方法作用是一致的。

otherClass.m
person *pp = [[person alloc] initWithUserName:@"1111"];
person *pp11 = [[person alloc] initWithUserName:@"1111"];
NSMutableSet *set = [NSMutableSet set];
[set addObject:pp];
[set addObject:pp11];

NSLog(@"=====%@",set);

期望輸出:set中置于一個元素,因為我們重定義了isEqual,只要是userName相同,我們就認為對象是相同的。所以pp和pp11在這里是相等的,不應該被他添加到集合中。
實際輸出:2個元素都被加入了集合中。
原因分析:在添加第二個元素時,因為hash值返回的是每個對象的內存地址,所以被判斷為不相等,沒有執行isEqual:函數。幸而直接被添加進set中。

疑問:如果把上面的代碼作如下改動:

//添加代碼
person *pp22 = [[person alloc] initWithUserName:@"1111"];
[set addObject:pp22];
NSLog(@"=====%@",set);

會發現pp22沒有被添加到集合中,打印pp22 的hash值發現同pp、pp11不相同,而且這里卻執行了isEqual:函數。所以不明白為什么沒有添加進去? 如果有同學有好的理解,請在評論區跟我分享。

如何重寫hash函數:

直接說結論:

將對象關鍵屬性的hash值進行位或運算,將運算結果作為對象的hash值。

這里只是提供了一種還算不錯的實現方式,諸多開源庫其實都有很好的實踐。大家可自行參閱。

代碼示例:

- (NSUInteger)hash {
    return [self.userName hash] ^ [self.lastName hash];
}

hash的設計是為了快速查找,要盡可能的避免hash沖突,也就是不滿足isEqueal的兩個元素,盡量hash不相等,在設計hash的時候要考慮,是否會比較輕易的出現兩個不等的對象hash值相等的情況。如果是,那就需要重新設計hash函數的實現。
以上面的實現為例。(例如有人曾給出這個例子)john smith 和 smith john結果是一樣的。
所以這里比較好的實踐為:

- (NSUInteger)hash {
  return [self.firstName hash << 8] ^ [self.secondName hash];
}

添加進集合后,保證對象的hash值不可變:

如果重寫了對象的hash函數,而且把對象作為 基于“哈希表”實現的集合(NSSet、NSDictionary、NSMapTable、NSHashTable)中的key時,需要保證在集合內的期間,對象的hash不變。
看下面代碼具體解釋下:

NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setObject:@"hhhhh" forKey:pp];
NSLog(@"====%@",[dic objectForKey:pp]);

結果:輸出hhh
但是我們稍加改造,如下:

    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    [dic setObject:@"hhhhh" forKey:pp];
    NSLog(@"====%@",[dic objectForKey:pp]);
    pp.userName = @"test";
    NSLog(@"====%@",[dic objectForKey:pp]);
結果:
 TestIsE&Hash[7399:1002659] ====hhhhh
 TestIsE&Hash[7399:1002659] ====(null)

當對象在集合內期間,如果改變了對象的hash值,會導致hash表結構的結合無法正確查找的問題。

添加isEqualXXX:函數:

NSObject子類重寫了isEqual:后,需要做一下三方面的工作:

1、實現一個新的 isEqualTo__ClassName__ 方法,進行實際意義上的值的比較。
2、重載 isEqual: 方法進行類和對象的本體性檢查,如果失敗則回退到上面提到的值(相等性)比較方法。
3、重載 hash 方法。

參考:
isEqual & hash
不懂isEqual
解析和重寫NSObjetc的isEqual和Hash
Equality

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380