理解"對象等同性"概念

根據(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)該使用計算速度快而且哈希碼碰撞幾率低的算法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容