iOS 判等

Junyiii發表於2017-06-27

兩個概念 相等性與本體性

來看段程式碼,猜猜結果如何

    NSArray *a = @[@1];
    NSArray *b = a;
    NSArray *c = @[@1];

    if (a == b) {
        NSLog(@"a == b");
    }

    if (a == c) {
        NSLog(@"a == c");
    }

    if ([a isEqualToArray:c]){
        NSLog(@"a c 具有本體性");
    }複製程式碼

列印結果

 a == b
 a c 具有本體性複製程式碼

這段程式碼,首先我們要搞明白記憶體到底是怎麼分配的。
a,b 都是指標。
[NSObject new]則在堆上分配了一塊記憶體空間
該記憶體空間的首地址被儲存在 a 中。
將a的值 賦值給 b.

如圖:

相等性:當兩個物體有一系列相同的可觀測的屬性時,兩個物體可能是互相相等或者等價的。但這兩個物體仍然是不同的,他們各自有自己的本體。
本體性:在程式設計中,一個物件的本體和它的記憶體地址是相互關聯的。關聯的記憶體地址相同則具有本體性。

三種判等方式
你可能會對以下三種判等方式的實現有疑惑

  • isEqual:
  • ==
  • isEqualToArray:

Objective-C 對於判等的實現

接觸了 相等性 和 本體性之後, 來看看 Objective-C 對於判等是如何實現的.

isEqual:

NSObject 使用 isEqual: 這個方法來測試和其他物件的相等性。在它的基類實現中,相等性檢查本質上就是對本體性的檢查。兩個 NSObject 如果指向了同一個記憶體地址,那它們就被認為是相同的。

子類如果需要重寫isEqual:

  • 實現一個新的 isEqualTo__ClassName__ 方法,進行實際意義上的值的比較。
  • 重寫 isEqual: 方法進行類和物件的本體性檢查,如果失敗則回退到上面提到的值比較方法。
  • 重寫 hash 方法

==

是對本體性的比較,比較指標。

isEqualToArray:

是對相等性進行檢查。
NSArray 作為NSObject的子類,實現了 isEqualToArray:的方法。
isEqualToArray:對 相等性進行檢查。

相等性檢查的實現

在程式設計中,是如何實現相等性檢查的呢?
藉助Hash來實現。
關於Hash的知識,Wiki_Hash

如果兩個物件相等,它們的 hash 值也一定是相等的(反之不然)
對於自定義的子類來說,判斷相等性需要實現hash方法

Object comparison

給NSHipster糾個錯:
Swift-Comparison-Protocols
中提到的:
Objective-C 中對於物件的比較,== 操作符的運算結果就是來自 isEqual: 方法的結果:
根據文件的意思是有問題的
Each class determines equality for its instances by implementing class-specific comparison logic. The root class, NSObject, measures equality by simple pointer comparison; at the other extreme, the determinants of equality for a custom class might be class membership plus all encapsulated values.

Example:
來自NSHipster

@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];
}複製程式碼

Swift 中的判等

(Swift 並不是我的主力語言 簡單介紹下)

Swift === 判讀本體性
Swift == 判斷相等性

Objective-C == 判斷兩個物件是否是同一個引用
你可以通過重寫isEqual:方法來改變==的實現。
如果沒有重寫,預設呼叫的NSObject的實現。
或者更進一步去實現類似 -isEqualToString: 這樣的 -isEqualToClass: 的帶有型別資訊的方法來進行內容判等。

在 Swift 中情況大不一樣,Swift 裡的 == 是一個操作符的宣告,在 Equatable 裡宣告瞭這個操作符的介面方法:

protocol Equatable {
    func ==(lhs: Self, rhs: Self) -> Bool
}複製程式碼

實現這個介面的型別需要定義適合自己型別的==操作符,如果我們認為兩個輸入有相等關係的話,就應該返回true.

Swift 中於 OC == 相對應地是 === 操作符

一些特殊情況

NSString 與 字串駐留

    NSString *s1 = @"123";
    NSString *s2 = @"123";
    NSString *s3 = [NSString stringWithFormat:@"%@",@"123"];

    if (s1 == s2) {
        NSLog(@"s1 == s2");
    }

    if (s1 == s3) {
        NSLog(@"s1 == s3");
    }

    if ([s1 isEqualToString:s3]) {
        NSLog(@"s1 isEqualToString:s3");
    }複製程式碼

列印:

s1 == s2
s1 isEqualToString:s3複製程式碼

由於使用了字串駐留技術
在使用字串字面量來初始化NSString時,字串駐留技術把一個不可變字串物件的值拷貝給各個不同的指標。s1 和 s2都指向同樣一個駐留字串值。

這就導致了 s1 s2 指向相同的記憶體地址。
s1 s3 指向的記憶體地址不同,但是值是相同的。

類族對於判等的影響

首先什麼是類族?簡單介紹下:
Cocoa框架中的類很多都是類族,隱藏了背後的實現。NSNumber,NSArray,還有眾多collection類都是類族。

+ (UIButton *)buttonWithType:(UIButtonType)type;複製程式碼

該方法返回的物件,其型別取決於傳入的按鈕型別。(這就隱藏了實現細節,使用者只需根據需求傳入不同的UIButtonType)

如果我們用物件的型別進行判等自然會出現問題:

id maybeAnArray = /* ...*/;
if ([maybeAnArray class] == [NSArray class]) {
    //will never be hit 
}複製程式碼

如果不知道類族,你會迷惑這段程式碼的問題到底出在何處。
我們可以改用 isKindOfClass: 方法來進行判斷。
isKindOfClass來確定一個物件是否是一個類的成員,或者是派生自該類的成員
由於類簇中的類 是 抽象類的子類,所以 isKindOfClass: 來判等是可行的。

相關文章