「Set」にはGetメソッドが必要ですか?


22

このC#クラスを作成しましょう(Javaでもほぼ同じです)。

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}

   public override bool Equals(object obj) {
        var item = obj as MyClass;

        if (item == null || this.A == null || item.A == null)
        {
            return false;
        }
        return this.A.equals(item.A);
   }

   public override int GetHashCode() {
        return A != null ? A.GetHashCode() : 0;
   }
}

ご覧のとおり、の2つのインスタンスが等しいMyClassかどうかAだけに依存しています。したがって、等しい2つのインスタンスが存在する可能性がありますが、それらのBプロパティには異なる情報が保持されます。

多くの言語(もちろんC#とJavaを含む)の標準コレクションライブラリにはSetHashSetC#で)があり、これは各インスタンスの等しいセットから最大1つの項目を保持できるコレクションです。

アイテムを追加、アイテムを削除し、セットにアイテムが含まれているかどうかを確認できます。しかし、セットから特定のアイテムを取得することが不可能なのはなぜですか?

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
    //something
}

//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye

アイテムを取得する唯一の方法は、コレクション全体を反復処理し、すべてのアイテムが等しいかどうかを確認することです。ただし、これにはO(n)代わりに時間がかかりますO(1)

これまでのところ、セットからの取得をサポートする言語は見つかりませんでした。私が知っているすべての「共通」言語(Java、C#、Python、Scala、Haskell ...)は同じように設計されているようです。アイテムを追加することはできますが、取得することはできません。これらすべての言語がそれほど簡単で明らかに役立つものをサポートしない理由はありますか?それらはすべて間違っているとは限りませんよね?それをサポートする言語はありますか?セットから特定のアイテムを取得するのは間違っているかもしれませんが、なぜですか?


関連するSOの質問がいくつかあります。

/programming/7283338/getting-an-element-from-a-set

/programming/7760364/how-to-retrieve-actual-item-from-hashsett


12
C ++ std::setはオブジェクトの取得をサポートしているため、すべての「共通」言語があなたの説明通りであるとは限りません。
モニカの復活

17
あなたがいること(およびコード)主張するならば、同じA値と異なるBを持っている「のMyClassの2つのインスタンスの平等がAに依存するだけで、」その後、別のインスタンスが効果的であるあなた自身が定義されているので、「その特定のインスタンス」という彼らだ等しく、 Bの違いは重要ではありません。コンテナは等しいため、他のインスタンスを返すことが「許可」されます。
ペティス

7
実話:Javaでは、多くのSet<E>実装がMap<E,Boolean>内部にあります。
corsiKa

10
人Aに話す:「こんにちは、人Aをここに連れてきてください」
ブラッドトーマス

7
これはa == b、caseの反射性を破壊します(常にtrue)this.A == nullif (item == null || this.A == null || item.A == null)試験は、おそらく人工的に「高品質」コードを作成するために、はるかに「行き過ぎ」をチェックします。この種の「オーバーチェック」が見られ、Code Reviewでは常に過度に正確になっています。
usr

回答:


66

ここでの問題はメソッドがHashSet不足していることではなくGet、コードがHashSet型の観点から意味をなさないことです。

そのGet方法は事実上、「この値を取得してください」であり、.NETフレームワークの人々は「え?すでにその値を持っています<confused face />」と賢明に答えます。

アイテムを保存し、わずかに異なる別の値の一致に基づいてアイテムを取得する場合は、次のように使用Dictionary<String, MyClass>します。

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

カプセル化されたクラスから平等の情報が漏れます。に含まれるプロパティのセットを変更する場合はEquals、外部のコードを変更する必要がありますMyClass...

ええ、しかし、それMyClassは、最小の驚き(POLA)の原則に驚いているからです。同等の機能がカプセル化されているため、次のコードが有効であると仮定することは完全に合理的です。

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

これを防ぐには、MyClassその奇妙な形の平等に関して明確に文書化する必要があります。それを行った後、それはもはやカプセル化されず、その平等がどのように機能するかを変更すると、オープン/クローズの原則が破られます。エルゴ、変更しないでください。したがってDictionary<String, MyClass>、この奇妙な要件に適したソリューションです。


2
@vojta、その場合、を使用Dictionary<MyClass, MyClass>するキーに基づいて値を取得するように使用しMyClass.Equalsます。
デビッドアルノ

8
Dictionary<MyClass, MyClass>適切なIEqualityComparer<MyClass>で提供されたを使用し、MyClassなぜMyClassそのインスタンスについてこの関係を知る必要があるのかから等価関係を引き出しますか?
カレス

16
@vojtaとそこのコメント: "meh。equals の実装をオーバーライドして、等しくないオブジェクトが" equal "になるようにすることが問題です。「このオブジェクトと同じオブジェクトを取得してください」というメソッドを求めてから、同一ではないオブジェクトが返されることを期待していて、メンテナンスの問題を引き起こす可能性があります。それは多くの場合、SOに問題があります:真剣に欠陥の回答は、その壊れたコードに簡単な修正のために自分の欲望のインプリて考えていない民族でupvotedを取得...
デビッドアルノ

6
@DavidArno:平等と同一性を区別する言語を使用し続ける限り、避けられないものです;-)等しいが同一ではないオブジェクトを正規化する場合は、「同一にする」というメソッドが必要ですこのオブジェクトへのオブジェクト」、「このオブジェクトに等しい標準オブジェクトを取得してください」。これらの言語でHashSet.Getを実行すると、「同一のオブジェクトを取得する」ことを意味すると考えている人は、すでに重大なエラーに陥っています。
スティーブジェソップ

4
この答えには、などの多くの包括的な声明があります...reasonable to assume...。このすべては99%のケースで当てはまるかもしれませんが、それでもセットからアイテムを取得する機能は便利です。現実の世界のコードは、常にPOLAなどの原則に準拠しているとは限りません。たとえば、大文字と小文字を区別せずに文字列を重複排除する場合は、「マスター」アイテムを取得できます。Dictionary<string, string>回避策ですが、perfがかかります。
usr

24

セットに「入っている」アイテムがすでにあります-キーとして渡しました。

「しかし、それは私がAdd withを呼び出したインスタンスではありません」-はい、しかしあなたはそれらが等しいと明確に主張しました。

A Setは、Map|の特殊なケースでもあります。Dictionary、値の型としてvoidを使用します(無用なメソッドは定義されていませんが、それは問題ではありません)。

探しているデータ構造は、MyClassからAsを何らかの形で取得するDictionary<X, MyClass>場所Xです。

C#辞書型は、キーに関してIEqualityComparerを提供できるため、この点で優れています。

与えられた例では、次のようになります:

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

このように使用されます:

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye

キーに一致するオブジェクトを持つコードが、キーとして使用されるオブジェクトへの参照に置き換えることが有利になる場合がいくつかあります。たとえば、多くの文字列がハッシュされたコレクション内の文字列と一致することがわかっている場合、それらすべての文字列への参照をコレクション内の文字列への参照に置き換えるとパフォーマンスが向上する可能性があります。
-supercat

今日の@supercatはDictionary<String, String>
MikeFHay

@MikeFHay:ええ、しかし、各文字列参照を2回保存しなければならないのは少しエレガントではないようです。
-supercat

2
@supercat 同一の文字列を意味する場合、それは単なる文字列インターンです。組み込みのものを使用します。ある種の「標準的な」表現(単純な大文字小文字の変更技術などでは達成できない表現)を意味する場合、基本的にインデックスが必要なように聞こえます(DBが用語を使用するという意味で)。各「非標準形式」を標準形式にマッピングするキーとして保存することに問題はありません。(「標準」形式が文字列でない場合、これは同じように当てはまると思います。)これがあなたが話しているものでないなら、あなたは完全に私を失いました。
jpmc26

1
カスタムComparerDictionary<MyClass, MyClass>実用的なソリューションです。Javaでは、同じことがによって達成することができるTreeSetか、TreeMapプラスカスタムComparator
マーカスクル

19

あなたの問題は、平等という2つの矛盾する概念があることです。

  • すべてのフィールドが等しい実際の平等
  • メンバーシップの平等を設定します。Aのみが等しい

セットで実際の等式関係を使用する場合、セットから特定のアイテムを取得する問題は発生しません。オブジェクトがセット内にあるかどうかを確認するには、そのオブジェクトが既にあります。したがって、正しい等式関係を使用していると仮定して、セットから特定のインスタンスを取得する必要はありません。

セットは、or 関係(「特性関数」)によって純粋に定義される抽象データ型であると主張することもできます。他の操作が必要な場合、実際にはセットを探しているわけではありません。S contains xx is-element-of S

非常に頻繁に発生しますが、セットではないのは、すべてのオブジェクトを異なる等価クラスにグループ化することです。そのような各クラスまたはサブセットのオブジェクトは同等であり、同等ではありません。そのサブセットの任意のメンバーを介して各等価クラスを表すことができ、その後、その表す要素を取得することが望ましくなります。これは、等価クラスから代表要素へのマッピングになります。

C#では、辞書は明示的な等値関係を使用できると思います。それ以外の場合、このような関係は、クイックラッパークラスを記述することで実装できます。擬似コード:

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}

「セットから特定のインスタンスを取得する」これは、「インスタンス」を「メンバー」に変更した場合の意味をより直接的に伝えると思います。ちょっとした提案。=)+1
jpmc26

7

しかし、セットから特定のアイテムを取得することが不可能なのはなぜですか?

それがセットの目的ではないからです。

例を言い換えましょう。

「MyClassオブジェクトを格納するHashSetがあり、オブジェクトのプロパティAと等しいプロパティAを使用してそれらを取得できるようにしたい」

「HashSet」を「Collection」に、「objects」を「Values」に、「property A」を「Key」に置き換えると、文は次のようになります。

「MyClass値を格納するコレクションがあり、オブジェクトのキーに等しいキーを使用してそれらを取得できるようにしたい」

説明されているのは辞書です。実際に尋ねられる質問は、「HashSetを辞書として扱うことができないのはなぜですか?」です。

答えは、同じことには使用されないということです。セットを使用する理由は、個々のコンテンツの一意性を保証するためです。それ以外の場合は、リストまたは配列を使用するだけで済みます。質問で説明されている動作は、辞書の目的です。すべての言語設計者が失敗したわけではありません。オブジェクトがあり、セット内にある場合、それらは同等であるため、getメソッドを提供しません。つまり、同等のオブジェクトを「取得」することになります。HashSetは、同等であると定義した同等でないオブジェクトを「取得」できるような方法で実装する必要があると主張することは、言語がそれを可能にする他のデータ構造を提供する場合、非スターターです。

OOPおよび平等コメント/回答に関するメモ。マッピングのキーを、ディクショナリに格納された値のプロパティ/メンバーにすることは問題ありません。たとえば、キーとしてGuidを使用し、equalsメソッドに使用されるプロパティも完全に合理的です。合理的でないのは、残りのプロパティに異なる値を設定することです。その方向に向かっている場合、クラス構造を再考する必要があると思います。


6

equalsをオーバーライドするとすぐに、ハッシュコードをオーバーライドする方が適切です。これを行うとすぐに、「インスタンス」が内部状態を再び変更することはありません。

等号をオーバーライドせず、ハッシュコードVMオブジェクトIDを使用して等号を判断する場合。このオブジェクトをセットに入れると、再び見つけることができます。

同等性を判断するために使用されるオブジェクトの値を変更すると、ハッシュベースの構造でこのオブジェクトの追跡不能性が発生します。

したがって、Aのセッターは危険です。

今、あなたは平等に参加していないBを持っていません。ここでの問題は、意味的には技術的にではありません。技術的にBを変更することは、平等の事実に依存しないためです。意味的に、Bは「バージョン」フラグのようなものでなければなりません。

ポイントは:

Aに等しいがBに等しくない2つのオブジェクトがある場合、これらのオブジェクトの1つが他のオブジェクトよりも新しいという仮定があります。Bにバージョン情報がない場合、セット内のこのオブジェクトを「上書き/更新」することにした場合、この仮定はアルゴリズムに隠されます。これが発生するこのソースコードの場所は明らかではない場合があるため、開発者はオブジェクトXとオブジェクトYの関係を特定するのに苦労します。

Bにバージョン情報がある場合、以前は暗黙的にコードからのみ導出可能であったという仮定を明らかにします。これで、オブジェクトYがXの新しいバージョンであることがわかります。

あなた自身について考えてみてください:あなたのアイデンティティはあなたの一生のままであり、おそらくいくつかの特性が変化します(例えばあなたの髪の色;-))。確かに、茶色の髪の写真と灰色の髪の写真の2枚の写真がある場合、茶色の髪の写真の方が若いかもしれません。しかし、あなたは髪を色付けしたのでしょうか?問題は、あなたが自分の髪を着色したことを知っているかもしれないということです。他の人も?これを有効なコンテキストに入れるには、プロパティの年齢(バージョン)を導入する必要があります。そうすれば、あなたは意味的に明示的で明確になります。

「古いオブジェクトを新しいオブジェクトに置き換える」という隠された操作を回避するには、Setにgetメソッドを含めることはできません。このような動作が必要な場合は、古いオブジェクトを削除して新しいオブジェクトを追加することにより、明示的にする必要があります。

ところで:あなたが取得したいオブジェクトと等しいオブジェクトを渡す場合、それはどういう意味ですか?それは意味がありません。技術的には誰もあなたを邪魔しませんが、セマンティクスをきれいに保ち、これをしないでください。


7
「オーバーライドするとすぐにハッシュコードをオーバーライドした方がよい。これを行うとすぐに、「インスタンス」が内部状態を再び変更することはありません。」その声明は+100の価値があります。
デビッドアルノ

可変状態に応じて平等とハッシュコードの危険性を指摘するための+1
ハルク

3

特にJavaでは、HashSet最初はHashMapとにかくを使用して実装され、値を無視しました。そのため、初期設計では、getメソッドをに提供することの利点を期待していませんでしたHashSet。等しいさまざまなオブジェクト間で標準値を格納および取得する場合は、HashMap自分自身を使用します。

私はので、私は等のC#にはおろかしかし、この推論はまだJavaでいっぱいに適用されるかどうかを言うことはできませんが、このような実装の詳細を最新の状態に保たれていない場合でも、HashSetより少ないメモリを使用するために再実装されたHashMapどのような場合には、それSetインターフェースに新しいメソッドを追加するための重大な変更になります。だから、誰もが価値があるとみなしていない利益のためにそれは非常に多くの痛みです。


さて、Javaでは、これを中断しない方法で実現するための実装を提供することができますdefault。それほど便利な変更ではないようです。
ハルク

@Hulk:私は間違っているかもしれませんが、質問者が言うように、「私のアイテムを取得する唯一の方法はコレクション全体を反復し、すべてのアイテムが等しいかどうかをチェックすることです」非常に良い点は、後方互換性のある方法でそれを行うことができますがO(n)、ハッシュ関数が良好な分布を与えている場合でも、結果のget関数が比較でのみ実行することを保証するという落とし穴を追加することです。次に、Setその実装は、を含むインターフェースのデフォルト実装をオーバーライドしHashSet、より良い保証を与えることができます。
スティーブジェソップ

合意-それは良い考えだとは思わない。ただし、この種の動作には優先順位があります-List.get(int index)または-最近追加されたデフォルトの実装を選択するにはList.sort。インターフェースによって最大の複雑性が保証されますが、一部の実装は他の実装よりもはるかに優れている場合があります。
ハルク

2

セットに必要なプロパティがある主要言語があります。

C ++では、std::set順序付きセットです。指定.findした順序演算子<またはバイナリbool(T,T)関数に基づいて要素を検索するメソッドがあります。findを使用して、必要なget操作を実装できます。

実際、bool(T,T)提供する関数に特定のフラグが設定されている場合(is_transparent)、関数がオーバーロードしている別のタイプのオブジェクトを渡すことができます。つまり、「ダミー」データを2番目のフィールドに固定する必要はなく、使用する順序付け操作がルックアップ型とセットに含まれる型の間で順序付けできることを確認するだけです。

これにより、効率が向上します。

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

whereは、my_string_compare最初に整数を文字列に変換せずに整数と文字列を並べる方法を理解します(潜在的なコストがかかります)。

以下のためにunordered_set(C ++のハッシュセット)、同等の透明フラグが(まだ)ありません。メソッドにTを渡す必要がありunordered_set<T>.findます。追加することもできます==が、順序付けが必要な順序付きセットとは異なり、ハッシュにはハッシュとハッシュが必要です。

一般的なパターンは、コンテナがルックアップを実行し、コンテナ内のその要素に「イテレータ」を与えるというものです。その時点で、セット内の要素を取得または削除することができます。

要するに、すべての言語の標準コンテナにあなたが説明する欠陥があるわけではありません。C ++標準ライブラリのイテレータベースのコンテナは存在せず、少なくともいくつかのコンテナは、あなたが説明した他の言語よりも前に存在し、説明よりもさらに効率的に取得する機能が追加されました。デザインに問題はありませんし、その操作を望んでいます。使用しているセットのデザイナーは、単にそのインターフェイスを提供していません。

アセンブリで効率的に記述する方法に一致するように設計された、同等のハンドロールCコードの低レベル操作をきれいにラップするように設計されたC ++標準コンテナ。その反復子は、Cスタイルポインターの抽象化です。あなたが言及した言語はすべて、概念としてのポインタから遠ざかりました。そのため、イテレータの抽象化は使用しませんでした。

C ++にこの欠陥がないという事実は、設計の偶然かもしれません。イテレータ中心のパスとは、連想コンテナ内のアイテムとやり取りするには、まず要素へのイテレータを取得し、次にそのイテレータを使用してコンテナ内のエントリについて話すことを意味します。

代償として、追跡する必要がある反復の無効化ルールがあり、一部の操作では、1つではなく2つのステップが必要になります(これにより、クライアントコードのノイズが増えます)。利点は、堅牢な抽象化により、APIデザイナーが当初想定していたものよりも高度な使用が可能になることです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.