IsEqual與Hash個人理解 isEqual NSObject類的實例方法: 主要是根據對象的記憶體地址來判斷兩個對象是否相等,這裡與 效果相同。 isEqualToString (BOOL)isEqualToString:(NSString )aString 是NSString類的實例方法,它主 ...
IsEqual與Hash個人理解
isEqual
NSObject類的實例方法: - (BOOL)isEqual:(id)object
主要是根據對象的記憶體地址來判斷兩個對象是否相等,這裡與 ==
效果相同。
-
isEqualToString
(BOOL)isEqualToString:(NSString *)aString 是NSString類的實例方法,它主要用於比較兩個字元串中的內容是否相同,而非比較兩個字元串所在記憶體地址。該方法常用。
-
自定義類的isEqual
在開發需求中,如果有比較兩個類對象是否具有等同性時,通常會根據需求來對父類的isEqual進行改寫,這裡的做法通常是:
-
先創建自定義的等同性判斷方法,代碼如下:
-(BOOL)isEqualToStudent:(Student *)otherStudent{ if(self == otherStudent) return YES; if(![_name isEqualToString:otherStudent.name]) return NO; if(_age != otherStudent.age) return NO; return YES; }
-
在編寫完判定方法後,應改寫類中的isEqual實例方法。如果傳入的對象為同一個類時,採用剛剛寫的自定義方法來判定,否則就交給父類判斷:
-(BOOL)isEqual:(id)object{ if([self class] == [object class]) return [self isEqualToStudent:(Student *)object]; else return [super isEqual:object]; }
-
Hash
Hash主要是用於NSMutableSet中的判斷添加的新對象是否已經存在了容器當中,如果不存在,則將新對象放入Set容器中,而判斷的依據就是根據實例對象的Hash屬性。
但是因為不同對象的Hash值會有衝突的可能性(相同的對象Hash值一定相同,這裡的相同指的是記憶體地址相同),所以最後如果哈希值發生衝突的話,則會調用類中的isEqual方法進行等同性判斷。
既然都會調用isEqual方法,那麼為什麼不直接按順序遍歷set容器,依次調用isEqual方法?
答:首先set容器是非順序容器,它的記憶體空間結構不是按順序存儲的,如果按插入的順序遍歷,開支很大(當然set的插入是無順序的)。其次除非是大量數據的存儲才會發生衝突的可能,在少量數據的情況下基本上不會出現依次遍歷衝突對象的isEqual方法的衝突情況。這樣看來以O(1)的時間複雜度就可以完成的任務,當然就選擇是它了。
在NSObject對象中,Hash值是根據對象所在的記憶體空間來進行計算的。所以在修改了isEqual方法後,如果沒有修改Hash的setter方法,系統在執行set容器操作該對象時,仍然會以記憶體地址為準來判斷兩個對象是否相同。所以為了防止出現這種情況,在修改等同性判斷方法的時候應順便修改Hash的setter方法
-
自定義Hash方法
當自定義完isEqual後,一般為了防止後面的開發用到有關set容器出現意外,所以一般都會對Hash的Setter方法進行修改。代碼如下:
-(NSUInteger)hash{ //這裡的字元串只要能體現出類對象的name和age即可(依需求而定) NSString* stringToHash=[NSString stringWithFormat:@"%@ %i",_name,_age]; return [stringToHash hash]; }
但這裡有個弊端,因為在這裡新建一個字元串的開銷很大(與返回一個屬性值相比較)。所以一般採取下麵這種策略:
-(NSUInteger)hash{ NSUInteger nameHash=[_name hash]; NSUInteger ageHash=_age; return nameHash^ageHash; }
這種方法既考慮了開銷也考慮了屬性的相關性和隨機性。---------Effective OC
-
Hash的調用順序
例子如下:
//Student.m -(BOOL)isEqualToStudent:(Student *)otherStudent{ if(self == otherStudent){ NSLog(@"(%@:%i)與(%@:%i)衝突了",_name,_age,otherStudent.name,otherStudent.age); return YES; } if(![_name isEqualToString:otherStudent.name]) return NO; if(_age != otherStudent.age) return NO; NSLog(@"[%@:%i]與[%@:%i]衝突了",_name,_age,otherStudent.name,otherStudent.age); return YES; } -(BOOL)isEqual:(id)object{ if([self class] == [object class]) return [self isEqualToStudent:(Student *)object]; else return [super isEqual:object]; } -(NSUInteger)hash{ return _age; } -(NSString *)description{ return [NSString stringWithFormat:@"%@:%i",_name,_age ]; }
//main.m Student* stu1 = [[Student alloc] initWithName:@"小明" age:10]; Student* stu2 = [[Student alloc] initWithName:@"小明" age:10]; Student* stu3 = [[Student alloc] initWithName:@"小明" age:0]; NSMutableSet* set = [NSMutableSet set]; [set addObject:stu1]; //步驟1 [set addObject:stu2]; //步驟2 [set addObject:stu3]; //步驟3 NSLog(@"%@",set);
輸出結果:
2020-05-08 22:50:12.172207+0800 effective-OC-test[83370:76838657] [小明:10]與[小明:10]衝突了 2020-05-08 22:50:12.172542+0800 effective-OC-test[83370:76838657] {( 小明:0, 小明:10 )} Program ended with exit code: 0
-
當執行步驟1時,會先去調用hash方法得到對象的哈希值,因為set容器為空,所以直接根據hash值將對象存入記憶體當中。stu1成功存入容器中;
-
當執行步驟2時,同樣會先去調用hash方法得到哈希值,因為這裡的hash值與age有關,stu1.age==stu2.age,所以兩個對象的哈希值相同,這時候就回去調用isEqual方法去判斷是否兩個對象是等同的。又因為我這裡設置的當姓名和年齡一致時,即判斷為同一對象,所以這裡會列印出衝突。並且不會將stu2存入容器中;
-
當執行步驟3時,調用完hash方法後,因為age不同,所以得到的hash值不同(很小幾率會出現相同),從而直接通過的到的hash值去分配記憶體空間。stu3成功存入容器中。
-
如果這裡將isEqualToStudent的判斷條件修改一下,將年齡處修改為:
if(_age == otherStudent.age) return NO;
這時候,再執行一遍,輸出如下:
2020-05-08 23:02:13.177534+0800 effective-OC-test[84327:76859194] {( 小明:0, 小明:10, 小明:10 )}
這時候就會發現stu2已經成功存入了容器中,因為在hash值衝突後調用isEqualToStudent方法後,因為兩個年齡一致,返回了NO,說明兩個對象這時候已經被當成了不同的兩個對象。
-
總結
- isEqual方法是用來判斷類對象的等同性。在自定義等同性判斷時,如果判斷對象與該對象同屬一個類時,就調用自己編寫的判定方法,否則就交給父類的isEqual來解決。
- Hash值一般與容器相關,特別是Set容器。set容器通過對象的哈希值來分配記憶體地址,當遇到hash衝突後時會調用isEqual來進行二次驗證。
- 原生NSObject類的Hash值和isEqual方法都是根據對象的記憶體地址得到的結果。