web-dev-qa-db-ja.com

-hash / -isEqual:/ -isEqualTo ...の実装:Objective-Cコレクション用

注:次のSO質問は関連していますが、それらもリンクされたリソースも私の質問に完全に答えていないようです、特に、オブジェクトのコレクションの同等性テストの実装に関連して。


バックグラウンド

NSObjectは、 -hash(NSUInteger)selfなどのインスタンスのアドレスを返す)および -()のdefault実装を提供します。 -isEqual: (レシーバーのアドレスとパラメーターが同一でない限り、NOを返します)。これらのメソッドは必要に応じてオーバーライドされるように設計されていますが、ドキュメントでは、両方を提供するか、どちらも提供しない必要があることが明確になっています。さらに、-isEqual:が2つのオブジェクトに対してYESを返す場合、それらのオブジェクトに対する-hashの結果must同じであること。そうでない場合、同じであるはずのオブジェクト(-compare:NSOrderedSameを返す2つの文字列インスタンスなど)がCocoaコレクションに追加されるか、直接比較されると、問題が発生する可能性があります。

環境

私は CHDataStructures.framework 、Objective-Cデータ構造のオープンソースライブラリを開発しています。私はいくつかのコレクションを実装し、現在それらの機能を改良および強化しています。追加したい機能の1つは、コレクションが他のコレクションと等しいかどうかを比較する機能です。

これらの比較では、メモリアドレスのみを比較するのではなく、2つのコレクションに存在するオブジェクトを考慮する必要があります(該当する場合は順序付けを含む)。このアプローチはCocoaでかなり前例があり、通常、次のような別の方法を使用します。

カスタムコレクションを同等性のテストに対して堅牢にしたいので、他のコレクションに安全に(そして予測どおりに)追加し、他のコレクション(NSSetなど)が2つのコレクションが同等/同等/重複しているかどうかを判断できるようにします。

問題

-isEqualTo...:メソッドはそれ自体でうまく機能しますが、これらのメソッドを定義するクラスは通常、パラメーターがレシーバーと同じクラス(またはおそらくサブクラス)である場合、-isEqual:をオーバーライドして[self isEqualTo...:]を呼び出します。 、または[super isEqual:]それ以外の場合。つまり、クラスは-hashも定義して、同じ内容の異なるインスタンスに対して同じ値を返すようにする必要があります。

さらに、Appleの-hashに関するドキュメントでは、次のように規定されています:(私の強調)

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

編集:これが必要な理由を明確に理解し、推論に完全に同意します—追加のコンテキストを提供するためにここで言及しました。簡潔にするために、なぜそうなるのかというトピックを避けました。

私のコレクションはすべて変更可能であり、ハッシュはコンテンツの少なくともsomeを考慮する必要があるため、ここでの唯一のオプションは、格納されているコレクションを変更することをプログラミングエラーと見なすことです。別のコレクションで。 (私のコレクションはすべて NSCopying を採用しているので、NSDictionaryのようなコレクションは、キーとして使用するためのコピーを正常に作成できます。)

-isEqual:-hashを実装することは理にかなっています。たとえば、私のクラスの1つの間接ユーザーは、呼び出す特定の-isEqualTo...:メソッドを知らない可能性があるためです。 2つのオブジェクトは同じクラスのインスタンスです。 id型の任意の変数で-isEqual:または-hashを呼び出して、期待される結果を得ることができるはずです。

-isEqual:(比較対象の2つのインスタンスにアクセスできる)とは異なり、-hashは、特定のインスタンス内のデータにのみアクセスして、結果を「ブラインド」で返す必要があります。 ハッシュが何に使用されているかを知ることができないため、結果は、等しい/同一と見なされるべきallの可能なインスタンスに対して一貫している必要があります。常に-isEqual:に同意する必要があります(編集:これは以下の回答によって明らかにされており、確かに作業が楽になります。)さらに、優れたハッシュ関数を作成することは簡単ではありません。一意性を保証することは特に課題です。それを表すNSUInteger(32/64ビット)しかない場合。

質問

  1. 実装する際のベストプラクティスはありますか 等式の比較 コレクションの場合は-hash
  2. Objective-CおよびCocoa風のコレクションで計画する特別な点はありますか?
  3. 妥当な信頼度で-hashをユニットテストするための良いアプローチはありますか?
  4. 任意のタイプの要素を含むコレクションに対して-hashに同意するために-isEqual:を実装することに関する提案はありますか?どのような落とし穴について知っておくべきですか? (編集:私が最初に思ったほど問題はありません—@ kperryuaが指摘するように、 "equal -hash値はnot-isEqual: "を意味します。)

編集:-isEqual:または-isEqualTo ..の実装方法について混乱していないことを明確にする必要があります。 :コレクションの場合、それは簡単です。私の混乱は主に、-isEqual:がNOを返す場合、-hashは異なる値を返さなければならないという(誤って)考えから生じたと思います。過去に暗号化を行ったことがあるので、異なる値のハッシュは異なる必要があると考えていました。しかし、以下の答えは、「良い」ハッシュ関数が本当に約であることに気づきました 最小化-hashを使用するコレクションのバケットの衝突と連鎖。一意のハッシュが望ましいですが、厳密な要件ではありません。

46
Quinn Taylor

コレクションの一意のハッシュ値を生成する、一般的に便利なハッシュ関数を考え出すのは無駄だと思います。すべてのコンテンツのハッシュを組み合わせるというU62の提案は、ハッシュ関数O(n)を作成するため、適切にスケーリングされません。良好なパフォーマンスを確保するには、ハッシュ関数を実際にO(1)にする必要があります。そうしないと、ハッシュの目的が無効になります(配列やその他の辞書を含む辞書であるplistの一般的なCocoa構造を検討してください。コレクションのハッシュ関数がO(n)の場合、大きなplistのトップレベルの辞書のハッシュを取得しようとすると、非常に遅くなります。)

私の提案は、コレクションのハッシュについてあまり心配しないことです。あなたが述べたように、-isEqual:は等しい-hash値を意味します。一方、等しい-hash値はnot-isEqual:を意味します。その事実は、単純なハッシュを作成するための多くの余裕を与えます。

ただし、衝突について本当に心配している場合(そして、実際の状況の具体的な測定で、それが心配すべきことであることを確認する証拠があります) 、U62のアドバイスにある程度従うことはできます。たとえば、コレクションの最初または最後の要素、あるいはその両方のハッシュを取得し、それをコレクションの-countなどと組み合わせることができます。それはまともなハッシュを提供するのに十分です。

それがあなたの質問の少なくとも1つに答えることを願っています。

1番については、-isEqual:の実装はかなりカットされて乾燥しています。内容を列挙し、各要素でisEqual:を確認します。

コレクションの-hash関数に対して行うことを決定することに影響を与える可能性があることに注意することが1つあります。コレクションのクライアントは、-isEqual:および-hashを管理するルールも理解する必要があります。コレクションの-hashでコンテンツ '-hashを使用する場合、コンテンツ' isEqual:-hashが一致しないと、コレクションが破損します。もちろん、これはクライアントの責任ですが、コレクションの内容に基づいて-hashを作成することに対するもう1つの論拠です。

2番は漠然としている。あなたがそこで何を考えているのかわからない。

18
kperryua

2つのコレクションが同じ要素を含んでいる場合は等しいと見なされ、さらにコレクションが順序付けられている場合は、要素が同じ順序であると見なされます。

コレクションのハッシュに関しては、要素のハッシュを何らかの方法で組み合わせるだけで十分です(XORまたはモジュロ加算)。ルールでは、IsEqualに従って等しい2つのオブジェクトは同じハッシュを返す必要があると規定されていますが、反対のハッシュは成り立たないことに注意してください。ハッシュの一意性は望ましいものですが、ソリューションの正確さには必要ありません。したがって、順序付けられたコレクションは、要素の順序を考慮する必要はありません。

ちなみに、Appleドキュメントからの抜粋は、必要な制限です。オブジェクトは、同じ値を持つオブジェクトが同じハッシュを持つことを保証しながら、変更時に同じハッシュ値を維持できませんでした。これが適用されます。最も単純なオブジェクトとコレクションの場合。もちろん、通常は、ハッシュを使用して要素を整理するコンテナ内にあるときにオブジェクトのハッシュが変更されることだけが重要です。これらすべての結果として、可変コレクションは次の場合に変更されるべきではありません。別のコンテナ内に配置されますが、真のハッシュ関数を持つオブジェクトも配置されません。

4
U62

NSArrayとNSMutableArrayのデフォルトのハッシュ実装について調査しましたが、(何かを誤解していない限り)Appleは独自のルールに従わないでください:

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

これが私のテストコードです

NSMutableArray* myMutableArray = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil];
NSMutableArray* containerForMutableArray = [NSMutableArray arrayWithObject:myMutableArray];

NSUInteger hashBeforeMutation = [[containerForMutableArray objectAtIndex:0] hash];
[[containerForMutableArray objectAtIndex:0] removeObjectAtIndex:1];
NSUInteger hashAfterMutation = [[containerForMutableArray objectAtIndex:0] hash];

NSLog(@"Hash Before: %d", hashBeforeMutation);
NSLog(@"Hash After : %d", hashAfterMutation);

出力は次のとおりです。

Hash Before: 3
Hash After : 2

したがって、NSArrayとNSMutableArrayの両方でのHashメソッドのデフォルトの実装は配列の数であり、コレクション内にあるかどうかは関係ありません。

3
Robert