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