NSSet に入れる自作クラスは isEqual: と hash をオーバーライドする


NSSet はあまり使わなくて、いざ使おうとするときに「あれ、どうだったっけ？」ってなるのでメモしておきます。

## はじめに

[NSSet](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSSet_Class/index.html) はコレクションのひとつで、要素が重複しないようにオブジェクトを管理してくれます。

重複のチェックがどのように行われているのがが気になるので、[NSMutableSet](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableSet_Class/index.html#//apple_ref/occ/cl/NSMutableSet) を使って動作検証してみます。

検証環境は、Xcode 6.1.1、iOS SDK 8.1 です。

## 検証コード

以下の自作クラスを使用します。とりあえず、[こちら](https://developer.apple.com/jp/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_7.html)のドキュメントを参考に　`isEqual:` と `hash` をオーバーライドしてログを出力するようにしています。

```objc:MyClass.h
#import <Foundation/Foundation.h>

@interface MyClass : NSObject

@property (nonatomic) NSString *itemName;
@property (nonatomic) NSInteger price;

@end
```

```objc:MyClass.m
#import "MyClass.h"

@implementation MyClass

- (BOOL)isEqual:(id)object {
    __typeof(self) other = object;
    NSLog(@"%s (%@, %@) (%@, %@)", __PRETTY_FUNCTION__, self.itemName, @(self.price), other.itemName, @(other.price));
    return [self.itemName isEqualToString:other.itemName] && self.price == other.price;
}

- (NSUInteger)hash {
    NSString *str = [NSString stringWithFormat:@"%@_%@", self.itemName, @(self.price)];
    NSLog(@"%s (%@, %@, %@)", __PRETTY_FUNCTION__, self.itemName, @(self.price), @(str.hash));
    return str.hash;
}

@end
```

自作クラスのオブジェクトを NSMutableSet へ追加するコードはこんな感じです。`itemName` と `price` が全く同じオブジェクトを2個ずつ、合計10個追加しています。

```objc
NSMutableSet *set = [NSMutableSet new];
for (NSInteger i = 0; i < 10; i++) {
    MyClass *obj = [MyClass new];
    obj.itemName = @"Mac mini";
    obj.price = i % 5;
    NSLog(@"ADD: %@, %@", obj.itemName, @(obj.price));
    [set addObject:obj];
}
NSLog(@"count: %@", @(set.count));
```

## 検証結果

NSLog の出力は以下のようになりました。`isEqual:` と `hash` の両方が使用されるようです。

```
2014-12-10 01:55:30.000 SetSample[1534:46412] ADD: Mac mini, 0
2014-12-10 01:55:30.002 SetSample[1534:46412] -[MyClass hash] (Mac mini, 0, 8380095419414328695)
2014-12-10 01:55:30.002 SetSample[1534:46412] -[MyClass hash] (Mac mini, 0, 8380095419414328695)
2014-12-10 01:55:30.003 SetSample[1534:46412] ADD: Mac mini, 1
2014-12-10 01:55:30.003 SetSample[1534:46412] -[MyClass hash] (Mac mini, 1, 8380095419414329720)
2014-12-10 01:55:30.003 SetSample[1534:46412] ADD: Mac mini, 2
2014-12-10 01:55:30.003 SetSample[1534:46412] -[MyClass hash] (Mac mini, 2, 8380095419414330745)
2014-12-10 01:55:30.004 SetSample[1534:46412] ADD: Mac mini, 3
2014-12-10 01:55:30.004 SetSample[1534:46412] -[MyClass hash] (Mac mini, 3, 8380095419414331770)
2014-12-10 01:55:30.004 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 0) (Mac mini, 3)
2014-12-10 01:55:30.004 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 2) (Mac mini, 3)
2014-12-10 01:55:30.005 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 1) (Mac mini, 3)
2014-12-10 01:55:30.005 SetSample[1534:46412] -[MyClass hash] (Mac mini, 2, 8380095419414330745)
2014-12-10 01:55:30.005 SetSample[1534:46412] -[MyClass hash] (Mac mini, 1, 8380095419414329720)
2014-12-10 01:55:30.005 SetSample[1534:46412] -[MyClass hash] (Mac mini, 0, 8380095419414328695)
2014-12-10 01:55:30.026 SetSample[1534:46412] -[MyClass hash] (Mac mini, 3, 8380095419414331770)
2014-12-10 01:55:30.027 SetSample[1534:46412] ADD: Mac mini, 4
2014-12-10 01:55:30.027 SetSample[1534:46412] -[MyClass hash] (Mac mini, 4, 8380095419414332795)
2014-12-10 01:55:30.027 SetSample[1534:46412] ADD: Mac mini, 0
2014-12-10 01:55:30.028 SetSample[1534:46412] -[MyClass hash] (Mac mini, 0, 8380095419414328695)
2014-12-10 01:55:30.028 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 0) (Mac mini, 0)
2014-12-10 01:55:30.028 SetSample[1534:46412] ADD: Mac mini, 1
2014-12-10 01:55:30.029 SetSample[1534:46412] -[MyClass hash] (Mac mini, 1, 8380095419414329720)
2014-12-10 01:55:30.029 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 1) (Mac mini, 1)
2014-12-10 01:55:30.029 SetSample[1534:46412] ADD: Mac mini, 2
2014-12-10 01:55:30.030 SetSample[1534:46412] -[MyClass hash] (Mac mini, 2, 8380095419414330745)
2014-12-10 01:55:30.030 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 2) (Mac mini, 2)
2014-12-10 01:55:30.031 SetSample[1534:46412] ADD: Mac mini, 3
2014-12-10 01:55:30.031 SetSample[1534:46412] -[MyClass hash] (Mac mini, 3, 8380095419414331770)
2014-12-10 01:55:30.031 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 3) (Mac mini, 3)
2014-12-10 01:55:30.032 SetSample[1534:46412] ADD: Mac mini, 4
2014-12-10 01:55:30.032 SetSample[1534:46412] -[MyClass hash] (Mac mini, 4, 8380095419414332795)
2014-12-10 01:55:30.032 SetSample[1534:46412] -[MyClass isEqual:] (Mac mini, 4) (Mac mini, 4)
2014-12-10 01:55:30.032 SetSample[1534:46412] count: 5
```

## さいごに

**2014/12/10 コメントを頂きましたので、記事を修正しました。**

`isEqual:` と `hash` の両方が使われるので、`isEqual:` が `YES` を返す場合は、`hash` の値が一致するように辻褄を合わせておく必要があります。

NSLog の内容を見ると、まず一致性を素早く確認するために `hash` が比較され、`hash` が一致した場合はさらに `isEqual:` で比較されるようです。（`hash` は衝突があり得るので、一致しても同じオブジェクトではない可能性があります）

ちなみに、MyClass.m の `hash` をオーバーライドしない状態で実行すると、ログの最後の `count` の値が実行するたびに変わってしまって、期待する動作をしませんでした。

`isEqual:` をオーバーライドしない状態で実行すると `count` の値は常に10で、こちらも期待する動作ではありませんでした。