iOS判斷對象相等 重寫isEqual、isEqualToClass、hash

相等的概念是探究哲學和數學的核心,而且對道德、公正和公共政策的問題有着深遠的影響。
從一個經驗主義者的角度來看,兩個物體不能依據一些觀測標準中分辨出來,它們就是相等的。在人文方面,平等主義者認爲相等意味着要保持每一個人的社會、經濟、政治和他們住地的司法系統都一致。
對程序員來講,協調好邏輯和感官能力來理解咱們塑造的'相同'的語義是一項任務。'相同的問題'(的探討)太微妙,同時有太容易被忽視。對語義沒有充分的理解就直接去實現它,可能會致使不必的工做和不正確的結果。所以對數學和邏輯系統的深入理解與按既定計劃實現一樣必要。html

雖然全部的技術博文都是有誘惑你來讀它的標題和代碼,但請花幾分鐘時間來閱讀和理解這些文字。逐字地複製看似有用的代碼而不知道爲何這樣寫頗有可能致使一些錯誤。相等性是個重要話題之一,但它仍包含了許多混亂的概念,尤爲是在Objective-C中。程序員

Equality & Identity

首先,弄清楚equality和identity的區別很重要。
若是兩個物體具備相同的觀測屬性,它們是能夠相互等同的。可是,這兩個對象仍然能夠分辨出差別,它們各自的identity。在程序中,一個對象的identity是和它的內存地址關聯的。編程

NSObject對象測試和另外一個對象是否相同使用isEqual:方法,在它的基本實現裏性等性檢查本質上是對identity的檢查,若是兩個對象指向了相同的內存地址,它們被認爲是相等的。數組

@implementation NSObject (Approximate)
- (BOOL)isEqual:(id)object {
  return self == object;
}
@end

對於內置的類,像NSArray, NSDictionary, 和 NSString,進行了一個深層的相等性比較,來測試在集合中的每一個元素是否相等,這是一個應該也確實很是有用的作法。
NSObject 的子類要實現它們各自的isEqual:方法時,應該作到如下幾點:數據結構

1.實現一個isEqualTo__ClassName__:方法來執行有意義的值比較.
2.重寫isEqual:方法 來做類型和對象identity檢查, 回調上述的值比較方法.
3.重寫 hash, 這個會在下一部分解釋.ide

這裏有一個NSArray實現這個的大概的思路(這個例子忽略了類簇, 實際實現會更具體複雜):函數

@implementation NSArray (Approximate)
- (BOOL)isEqualToArray:(NSArray *)array {
  if (!array || [self count] != [array count]) {
    return NO;
  }

  for (NSUInteger idx = 0; idx < [array count]; idx++) {
      if (![self[idx] isEqual:array[idx]]) {
          return NO;
      }
  }

  return YES;
}

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[NSArray class]]) {
    return NO;
  }

  return [self isEqualToArray:(NSArray *)object];
}
@end

下面的在Foudation中NSObject的子類已經自定義了判等實現,用了相關的方法:測試

NSAttributedString -isEqualToAttributedString:
NSData -isEqualToData:
NSDate -isEqualToDate:
NSDictionary -isEqualToDictionary:
NSHashTable -isEqualToHashTable:
NSIndexSet -isEqualToIndexSet:
NSNumber -isEqualToNumber:
NSOrderedSet -isEqualToOrderedSet:
NSSet -isEqualToSet:
NSString -isEqualToString:
NSTimeZone -isEqualToTimeZone:
NSValue -isEqualToValue:

當比較任何這些類的兩個實例時,推薦使用它們各自的高級別的method而不是isEqual:
然而,咱們的理論實現尚未完成,如今,讓咱們把注意力轉向hash(一段插曲:先清理一下NSString的問題)。優化

NSString判等的奇怪案例

一個有趣的插曲,看一下這個代碼:翻譯

NSString *a = @"Hello";
NSString *b = @"Hello";
BOOL wtf = (a == b); // YES

鄭重地聲明一下正確的比較兩個NSString對象相等的方法是使用-isEqualToString:方法,不管如何也不能經過==操做符來比較兩個NSString。
那麼這裏是怎麼回事呢?爲何 NSArray或者NSDictionary字面量相同不會這樣,而它(NSString)會這樣呢。

這都是一種被稱爲字符串駐留的優化技術作的,由於這種優化不一樣的值能夠對一份不可變的字符串值的備份進行拷貝。NSString類型的a指針和b指針對駐留字符串 @"Hello"進行了相同的拷貝。注意這個優化僅僅對靜態聲明的不可變字符串有效。

更有趣的是,OC的selector的名字也會被當作駐留字符串存儲在一個共用的字符串pool中。

Hashing

最平常的面向對象編程來講,對象判等最主要的用法在於決定集合成員。爲了讓這一步更快一些,自定義判等實現的類應該也實現hash:

1.對象相等是相互的([a isEqual:b] ⇒ [b isEqual:a])
2.若是對象相等,它們的hash值必須相等([a isEqual:b] ⇒ [a hash] == [b hash])
可是,反過來不必定成立:若是它們的hash值相等,兩個對象不必定相等。([a hash] == [b hash] ¬⇒ [a isEqual:b])

如今快速翻看一下《計算機科學》101:

hash表式編程中的基本的數據結構,它可使NSSet & NSDictionary 快速地(O(1))查找它的元素。
咱們也能夠經過對比着數組很好地理解hash表:

Arrays按照有序的索引存儲元素,所以一個大小爲n的數組會把元素放在索引1,2直到n-1.爲了肯定數組中的一個元素存在了哪裏,不得不一個個檢查每一個位置(除非數組碰巧已經排序好,但這是另外一回事)。

Hash表使用了略微不一樣的方法。而不是按順序存儲元素,hash表在內存中分配了n個位置,同時用一個函數來計算在這個範圍內計算一個位置。一個hash函數是肯定性的,同時一個好的hash函數使用一個相對均勻的散列來生成值,並且不會有太多的計算過程。當兩個不一樣的對象計算出相同的hash值時,會產生hash衝突。當衝突發生時,hash表會尋找衝突點同時把新加的對象放到第一個可用的位置。當hash表變得愈來愈擁擠,衝突的可能性會增長,這會致使花費更多的時間來尋找空間(這就是爲何均勻散列的hash函數不菲的緣由。)

一個關於實現hash函數的錯誤共識來自於隨之發生的斷言,這個錯誤的共識認爲hash值必須是不一樣的。這個錯誤共識會致使沒必要要地複雜實現,包括從Java textbooks複製過來的質數的神奇咒語。實際上,一個簡單的對關鍵屬性hash值的XOR(異或運算)對於99%的狀況來講已經夠用了。

技巧就是思考對象中的哪一個值是關鍵的。
對NSDate來講,對參照日期的時間間隔已經夠用了:

@implementation NSDate (Approximate)
- (NSUInteger)hash {
  return (NSUInteger)abs([self timeIntervalSinceReferenceDate]);
}

對UIColor來講,移位以後的RGB值是很是方便計算的

@implementation UIColor (Approximate)
- (NSUInteger)hash {
  CGFloat red, green, blue;
  [self getRed:&red green:&green blue:&blue alpha:nil];
  return ((NSUInteger)(red * 255) << 16) + ((NSUInteger)(green * 255) << 8) + (NSUInteger)(blue * 255);
}
@end

在子類中實現 -isEqual: 和 hash

綜合在一塊兒,這裏有一個如何在子類重寫默認的判等實現的例子:

@interface Person
@property NSString *name;
@property NSDate *birthday;

- (BOOL)isEqualToPerson:(Person *)person;
@end

@implementation Person

- (BOOL)isEqualToPerson:(Person *)person {
  if (!person) {
    return NO;
  }

  BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
  BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

#pragma mark - NSObject

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[Person class]]) {
    return NO;
  }

  return [self isEqualToPerson:(Person *)object];
}

- (NSUInteger)hash {
  return [self.name hash] ^ [self.birthday hash];
}

@end

若是想知足好奇心或者出於學究式的研究,看一下這個 Mike Ash的文章 ,解釋了經過移位和翻轉組合值如何改善了可能產生重疊(衝突)的hash.

本文徹底翻譯自: http://nshipster.com/equality/

相關文章
相關標籤/搜索