web-dev-qa-db-ja.com

isEqual:およびhashをオーバーライドするためのベストプラクティス

Objective-CでisEqual:を適切にオーバーライドするにはどうすればよいですか? 「キャッチ」は、2つのオブジェクトが等しい場合(isEqual:メソッドによって決定される)、同じハッシュ値を持つ必要があるようです。

Cocoa Fundamentals GuideIntrospection セクションには、MyWidgetという名前のクラスに対して、次のようにコピーされたisEqual:をオーバーライドする方法の例があります。

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}

- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

ポインターの等価性、次にクラスの等価性をチェックし、最後にnameおよびdataプロパティのみをチェックするisEqualToWidget:を使用してオブジェクトを比較します。例が表示されないのは、hashをオーバーライドする方法です。

ageなど、等式に影響しない他のプロパティがあると仮定します。 hashnameのみがハッシュに影響するように、dataメソッドをオーバーライドすべきではありませんか?もしそうなら、どのようにそれをしますか? namedataのハッシュを追加するだけです?例えば:

- (NSUInteger)hash {
    NSUInteger hash = 0;
    hash += [[self name] hash];
    hash += [[self data] hash];
    return hash;
}

それで十分ですか?より良いテクニックはありますか? intなどのプリミティブがある場合はどうなりますか?それらをNSNumberに変換してハッシュを取得しますか?またはNSRectのような構造体ですか?

Brain fart:もともと|=と一緒に「ビット単位のOR」を記述しました。意味のある追加です。)

264
Dave Dribin

皮切りに

 NSUInteger prime = 31;
 NSUInteger result = 1;

次に、すべてのプリミティブに対して

 result = prime * result + var

64ビットの場合、シフトおよびxorも必要になる場合があります。

 result = prime * result + (int) (var ^ (var >>> 32));

オブジェクトの場合、nilには0を使用し、それ以外の場合はハッシュコードを使用します。

 result = prime * result + [var hash];

ブール値には、2つの異なる値を使用します

 result = prime * result + (var)?1231:1237;

説明と帰属

これはtcurdtの仕事ではなく、コメントはさらなる説明を求めていたので、帰属の編集は公正だと思います。

このアルゴリズムは、「Effective Java」という本で人気があり、 関連する章は現在オンラインでここにあります 。その本はアルゴリズムを普及させ、現在では多くのJavaアプリケーション(Eclipseを含む)のデフォルトになっています。ただし、Dan BernsteinまたはChris Torekにさまざまに起因するさらに古い実装から派生しました。その古いアルゴリズムはもともとUsenetに浮かび、特定の帰属は困難です。たとえば、元のソースを参照する このApacheコードの興味深い解説 (名前の検索)があります。

要するに、これは非常に古い、単純なハッシュアルゴリズムです。それは最もパフォーマンスの高いものではなく、「良い」アルゴリズムであることが数学的に証明されていません。しかし、それは単純であり、多くの人が長い間それを使用して良い結果を出しているので、多くの歴史的なサポートがあります。

110
tcurdt

私はObjective-Cを自分で拾っているので、その言語について具体的に話すことはできませんが、2つのインスタンスが「等しい」場合は同じハッシュを返す必要がある他の言語で使用します-それ以外の場合はすべてを使用しますハッシュテーブル(または任意の辞書タイプのコレクション)のキーとして使用しようとすると、ある種の問題が発生します。

一方、2つのインスタンスが等しくない場合、それらは同じハッシュを持つ場合と持たない場合があります。等しくない場合が最適です。これは、ハッシュテーブルでのO(1)検索とO(N)検索の違いです。すべてのハッシュが衝突した場合、テーブルの検索は良くないことがわかります。リストを検索するよりも。

ベストプラクティスの観点から、ハッシュは入力に対して値のランダムな分布を返す必要があります。これは、たとえば、doubleがあり、値の大部分が0から100の間でクラスター化する傾向がある場合、それらの値によって返されるハッシュが可能なハッシュ値の範囲全体に均等に分散されることを確認する必要があることを意味します。これにより、パフォーマンスが大幅に向上します。

いくつかのハッシュアルゴリズムがあり、その中にはいくつかのアルゴリズムが含まれています。パフォーマンスに大きな影響を与える可能性があるため、新しいハッシュアルゴリズムの作成を避けようとしています。そのため、既存のハッシュメソッドを使用し、例のようにビット単位の組み合わせを行うことが、これを回避する良い方法です。

81
Brian B.

重要なプロパティのハッシュ値に対する単純なXORは、99%の時間で十分です。

例えば:

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

解決策は http://nshipster.com/equality/ Mattt Thompsonによって発見されました(彼の投稿でもこの質問に言及しています!)

31
Yariv Nissim

このスレッドは、isEqual:メソッドとhashメソッドを1つのキャッチで実装するために必要なすべてを提供するのに非常に役立ちました。 isEqual:のオブジェクトインスタンス変数をテストする場合、サンプルコードは次を使用します。

if (![(id)[self name] isEqual:[aWidget name]])
    return NO;

これは繰り返し失敗しました(つまり.、 戻ってきた 番号)なしでエラーが発生した場合、 知っていた 私の単体テストではオブジェクトは同一でした。その理由は、NSStringインスタンス変数の1つが なし したがって、上記のステートメントは次のとおりです。

if (![nil isEqual: nil])
    return NO;

それ以来 なし あらゆる方法に対応します。これは完全に合法ですが、

[nil isEqual: nil]

返却値 なし、 番号、したがって、オブジェクトとテストされているオブジェクトの両方に なし それらは等しくないと見なされるオブジェクト(つまり.isEqual:は戻ります 番号)。

この簡単な修正は、ifステートメントを次のように変更することでした。

if ([self name] != [aWidget name] && ![(id)[self name] isEqual:[aWidget name]])
    return NO;

このように、アドレスが同じ場合、両方であるかどうかに関係なくメソッド呼び出しをスキップします なし または両方が同じオブジェクトを指しているが、どちらかが異なる場合 なし または、異なるオブジェクトを指している場合、コンパレータが適切に呼び出されます。

これにより、誰かが数分間頭を悩ませることがなくなることを願っています。

27
LavaSlider

ハッシュ関数は、他のオブジェクトのハッシュ値と衝突したり一致したりする可能性が低い準一意の値を作成する必要があります。

以下に完全なハッシュ関数を示します。これは、クラスのインスタンス変数に適合させることができます。 64/32ビットアプリケーションとの互換性のために、intではなくNSUIntegerを使用します。

異なるオブジェクトの結果が0になると、ハッシュが衝突する危険があります。ハッシュが衝突すると、ハッシュ関数に依存するコレクションクラスの一部を操作するときに、予期しないプログラムの動作が発生する可能性があります。使用する前にハッシュ関数をテストしてください。

-(NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;
    NSUInteger yesPrime = 1231;
    NSUInteger noPrime = 1237;

    // Add any object that already has a hash function (NSString)
    result = prime * result + [self.myObject hash];

    // Add primitive variables (int)
    result = prime * result + self.primitiveVariable; 

    // Boolean values (BOOL)
    result = prime * result + self.isSelected?yesPrime:noPrime;

    return result;
}
20
Paul Solt

これは私を大いに助けてくれました!あなたが探している答えかもしれません。 平等とハッシュの実装

18
Steve M

簡単ですが非効率な方法は、すべてのインスタンスに対して同じ-hash値を返すことです。そうでなければ、はい、平等に影響するオブジェクトのみに基づいてハッシュを実装する必要があります。 -isEqual:で緩い比較を使用する場合、これは注意が必要です(たとえば、大文字と小文字を区別しない文字列比較)。 intの場合、NSNumbersと比較するのでなければ、通常はint自体を使用できます。

| =は使用しないでください。ただし、飽和します。代わりに^ =を使用してください。

ランダムな楽しい事実:[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]]、ただし[[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]。 (rdar:// 4538282、2006年5月5日以降オープン)

13
Jens Ayton

isEqualがtrueの場合にのみ等しいハッシュを提供する必要があることに注意してください。 isEqualがfalseの場合、ハッシュは不一致である必要はありませんが、不一致であると思われます。したがって:

ハッシュをシンプルに保ちます。最も特徴的なメンバー(または少数のメンバー)変数を選択します。

たとえば、CLPlacemarkの場合、名前だけで十分です。はい、まったく同じ名前の2つまたは3つの異なるCLPlacemarkがありますが、それらはまれです。そのハッシュを使用します。

@interface CLPlacemark (equal)
- (BOOL)isEqual:(CLPlacemark*)other;
@end

@implementation CLPlacemark (equal)

...

-(NSUInteger) hash
{
    return self.name.hash;
}


@end

市、国などを指定する必要はありません。名前で十分です。おそらく名前とCLLocation。

ハッシュは均等に分散する必要があります。したがって、キャレット^(xor記号)を使用して複数のメンバー変数を組み合わせることができます

のようなものです

hash = self.member1.hash ^ self.member2.hash ^ self.member3.hash

そうすれば、ハッシュは均等に分散されます。

Hash must be O(1), and not O(n)

では、配列で何をすべきか?

繰り返しますが、簡単です。配列のすべてのメンバーをハッシュする必要はありません。最初の要素、最後の要素、カウント、おそらくいくつかの中間要素をハッシュするのに十分であり、それだけです。

10
user4951

ちょっと待ってください。確かにこれを行う最も簡単な方法は、最初に- (NSString )descriptionをオーバーライドし、オブジェクトの状態の文字列表現を提供することです(この文字列でオブジェクトの状態全体を表す必要があります)。

次に、hashの次の実装を提供します。

- (NSUInteger)hash {
    return [[self description] hash];
}

これは、「2つの文字列オブジェクトが等しい場合(isEqualToString:メソッドによって決定される場合)、同じハッシュ値を持つ必要がある」という原則に基づいています。

ソース: NSStringクラス参照

7
Jonathan Ellis

イコールおよびハッシュコントラクトは、Javaの世界(@mipardiの回答を参照)で十分に指定され、徹底的に調査されていますが、Objective-Cにも同じ考慮事項が適用されます。

EclipseはJavaでこれらのメソッドを生成する信頼できる仕事をしているので、Objective-Cに手で移植されたEclipseの例を次に示します。

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if ([self class] != [object class])
        return false;
    MyWidget *other = (MyWidget *)object;
    if (_name == nil) {
        if (other->_name != nil)
            return false;
    }
    else if (![_name isEqual:other->_name])
        return false;
    if (_data == nil) {
        if (other->_data != nil)
            return false;
    }
    else if (![_data isEqual:other->_data])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = 1;
    result = prime * result + [_name hash];
    result = prime * result + [_data hash];
    return result;
}

そして、プロパティYourWidgetを追加するサブクラスserialNoの場合:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if (![super isEqual:object])
        return false;
    if ([self class] != [object class])
        return false;
    YourWidget *other = (YourWidget *)object;
    if (_serialNo == nil) {
        if (other->_serialNo != nil)
            return false;
    }
    else if (![_serialNo isEqual:other->_serialNo])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = [super hash];
    result = prime * result + [_serialNo hash];
    return result;
}

この実装により、AppleのサンプルisEqual:のサブクラス化の落とし穴が回避されます。

  • Appleのクラステストother isKindOfClass:[self class]は、MyWidgetの2つの異なるサブクラスに対して非対称です。等式は対称である必要があります:b = aの場合に限り、a = bです。これは、テストをother isKindOfClass:[MyWidget class]に変更することで簡単に修正でき、すべてのMyWidgetサブクラスは相互に比較可能になります。
  • isKindOfClass:サブクラステストを使用すると、サブクラスが洗練された同等性テストでisEqual:をオーバーライドできなくなります。これは、等式が推移的である必要があるためです。a= bおよびa = cの場合、b = cです。 MyWidgetインスタンスが2つのYourWidgetインスタンスと等しい場合、それらのYourWidgetインスタンスは、serialNoが異なっていても、互いに等しく比較する必要があります。

2番目の問題は、オブジェクトがまったく同じクラスに属する場合にのみ等しいと見なすことで修正できます。したがって、ここで[self class] != [object class]テストを行います。典型的なアプリケーションクラスの場合、これが最良のアプローチのようです。

ただし、isKindOfClass:テストが望ましい場合があります。これは、アプリケーションクラスよりも典型的なフレームワーククラスです。たとえば、NSStringは、NSString/NSStringの区別や、NSMutableStringクラスクラスター内のプライベートクラスに関係なく、同じ基になる文字シーケンスを持つ他のNSStringと比較する必要があります。

そのような場合、isEqual:には明確に定義され、十分に文書化された動作が必要であり、サブクラスがこれをオーバーライドできないことを明確にする必要があります。 Javaでは、equalsメソッドとhashcodeメソッドにfinalのフラグを立てることにより、「オーバーライドなし」の制限を実施できますが、Objective-Cには同等のものがありません。

5
jedwidz

このページ は、equals-およびhash-typeメソッドのオーバーライドに役立つガイドであることがわかりました。ハッシュコードを計算するための適切なアルゴリズムが含まれています。このページはJava向けですが、Objective-C/Cocoaに簡単に適合させることができます。

5
mipadi

これはあなたの質問に直接答えるものではありませんが、ハッシュを生成する前にMurmurHashを使用したことがあります。 murmurhash

理由を説明する必要があると思います:murmurhashは血まみれです...

5
schwa

私もObjective Cの初心者ですが、Objective Cのアイデンティティと平等に関する優れた記事を見つけました here 。私の読書から、デフォルトのハッシュ関数(一意のIDを提供する必要があります)を保持し、isEqualメソッドを実装してデータ値を比較できるように見えるかもしれません。

4
ceperry

Quinnは、つぶやきハッシュへの参照がここでは役に立たないのは間違っています。クインは、ハッシュの背後にある理論を理解したいのは正しいことです。雑音は、その理論の多くを実装に蒸留します。この実装をこの特定のアプリケーションに適用する方法を理解することは、検討する価値があります。

ここにいくつかの重要なポイントがあります:

Tcurdtの関数例は、「31」が素数であるため、適切な乗数であることを示唆しています。素数であることは必要十分条件であることを示す必要があります。実際、31(および7)は31 == -1%32であるため、特に素数ではありません。約半分のビットが設定され、半分のビットがクリアな奇数の乗数の方が優れている可能性があります。 (雑音ハッシュ乗算定数にはその特性があります。)

このタイプのハッシュ関数は、乗算後、結果の値がシフトとxorを介して調整された場合に強力になる可能性があります。乗算は、レジスタの上端で多くのビットの相互作用の結果を生成し、レジスタの下端で相互作用の低い結果を生成する傾向があります。シフトとxorは、レジスタの下端での相互作用を増加させます。

初期結果を、約半分のビットがゼロで約半分のビットが1である値に設定することも有用です。

要素が結合される順序に注意することが役立つ場合があります。まず、ブール値や値が強く分布していない他の要素を最初に処理する必要があります。

計算の最後に余分なビットスクランブルステージをいくつか追加すると便利です。

つぶやきハッシュがこのアプリケーションで実際に高速であるかどうかは、未解決の問題です。つぶやきハッシュは、各入力Wordのビットを事前に混合します。複数の入力ワードを並行して処理できるため、複数の問題をパイプライン処理したCPUに役立ちます。

3
Ces

プロパティ名を取得する の@tcurdtの回答と@ oscar-gomezの回答を組み合わせることで、isEqualとhashの両方の簡単なドロップインソリューションを作成できます。

NSArray *PropertyNamesFromObject(id object)
{
    unsigned int propertyCount = 0;
    objc_property_t * properties = class_copyPropertyList([object class], &propertyCount);
    NSMutableArray *propertyNames = [NSMutableArray arrayWithCapacity:propertyCount];

    for (unsigned int i = 0; i < propertyCount; ++i) {
        objc_property_t property = properties[i];
        const char * name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        [propertyNames addObject:propertyName];
    }
    free(properties);
    return propertyNames;
}

BOOL IsEqualObjects(id object1, id object2)
{
    if (object1 == object2)
        return YES;
    if (!object1 || ![object2 isKindOfClass:[object1 class]])
        return NO;

    NSArray *propertyNames = PropertyNamesFromObject(object1);
    for (NSString *propertyName in propertyNames) {
        if (([object1 valueForKey:propertyName] != [object2 valueForKey:propertyName])
            && (![[object1 valueForKey:propertyName] isEqual:[object2 valueForKey:propertyName]])) return NO;
    }

    return YES;
}

NSUInteger MagicHash(id object)
{
    NSUInteger prime = 31;
    NSUInteger result = 1;

    NSArray *propertyNames = PropertyNamesFromObject(object);

    for (NSString *propertyName in propertyNames) {
        id value = [object valueForKey:propertyName];
        result = prime * result + [value hash];
    }

    return result;
}

これで、カスタムクラスにisEqual:hashを簡単に実装できます。

- (NSUInteger)hash
{
    return MagicHash(self);
}

- (BOOL)isEqual:(id)other
{
    return IsEqualObjects(self, other);
}
3
johnboiles

作成後に変更可能なオブジェクトを作成している場合、オブジェクトがコレクションに挿入された場合、ハッシュ値はnot changeでなければならないことに注意してください。実際には、これはハッシュ値が最初のオブジェクト作成の時点から固定されなければならないことを意味します。詳細については、 NSObjectプロトコルの-hashメソッドに関するAppleのドキュメント を参照してください。

ハッシュ値を使用してコレクション内のオブジェクトの位置を判断するコレクションに可変オブジェクトが追加された場合、オブジェクトのハッシュメソッドによって返される値は、オブジェクトがコレクション内にある間は変更できません。したがって、ハッシュメソッドは、オブジェクトの内部状態情報に依存してはなりません。または、オブジェクトがコレクション内にある間、オブジェクトの内部状態情報が変更されないようにする必要があります。したがって、たとえば、変更可能なディクショナリはハッシュテーブルに入れることができますが、そこにある間は変更しないでください。 (特定のオブジェクトがコレクション内にあるかどうかを知るのは難しいことに注意してください。)

ハッシュルックアップの効率が大幅に低下する可能性があるため、これは完全に手っ取り早いように思えますが、注意を怠ってドキュメントに書かれていることに従う方が良いと思います。

2
user10345

ここで完全なcompleteを鳴らすリスクがある場合は申し訳ありませんが... ...「ベストプラクティス」に従うために、ターゲットオブジェクトが所有するすべてのデータを考慮しないequalsメソッドを絶対に指定しないでくださいequalsを実装するときは、データはオブジェクトに集約されますが、オブジェクトの関連オブジェクトに対しては考慮される必要があります。比較で「年齢」などを考慮したくない場合は、コンパレータを記述し、それを使用してisEqual:の代わりに比較を実行する必要があります。

等値比較を任意に実行するisEqual:メソッドを定義した場合、等値解釈で「ねじれ」を忘れると、このメソッドが別の開発者や自分自身によって悪用されるリスクが生じます。

エルゴ、これはハッシュに関する素晴らしいQ&Aですが、通常はハッシュメソッドを再定義する必要はありません。代わりにアドホックコンパレータを定義する必要があります。

1