淺析對(duì)象等同性判斷

前言

對(duì)數(shù)據(jù)的等同性判斷包括對(duì)基本數(shù)據(jù)類型等同性的判斷和對(duì)象等同性的判斷。對(duì)基本數(shù)據(jù)類型等同性的判斷是非常簡單的,比如對(duì)兩個(gè)NSInteger類型的變量等同性判斷,我們直接使用關(guān)系運(yùn)算符“==”即可。
相比于基本數(shù)據(jù)類型等同性熄诡,對(duì)象等同性的判斷就稍顯復(fù)雜。按照大神Mattt Thompson的說法徒扶,對(duì)象的等同性包括相等性本體性粮彤。從字面不難發(fā)現(xiàn),相等性是指:兩個(gè)對(duì)象的值是否相等姜骡。本體性是指:兩個(gè)對(duì)象本質(zhì)上是否是同一個(gè)對(duì)象导坟。

關(guān)系運(yùn)算符"=="不僅可以應(yīng)用在基本數(shù)據(jù)類型上,還可以應(yīng)用在兩個(gè)對(duì)象類型的對(duì)象上圈澈。不過惫周,按照==”比較兩個(gè)對(duì)象,本質(zhì)上是對(duì)兩個(gè)對(duì)象指針地址的比較康栈,即對(duì)象本體性的判斷递递。單純的比較兩個(gè)對(duì)象的指針并不能完全滿足要求喷橙,因?yàn)閷?duì)象的等同性不僅包括本體性,還包括相等性登舞。有時(shí)候指向兩個(gè)對(duì)象的指針雖然不相同贰逾,但是兩個(gè)對(duì)象的值是相同的,我們也認(rèn)為其是相同的菠秒,即相等性疙剑。換句話說,單純的通過比較兩個(gè)對(duì)象的指針來判斷等同性總是太過苛刻践叠。而對(duì)于自定義的類型言缤,開發(fā)中經(jīng)常要對(duì)兩個(gè)對(duì)象的相等性進(jìn)行判斷,即對(duì)兩個(gè)對(duì)象每個(gè)屬性進(jìn)行比較禁灼。如果兩個(gè)對(duì)象的類型相同管挟,且屬性值都一樣,我們也會(huì)認(rèn)為其是相等的弄捕。如果對(duì)象是集合類型僻孝,比如數(shù)組,相等性檢查要求我們對(duì)兩個(gè)數(shù)組相同位置的元素進(jìn)行逐個(gè)比較察藐。

NSFoundation提供的一些方法

Objective-C的NSFoundation框架中給我們提供了很多判斷對(duì)象等同性的方法皮璧。比如:

// NSString類提供了判斷兩個(gè)NSString對(duì)象是否相等的方法
- (BOOL)isEqualToString:(NSString *)aString;
// NSArray類提供了判斷兩個(gè)NSAarray對(duì)象是否相等的方法
- (BOOL)isEqualToArray:(NSArray<ObjectType> *)otherArray;
// NSDictionary類提供了判斷兩個(gè)NSDictionary對(duì)象是否相等的方法
- (BOOL)isEqualToDictionary:(NSDictionary<KeyType, ObjectType> *)otherDictionary;
// NSSet類提供了判斷兩個(gè)NSSet對(duì)象是否相等的方法
- (BOOL)isEqualToSet:(NSSet<ObjectType> *)otherSet;
// ......
- (BOOL)isEqualToData:(NSData *)other;
- (BOOL)isEqualToNumber:(NSNumber *)number;
- (BOOL)isEqualToValue:(NSValue *)value;
- (BOOL)isEqualToTimeZone:(NSTimeZone *)aTimeZone;
- (BOOL)isEqualToDate:(NSDate *)otherDate;
- (BOOL)isEqualToOrderedSet:(NSOrderedSet<ObjectType> *)other;
- (BOOL)isEqualToHashTable:(NSHashTable<ObjectType> *)other;
- (BOOL)isEqualToIndexSet:(NSIndexSet *)indexSet;

前面說舟扎,對(duì)于NSFoundation框架中的一些類分飞,蘋果已經(jīng)為我們提供了現(xiàn)成的等同性判斷的方法。比如NSString提供的判斷兩個(gè)字符串對(duì)象是否相等的方法- (BOOL)isEqualToString:睹限。

那么你可能會(huì)問:NSString類默認(rèn)提供了比較字符串等同性的方法譬猫,而那些繼承自NSObject基類的自定義類,我們該怎么判斷等同性呢羡疗?不用擔(dān)心染服,NSObject類的協(xié)議已經(jīng)默認(rèn)提供了- (BOOL)isEqual:(id)object;方法,且NSObject類也遵守并實(shí)現(xiàn)了NSObject協(xié)議中的isEqual:方法叨恨。我們可以通過調(diào)用- (BOOL)isEqual:方法來檢驗(yàn)兩個(gè)NSObject對(duì)象的等同性柳刮。其實(shí),個(gè)人認(rèn)為痒钝,NSString的- (BOOL)isEqualToString:就是在- (BOOL)isEqual:基礎(chǔ)之上進(jìn)行的擴(kuò)展秉颗。因?yàn)镹SString類繼承自NSObject這個(gè)基類,我們也可以使用- (BOOL)isEqual:方法對(duì)兩個(gè)字符串進(jìn)行比較送矩。但是不建議這么做蚕甥,因?yàn)橄到y(tǒng)已經(jīng)給我們提供了現(xiàn)成的API,調(diào)用- (BOOL)isEqualToString:比調(diào)用- (BOOL)isEqual:方法快栋荸。后者還要執(zhí)行額外的步驟菇怀,因?yàn)樗恢朗軠y對(duì)象的真實(shí)類型凭舶。

覆寫NSObject類的- (BOOL)isEqual:方法

NSObject類對(duì)- (BOOL)isEqual:的默認(rèn)實(shí)現(xiàn)是:當(dāng)且僅當(dāng)被比較的兩個(gè)對(duì)象的指針值相等時(shí),才被認(rèn)為相等。即仿野,isEqual:的默認(rèn)實(shí)現(xiàn)就是對(duì)對(duì)象本體性的判斷稍算。前面已經(jīng)說過,但對(duì)于自定義類型和集合類型义屏,這種默認(rèn)的判斷有時(shí)候太過苛刻。針對(duì)于這種情況蜂大,如果有判斷自定義對(duì)象等同性的需求闽铐,我們需要覆寫- (BOOL)isEqual:方法。

- (BOOL)isEqual:(id)object{
    // 兩個(gè)對(duì)象指針相等奶浦,其指向同一塊內(nèi)存兄墅,則肯定相等。
    if (self == object) {
        return YES;
    }

    // 一般來說澳叉,如果兩個(gè)對(duì)象的類型完全不同隙咸,則肯定不等。
    if ([self class] != [object class]) {
        return NO;
    }
    EOCPerson *otherPerson = (EOCPerson *)object;
    // 兩個(gè)對(duì)象相應(yīng)的屬性如果不等成洗,則也認(rèn)為不等(忽略繼承和多態(tài))五督。
    if (![self.firstName isEqualToString:otherPerson.firstName]){
        return NO;
    }
    if (![self.lastName isEqualToString:otherPerson.lastName]){
        return NO;
    }
    if (self.age != otherPerson.age) {
        return NO;
    }
    // 如果屬性也相等,則認(rèn)為相等瓶殃。
    return YES;
}

上面的EOCPerson類充包,實(shí)現(xiàn)了NSObject協(xié)議的- (BOOL)isEqual:方法,首先遥椿,直接判斷兩個(gè)指針是否相等基矮,若相等則其均指向同一個(gè)對(duì)象,所以受測對(duì)象肯定相等冠场。然后家浇,比較兩個(gè)受測對(duì)象所屬的類,若不屬于同一個(gè)類(忽略多態(tài))碴裙,則認(rèn)為兩對(duì)象不相等钢悲。最后,檢查兩個(gè)對(duì)象的屬性是否相等舔株,如果對(duì)象只要有某個(gè)屬性不相等莺琳,就認(rèn)為兩個(gè)對(duì)象不相等,否則對(duì)象相等督笆。

EOCPerson *p1 = [[EOCPerson alloc] init];
    p1.firstName = @"VV";
    p1.lastName = @"S";
    
    EOCPerson *p2 = [[EOCPerson alloc] init];
    p2.firstName = @"VV";
    p2.lastName = @"S";
    
    BOOL isEqual = [p1 isEqual:p2];
    NSLog(@"isEqual == %d",isEqual); // isEqual == 1
    

上面我們覆寫EOCPerson類的isEqual:方法時(shí)芦昔,沒有考慮多態(tài)的情況,開發(fā)中如果存在繼承娃肿,我們還需要對(duì)兩個(gè)對(duì)象的類型進(jìn)行比較咕缎,直接調(diào)用NSObject類型查詢方法- (BOOL)isKindOfClass:(Class)aClass;即可珠十。

- (BOOL)isEqual:(id)object{
    // return [super isEqual:object];
    // 考慮多態(tài)
    if (![self isKindOfClass:[object class]] && ![object isKindOfClass:[self class]]) {
        return NO;
    }
    
    // 兩個(gè)對(duì)象指針相等,其指向同一塊內(nèi)存凭豪,則肯定相等焙蹭。
    if (self == object) {
        return YES;
    }
    EOCPerson *otherPerson = (EOCPerson *)object;
    // 兩個(gè)對(duì)象相應(yīng)的屬性如果不等,則也認(rèn)為不等嫂伞。
    if (![self.firstName isEqualToString:otherPerson.firstName]){
        return NO;
    }
    if (![self.lastName isEqualToString:otherPerson.lastName]){
        return NO;
    }
    if (self.age != otherPerson.age) {
        return NO;
    }
    // 如果屬性也相等孔厉,則認(rèn)為相等。
    return YES;
}

Hash方法

Hash一詞經(jīng)常被譯作“哈咸”撰豺,有時(shí)候也被譯作“雜湊”“散列”。因此拼余,“hash table”有時(shí)候被譯作“哈希表”污桦,也有人稱之為“散列表”。我們只需要知道他們表達(dá)的是同一個(gè)意思匙监。

1.為什么要有Hash方法

根據(jù)約定:如果兩個(gè)對(duì)象相等凡橱,則其哈希值也相等,但是如果兩個(gè)哈希值相等亭姥,則對(duì)象未必相等稼钩。這是能否覆寫isEqual:方法的關(guān)鍵。
另外达罗,我們知道坝撑,哈希也是會(huì)存在碰撞的。即氮块,兩個(gè)對(duì)象如果相等則哈希值肯定相等绍载。但兩個(gè)對(duì)象的哈希值如果相等,則這兩個(gè)對(duì)象也不一定相等滔蝉。

哈希表是一種數(shù)據(jù)結(jié)構(gòu),經(jīng)常被用來實(shí)現(xiàn)set和dictionary塔沃。我們知道蝠引,set和dictionary都屬于collection(集合)的一種形式。set和dictionary相對(duì)于array而言蛀柴,是其查詢速度是比較快的螃概,事件復(fù)雜度僅為O(1)。而對(duì)于一個(gè)無序的array鸽疾,查詢某個(gè)元素的事件復(fù)雜度是O(n)(其中n為數(shù)組的長度)吊洼。這其中hash就起到了至關(guān)重要的作用。

2.Hash方法的默認(rèn)實(shí)現(xiàn)

hash的默認(rèn)實(shí)現(xiàn)是:返回對(duì)象的內(nèi)存地址作為哈希值制肮。即冒窍,NSObject類實(shí)現(xiàn)的hash方法递沪,本質(zhì)上是返回的對(duì)象的內(nèi)存地址。乍一想综液,這種默認(rèn)實(shí)現(xiàn)是可行的款慨,但是對(duì)于一些數(shù)據(jù)類型是不行的,比如我們自定義的類型谬莹。


    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    EOCPerson *p1 = [[EOCPerson alloc] init];
    p1.firstName = @"VV";
    p1.lastName = @"S";
    
    EOCPerson *p2 = [[EOCPerson alloc] init];
    p2.firstName = @"VV";
    p2.lastName = @"S";
    
    BOOL isEqual = [p1 isEqual:p2];
    NSLog(@"isEqual == %d",isEqual);
    
    NSMutableSet *set = [NSMutableSet set];
    [set addObject:p1];
    [set addObject:p2];
    NSLog(@"count == %ld",[set count]); // count == 2

如上檩奠,我們沒有覆寫默認(rèn)的isEqual:方法和默認(rèn)的hash方法。p1和p2雖然指針不同附帽,但是對(duì)象的屬性都是完全相同的埠戳。我們把本質(zhì)上兩個(gè)完全相同的對(duì)象插入到set這種數(shù)據(jù)類型中,set應(yīng)該是可以自動(dòng)去重的蕉扮。即乞而,正確的情況下set應(yīng)該只有一個(gè)對(duì)象。但是因?yàn)閔ash方法默認(rèn)返回指針地址作為哈希值慢显,導(dǎo)致set中出現(xiàn)了兩個(gè)本質(zhì)上完全相同的對(duì)象爪模,這完全違背了set的機(jī)制和作用。所以荚藻,返回內(nèi)存地址作為哈希值并不是一個(gè)好主意屋灌。
上面說過,根據(jù)約定:如果兩個(gè)對(duì)象相等应狱,則其哈希值也相等共郭,但是如果兩個(gè)哈希值相等,則對(duì)象未必相等疾呻。所以我們可以這么實(shí)現(xiàn)hash方法:

- (NSUInteger)hash {
    return 666;
}

這么寫顯然符合約定除嘹,即相等的對(duì)象hash值相等,相等hash值的對(duì)象未必相等岸蜗。但是在set中大量使用這種對(duì)象將會(huì)產(chǎn)生性能問題尉咕。因?yàn)閟et在檢索哈希表時(shí),會(huì)用對(duì)象的哈希值作為索引璃岳。set會(huì)根據(jù)哈希值把對(duì)象分組年缎。在向set中添加新對(duì)象時(shí),要根據(jù)待插入的新對(duì)象的哈希值找到與之相關(guān)的那個(gè)組铃慷。然后依次檢查各個(gè)元素(調(diào)用isEqual:方法)单芜,看待插入的對(duì)象是否和數(shù)組中的某個(gè)元素相等,如果相等犁柜,那么就說明待添加的對(duì)象已經(jīng)在set中存在洲鸠。由此可知,如果令每個(gè)對(duì)象都返回相同的哈希值馋缅,那么在set中有1000000個(gè)對(duì)象的情況下扒腕,若是繼續(xù)想其中添加對(duì)象绢淀,則需要將這1000000個(gè)對(duì)象全部遍歷一遍。這樣一來就喪失了hash值的作用袜匿,把set變成了一個(gè)活生生的array更啄。

稍微好一點(diǎn)的方法

- (NSUInteger)hash {
    NSString *stringToHash = [NSString stringWithFormat:@"%@,%@,%ld",self.firstName,self.lastName,self.age];
    return [stringToHash hash];
}

這次所使用的方法是將NSString對(duì)象中的屬性拼接成一個(gè)新的字符串,然后另該字符串調(diào)用hash方法居灯,返回該字符串的哈希值作為這個(gè)對(duì)象的哈希值祭务。這么做符合約定,因?yàn)閮蓚€(gè)相等的對(duì)象總是會(huì)返回相同的哈希值怪嫌。但是這樣做還需要負(fù)擔(dān)創(chuàng)建一個(gè)新字符串的額外的開銷义锥,所以比返回一個(gè)單一值慢。相對(duì)而言岩灭,把這種對(duì)象添加到collection中拌倍,也會(huì)產(chǎn)生性能問題。

更加優(yōu)秀的方法

分別計(jì)算每個(gè)屬性的哈希值噪径,然后對(duì)哈希值進(jìn)行按位異或運(yùn)算柱恤,的出的結(jié)果作為對(duì)象的哈希值。

- (NSUInteger)hash {
    NSInteger firstNameHash = [self.firstName hash];
    NSInteger lastNameHash = [self.lastName hash];
    NSInteger ageHash = self.age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

對(duì)哈希值進(jìn)行按位異或操作找爱,這種方式既能保持較高的效率梗顺,又能使生成的哈希值至少位于一定范圍之內(nèi),而不會(huì)過于頻繁的重復(fù)车摄。當(dāng)然寺谤,此算法的哈希值還是會(huì)生成碰撞,不過至少可以保證哈希值有多重可能的取值吮播,編寫hash方法時(shí)变屁,應(yīng)該用當(dāng)前的對(duì)象多做做實(shí)驗(yàn),以便在減少碰撞頻度與降低運(yùn)算復(fù)雜程度之間取舍意狠。

3.Hash方法調(diào)用時(shí)機(jī)

當(dāng)把一個(gè)對(duì)象添加到set時(shí)會(huì)調(diào)用這個(gè)對(duì)象的hash方法粟关。或者把一個(gè)對(duì)象作為key添加到dictionary中時(shí)摄职,也會(huì)調(diào)用這個(gè)對(duì)象的hash方法誊役。因?yàn)閐ictionary在查找某個(gè)value時(shí),也是根據(jù)key的hash值來提高查詢效率谷市。
當(dāng)然,如果我們把對(duì)象作為value添加到dictionary中击孩,并不會(huì)調(diào)用對(duì)象的hash方法迫悠。
注意:如果一個(gè)自定義對(duì)象作為dictionary的key,切記要實(shí)現(xiàn)NSCopying協(xié)議中的- (id)copyWithZone:(nullable NSZone *)zone方法巩梢。如下:

- (id)copyWithZone:(nullable NSZone *)zone {
    EOCPerson *copy = [[EOCPerson alloc] init];
    copy.lastName = self.lastName;
    copy.firstName = self.firstName;
    copy.age = self.age;
    return copy;
}

4.Hash方法與isEqual:的關(guān)系

拿set為例创泄,為了優(yōu)化插入效率艺玲,當(dāng)在set中插入某個(gè)對(duì)象時(shí),首先會(huì)調(diào)用待插入對(duì)象的hash方法鞠抑,根據(jù)返回的hash值查找hash table饭聚。如果待插入對(duì)象的hash值和set中的對(duì)象的hash值都不相等。則認(rèn)為set中不存在和待插入對(duì)象相等的對(duì)象搁拙,那么就可以把待插入的對(duì)象插入到set中秒梳。如果set中存在一個(gè)對(duì)象的hash值和待插入對(duì)象的hash值相等,則再調(diào)用對(duì)象的isEqual:方法箕速,進(jìn)行對(duì)象的判等酪碘,如果經(jīng)過isEqual:方法返回YES,則認(rèn)為兩個(gè)對(duì)象相等盐茎,即set中已經(jīng)存在一個(gè)和待插入對(duì)象相等的對(duì)象兴垦,待插入的對(duì)象不能插入到set中。否則繼續(xù)上面的操作字柠。

isEqual:調(diào)用時(shí)機(jī)

  • 當(dāng)手動(dòng)調(diào)用isEqual:方法探越,對(duì)兩個(gè)對(duì)象進(jìn)行顯式的比較時(shí)。
  • 當(dāng)把一個(gè)對(duì)象添加到一個(gè)成員count不為0的set中窑业,且待插入的對(duì)象的hash值和set中的成員的hash值相等的情況下钦幔,才會(huì)調(diào)用isEqual:方法。即数冬,首先調(diào)用hash方法节槐,然后才有可能調(diào)用isEqual:方法拐纱。

等同性判定的執(zhí)行深度

創(chuàng)建等同性判定方法時(shí),需啊喲決定是根據(jù)某個(gè)對(duì)象來判斷等同性揍庄,還是僅根據(jù)其中的某個(gè)或者某幾個(gè)屬性來判斷,這個(gè)取決于業(yè)務(wù)場景东抹。NSArray的檢測方式為:先看兩個(gè)數(shù)組所含對(duì)象個(gè)數(shù)是否等蚂子,若想等缭黔,則在每個(gè)對(duì)應(yīng)位置的兩個(gè)對(duì)象身上調(diào)用“isEqual:”方法。如果對(duì)應(yīng)位置上的對(duì)象均相等馏谨,那么這兩個(gè)數(shù)組相等别渔,這叫做“深度等同性判定”。不過有時(shí)無需將所有數(shù)據(jù)逐個(gè)比較哎媚,只根據(jù)其中部分?jǐn)?shù)據(jù)即可判斷二者是否相同。比如某個(gè)Person類總有一個(gè)identity字段代表身份證號(hào)碼拨与,在不存在臟數(shù)據(jù)的情況下买喧,完全可以僅憑這個(gè)identity字段判斷兩個(gè)對(duì)象是否是相同的。

不要向set中添加可變的對(duì)象

不要向set中添加可變的對(duì)象岗喉。確切的說,如果向set中添加了可變對(duì)象荚斯,那么盡量保證這個(gè)可變對(duì)象不再改變查牌。為什么呢?我們已經(jīng)了解兽泣,set和dictionary是通過哈希值檢索元素的胁孙,我們已經(jīng)說過,set火把各個(gè)對(duì)象按照其哈希值進(jìn)行分組稠鼻,如果某個(gè)可變對(duì)象在set中被分組后哈希值又改變了狂票,那么這個(gè)對(duì)象現(xiàn)在所在的組就不再合適了。要想解決這個(gè)問題闺属,我們需要確保被添加到set中的對(duì)象是不可變的或者確保可變對(duì)象被添加到set后就不再改變亚皂,或者這個(gè)對(duì)象的hash值的計(jì)算不受可變部分的影響国瓮,即匠楚,這個(gè)對(duì)象的hash值不是根據(jù)其可變部分計(jì)算出來的。

// 重寫isEqual:
- (BOOL)isEqual:(id)object{
    // return [super isEqual:object];
    // 一般來說峡懈,如果兩個(gè)對(duì)象的類型完全不同肪康,則肯定不等。
    if ([self class] != [object class]) {
        return NO;
    }
    // 兩個(gè)對(duì)象指針相等谒撼,其指向同一塊內(nèi)存雾狈,則肯定相等。
    if (self == object) {
        return YES;
    }
    EOCPerson *otherPerson = (EOCPerson *)object;
    // 兩個(gè)對(duì)象相應(yīng)的屬性如果不等辩蛋,則也認(rèn)為不等移盆。
    if (![self.firstName isEqualToString:otherPerson.firstName]){
        return NO;
    }
    if (![self.lastName isEqualToString:otherPerson.lastName]){
        return NO;
    }
    if (self.age != otherPerson.age) {
        return NO;
    }
    // 如果屬性也相等咒循,則認(rèn)為相等。
    return YES;
}
// 重寫hash
- (NSUInteger)hash {
    NSLog(@"%s",__func__);
    NSInteger firstNameHash = [self.firstName hash];
    NSInteger lastNameHash = [self.lastName hash];
    NSInteger ageHash = self.age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}
// 調(diào)用
    EOCPerson *p1 = [[EOCPerson alloc] init];
    p1.firstName = @"VV";
    p1.lastName = @"S";
    
    EOCPerson *p2 = [[EOCPerson alloc] init];
    p2.firstName = @"VV";
    p2.lastName = @"S";
    
    NSMutableSet *setM = [NSMutableSet set];
    [setM addObject:p1];
    [setM addObject:p2];
    NSLog(@"set count == %ld",[setM count]);  // set count == 1
    

上面我們看到颖医,向set中添加兩個(gè)相同的對(duì)象蚁署,firstName和lastName值完全相同,打印set中元素的個(gè)數(shù)哪痰,其打印結(jié)果為1久妆。這樣完全符合set能夠去重的功能筷弦。但是抑诸,如果我們繼續(xù)添加一個(gè)不同于p1和p2的p3對(duì)象爹殊,然后改變p3的各個(gè)屬性和p1相同,再觀察set count层玲,如下:

    EOCPerson *p1 = [[EOCPerson alloc] init];
    p1.firstName = @"VV";
    p1.lastName = @"S";
    
    EOCPerson *p2 = [[EOCPerson alloc] init];
    p2.firstName = @"VV";
    p2.lastName = @"S";
    
    NSMutableSet *setM = [NSMutableSet set];
    [setM addObject:p1];
    [setM addObject:p2];
    NSLog(@"set count == %ld",[setM count]); // set count == 1
    
    EOCPerson *p3 = [[EOCPerson alloc] init];
    p3.firstName = @"VV";
    [setM addObject:p3];
    NSLog(@"set count == %ld",[setM count]); // set count == 2
    
    p3.lastName = @"S";
    NSLog(@"set count == %ld",[setM count]); // set count == 2

我們看到辛块,上面給setM對(duì)象添加了一個(gè)p3后铅碍,其count == 2,這樣是ok的尘盼。但是把p3的lastName改為和p1的lastName相同時(shí)呜魄,set count 仍然為2爵嗅。此時(shí)set中竟然出現(xiàn)了兩個(gè)完全相同的對(duì)象!這完全違背了set的本意趟庄,因?yàn)閟et的作用就是去重伪很,根據(jù)set的語義,set中是不會(huì)也不應(yīng)該出現(xiàn)了兩個(gè)完全相同的對(duì)象猫十。
如果把這個(gè)setM對(duì)象在拷貝一下呆盖,情況更糟了:

NSSet *s = [setM copy];
NSLog(@"set count == %ld",[s count]); // set count == 1
    

你會(huì)發(fā)現(xiàn),s對(duì)象雖然是setM的副本宙项,但是s.count卻是1株扛。此s對(duì)象看上去像是由一個(gè)空set開始,通過把setM中的對(duì)象添加到s中而創(chuàng)建出來的盆繁。無論如何改基,這樣做已經(jīng)存在了很大的風(fēng)險(xiǎn),這可能給我們的程序調(diào)試帶來無法想象的難度。
舉這個(gè)例子是想說明:把某個(gè)對(duì)象放入set這種集合對(duì)象中躁染,就不宜改變其內(nèi)容吞彤。

總結(jié)

  1. 把某個(gè)對(duì)象添加到set中時(shí),都會(huì)調(diào)用這個(gè)對(duì)象的hash方法計(jì)算hash值挠羔。
  2. 把一個(gè)對(duì)象添加到set中時(shí)埋嵌,如果set中不存在任何元素,這個(gè)對(duì)象會(huì)被直接添加到set中范舀。相反了罪。如果set中存在元素泊藕,那么待添加的對(duì)象的hash值會(huì)和set中的每個(gè)元素的hash值進(jìn)行比較。如果不等玫锋,會(huì)繼續(xù)和set中的下一個(gè)元素比較hash值踊餐,直到待添加的對(duì)象的hash值和set中所有元素的hash值比較完畢為止。
  1. 如果set中存在一個(gè)元素的hash值和待添加的對(duì)象的hash值相等三痰,那么待插入的對(duì)象會(huì)調(diào)用自己的isEqual:方法散劫,以set中的元素為參數(shù),進(jìn)行比較获搏,如果isEqual:返回YES常熙,證明這兩個(gè)對(duì)象相同,那么待插入的對(duì)象不會(huì)插入到set中仿贬。如果isEqual:返回NO墓贿,證明這兩個(gè)對(duì)象不同,對(duì)象可以插入到set中队伟。
  2. hash的默認(rèn)實(shí)現(xiàn)是返回對(duì)象的指針地址嗜侮。isEqual:的默認(rèn)實(shí)現(xiàn)是比較兩個(gè)對(duì)象的指針地址代嗤。
  3. 相同的對(duì)象必須具有相同的hash值,但是兩個(gè)hash值相等的對(duì)象未必相同宜猜。
  4. 若想檢測對(duì)象的等同性硝逢,需要提供“isEqual:”與hash方法渠鸽。
  5. 根據(jù)實(shí)際也需重寫isEqual:方法,不要盲目檢查每條屬性憨奸,而是應(yīng)該按照具體需求來指定檢查方案凿试。
  6. 最好不要把可變對(duì)象添加到set中似芝,最好也請(qǐng)不要改變set中某個(gè)元素党瓮,否則容易產(chǎn)生想象不到的錯(cuò)誤盐类,也會(huì)增加調(diào)試的難度。
  7. hash方法應(yīng)該使用計(jì)算速度快而且哈希值碰撞幾率低的算法枪萄。一般情況下硬毕,建議使用按位異或操作吐咳。

最后元践,借用大神“Mattt Thompson”的話:
經(jīng)過這么多的解釋,希望我們在這個(gè)有些詭譎的話題上取得了”相同“的認(rèn)識(shí)沪羔。 作為人類蔫饰,我們很努力地去理解和實(shí)現(xiàn)平等愉豺,在我們的社會(huì)中,在自然生態(tài)環(huán)境中杖剪,在立法和執(zhí)法中驰贷,在選舉我們領(lǐng)導(dǎo)人的過程中括袒,在人類作為一個(gè)物種互相溝通延續(xù)我們的存在這一共識(shí)中。愿我們能繼續(xù)這個(gè)奮斗的過程芥炭,最終達(dá)到理想的彼岸,在那里米苹,評(píng)價(jià)一個(gè)人的標(biāo)準(zhǔn)是他的人格蘸嘶,就像我們判斷一個(gè)變量是通過它的內(nèi)存地址一樣陪汽。
文/VV木公子(簡書作者)
PS:如非特別說明,所有文章均為原創(chuàng)作品况增,著作權(quán)歸作者所有训挡,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)澜薄,并注明出處,所有打賞均歸本人所有颊艳!

如果您是iOS開發(fā)者忘分,或者對(duì)本篇文章感興趣妒峦,請(qǐng)關(guān)注本人,后續(xù)會(huì)更新更多相關(guān)文章绸狐!敬請(qǐng)期待累盗!

參考文章

iOS開發(fā) 之 不要告訴我你真的懂isEqual與hash!
Equality(翻譯)
Equality(英文)
isEqual & hash

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末若债,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子镜豹,更是在濱河造成了極大的恐慌蓝牲,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昔期,死亡現(xiàn)場離奇詭異硼一,居然都是意外死亡梦抢,警方通過查閱死者的電腦和手機(jī)奥吩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門霞赫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事橄抹÷ナ模” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵主守,是天一觀的道長参淫。 經(jīng)常有香客問我愧杯,道長,這世上最難降的妖魔是什么耍铜? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任棕兼,我火速辦了婚禮伴挚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摊灭。我一直安慰自己败徊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沪哺,像睡著了一般辜妓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酪夷,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天晚岭,我揣著相機(jī)與錄音勋功,去河邊找鬼狂鞋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛构回,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脐供,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼政己,長吁一口氣:“原來是場噩夢啊……” “哼掏愁!你這毒婦竟也來了果港?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤谢谦,失蹤者是張志新(化名)和其女友劉穎回挽,沒想到半個(gè)月后猩谊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牌捷,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡暗甥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年淋袖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锯梁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖合敦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情保檐,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布垒在,位于F島的核電站场躯,受9級(jí)特大地震影響旅挤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜签舞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一瘪菌、第九天 我趴在偏房一處隱蔽的房頂上張望师妙。 院中可真熱鬧屹培,春花似錦、人聲如沸蓄诽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至出吹,卻和暖如春捶牢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背渐排。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工飞盆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人次乓。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓吓歇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親票腰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子城看,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容