根據(jù)"等同性"來(lái)比較對(duì)象是一個(gè)非常有用的功能,不過(guò),按照"=="操作符比較出來(lái)的結(jié)果未必就是我們想要的.因?yàn)樵摬僮魇潜容^的兩個(gè)指針本身,而不是其所指向的對(duì)象,應(yīng)該使用NSObject協(xié)議中國(guó)聲明的"isEqual":方法來(lái)判斷兩個(gè)對(duì)象的等同性.一般來(lái)說(shuō),兩個(gè)類(lèi)型不同的對(duì)象總是不相等的.
NSObject協(xié)議中有兩個(gè)用于判斷等同性的關(guān)鍵方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject類(lèi)對(duì)這兩個(gè)方法的默認(rèn)實(shí)現(xiàn)是:當(dāng)且僅當(dāng)其"指針值(內(nèi)存地址)"完全相等時(shí),這兩個(gè)對(duì)象才相等.若想在自定義的對(duì)象中正確覆寫(xiě)這些方法,就必須先理解其約定.
如果"isEqual:"方法斷定兩個(gè)對(duì)象相等,那么其hash方法也必須返回同一個(gè)值,但是,如果兩個(gè)對(duì)象的hash方法返回同一個(gè)值,那么"isEqual:"方法未必會(huì)認(rèn)為兩者相等.
比如有下面這個(gè)類(lèi):
@interface CWGPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
我們認(rèn)為:如果兩個(gè)CWGPerson的所有字段全部相等, 那么這兩個(gè)對(duì)象就相等.于是"isEqual:"方法可以寫(xiě)成:
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if ([self class] != [object class]) return NO;
CWGPerson *otherPerson = (CWGPerson *)object;
if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
if (_age != otherPerson.age]) return NO;
return YES;
}
先判斷兩個(gè)指針是否相等,接下來(lái)判斷兩個(gè)對(duì)象所屬的類(lèi),最后檢測(cè)每個(gè)屬性是否相等。
接下來(lái)該實(shí)現(xiàn)hash方法了,下面這種寫(xiě)法完全可行:
- (NSUInteger)hash {
return 1337;
}
不過(guò)這樣的話(huà),在collection中使用這種對(duì)象將產(chǎn)生性能問(wèn)題,因?yàn)閏ollection在檢索哈希表時(shí),會(huì)用到對(duì)象的哈希碼做索引。這樣的話(huà)假如集合中有10000個(gè)對(duì)象,若是繼續(xù)向其中添加對(duì)象,則需要將這10000個(gè)對(duì)象全部掃描一遍。
hash方法也可以這樣來(lái)實(shí)現(xiàn):
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, _lastName, _age];
return [stringToHash hash];
}
這樣做將NSString對(duì)象中的熟悉都塞入另一個(gè)字符串中,然后令hash方法返回該字符串的哈希碼、這樣做符合約定,因?yàn)閮蓚€(gè)相等的CWGPerson對(duì)象總是返回相同的哈希碼。但是這樣做有額外增加了創(chuàng)建字符串的開(kāi)銷(xiāo)。
再來(lái)看一種方法:
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
這種做法既能保持較高效率,又能使生成的哈希碼至少位于一定范圍之內(nèi),而不會(huì)過(guò)于頻繁的重復(fù)。當(dāng)然,此算法生成的哈希碼還是會(huì)碰撞的,不過(guò)至少可以保證哈希碼有多種可能的取值。編寫(xiě)hash方法時(shí),應(yīng)該用當(dāng)前的對(duì)象做實(shí)驗(yàn),以便在減少碰撞頻度與降低運(yùn)算復(fù)雜程度間取舍。
特定類(lèi)所具有的等同性判定方法
如果經(jīng)常需要判斷等同性,那么可能會(huì)自己開(kāi)創(chuàng)建等同性判定方法,因?yàn)闊o(wú)需檢查參數(shù)類(lèi)型,所以能大大提升檢查速度。在編寫(xiě)判定方法時(shí),也應(yīng)該覆寫(xiě)“isEqual”方法,后者的常見(jiàn)實(shí)現(xiàn)方式為:如果受測(cè)的參數(shù)與接收該消息的對(duì)象都屬于同一個(gè)類(lèi),那么就該調(diào)用自己編寫(xiě)的判定方法,否則就交由超類(lèi)來(lái)判斷。
例如,在CWGPerson類(lèi)中可以豎線(xiàn)如下兩個(gè)方法:
- (BOOL)isEqualToPerson:(CWGPerson *)otherPerson {
if (self == object) return YES;
if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
if (_age != otherPerson.age]) return NO;
return YES;
}
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
return [self isEqualToPerson:(CWGPerson *)object];
} else {
return [super isEqual:object];
}
}
等同性判定的執(zhí)行深度
創(chuàng)建等同性判定方法時(shí),一定要根據(jù)整個(gè)對(duì)象來(lái)判斷等同性,還是僅僅根據(jù)其中幾個(gè)字段來(lái)判斷。NSArray的檢測(cè)方式為先看看兩個(gè)數(shù)組所含對(duì)象個(gè)數(shù)是否相同,若是相同的,則在每個(gè)對(duì)應(yīng)位子的兩個(gè)對(duì)象身上調(diào)用“isEqual”方法,這叫“深度等同性判定”。不過(guò)有時(shí)沒(méi)有必要這么做,比如說(shuō):我們假設(shè)CWGPerson類(lèi)的實(shí)例是根據(jù)數(shù)據(jù)庫(kù)中的數(shù)據(jù)創(chuàng)建出來(lái)的,那么其中就可能還有有個(gè)屬性叫做“主鍵”。在這樣的情況下,我們只要根據(jù)主鍵來(lái)判斷就行了。
容器中可變類(lèi)的等同性
在這里我們舉個(gè)例子就能很好的理解這點(diǎn):
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject: arrayA];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}
如果這時(shí),再向set中加入一個(gè)數(shù)組, 此數(shù)組與前面的數(shù)組一模一樣。那么:
NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[set addObject: arrayB];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}
此時(shí)set里仍然只有一個(gè)對(duì)象,因?yàn)閯偛乓尤氲哪莻€(gè)數(shù)組對(duì)象和set中也有的數(shù)組對(duì)象相等,所以set并沒(méi)有改變。但是如果我們添加的是一個(gè)不一樣的數(shù)組:
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject: arrayC];
NSLog(@"set = %@", set);
// Output: set = {((1), (1, 2))}
然而這時(shí),我們進(jìn)行如下操作:
[arrayC addObject:@2];
NSLog(@"set = %@", set);
// Output: set = {((1, 2), (1, 2))}
set中居然有2個(gè)相等的數(shù)組,根據(jù)set的語(yǔ)法規(guī)則,這時(shí)絕對(duì)不允許出現(xiàn)的。然而現(xiàn)在卻無(wú)法保證這一點(diǎn)了,因?yàn)槲覀冃薷牧藄et中已有的對(duì)象,若是拷貝此set,那就更可怕了:
NSSet *setB = [set copy];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}
復(fù)制之后又只剩下一個(gè)對(duì)象了,此set看上去好像是由一個(gè)空set開(kāi)始,通過(guò)逐個(gè)向其中添加新對(duì)象而創(chuàng)建出來(lái)的。這可能符合你的要求,也可能不符合,有的開(kāi)發(fā)者也許想要忽略set中的錯(cuò)誤,“找原樣”復(fù)制一個(gè)新的出來(lái),還有的開(kāi)發(fā)者則會(huì)認(rèn)為這樣做挺好的。其實(shí)這兩種拷貝算法都說(shuō)得通,于是就進(jìn)一步印證了剛才說(shuō)的那個(gè)問(wèn)題:如果把某個(gè)對(duì)象放入set之后又修改其內(nèi)容,那么后面的行為就很難預(yù)料。
舉這個(gè)例子是為了提醒大家,把某個(gè)對(duì)象放入collection之后改變其內(nèi)容將會(huì)造成什么后果。筆者并不是說(shuō)絕對(duì)不能這么做,而是要提醒你這樣做的隱患,用相印的代碼處理可能發(fā)生的問(wèn)題。
總結(jié):
- 若是檢測(cè)對(duì)象的等同性,請(qǐng)?zhí)峁癷sEqual:”和“hash”方法
- 相同的對(duì)象必須具有相同的哈希碼,但是兩個(gè)哈希碼相同的對(duì)象卻未必相同。
- 不要盲目的這個(gè)檢測(cè)每條屬性,二手應(yīng)該依照具體需求來(lái)制定檢測(cè)方案。
- 編寫(xiě)hash方法時(shí),應(yīng)該使用計(jì)算速度快而且哈希碼碰撞幾率低的算法。