この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;
}
}
_
ご覧のとおり、MyClass
の2つのインスタンスが等しいかどうかは、A
のみに依存します。したがって、等しい2つのインスタンスがあり、それらのB
プロパティに異なる情報を保持している可能性があります。
多くの言語(もちろんC#やJavaを含む)の標準コレクションライブラリにはSet
(C#ではHashSet
)があり、コレクションはそれぞれから最大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質問があります:
https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set
https://stackoverflow.com/questions/7760364/how-to-retrieve-actual-item-from-hashsett
ここでの問題は、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>
はこの奇妙な要件に適したソリューションです。
セットに「含まれている」アイテムが既にあります-キーとして渡しました。
「しかし、私がAddを呼び出したのはそのインスタンスではありません」-はい、しかし、あなたはそれらが等しいと具体的に主張しました。
Set
もMap
| Dictionary
の特殊なケースで、値の型としてvoidを使用します(役に立たないメソッドは定義されていませんが、それは問題ではありません)。 。
あなたが探しているデータ構造はDictionary<X, MyClass>
where X
は、なんとかしてMyClassからAsを取得します。
キーのIEqualityComparerを指定できるので、C#ディクショナリタイプはこの点でいいです。
与えられた例では、私は次のようになります:
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
あなたの問題は、平等の2つの矛盾する概念があるということです。
セットで実際の等価関係を使用する場合、セットから特定のアイテムを取得する問題は発生しません。オブジェクトがセット内にあるかどうかを確認するために、すでにそのオブジェクトを持っています。したがって、正しい等式関係を使用している場合は、セットから特定のインスタンスを取得する必要はありません。
set は 抽象的なデータ型 であり、S contains x
またはx is-element-of S
リレーション(「特性関数」)によって純粋に定義されると主張することもできます。 。他の操作が必要な場合は、実際にはセットを探しているわけではありません。
非常に頻繁に発生しますが、セットではありませんが、すべてのオブジェクトを個別の 同等クラス にグループ化します。このような各クラスまたはサブセットのオブジェクトは同等であり、同等ではありません。そのサブセットの任意のメンバーを通じて各等価クラスを表すことができ、その場合、その表す要素を取得することが望ましくなります。これは、等価クラスから代表的な要素へのmappingになります。
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)); }
}
しかし、セットから特定のアイテムを入手できないのはなぜですか?
それはセットの目的ではないからです。
例を言い換えましょう。
「MyClassオブジェクトを格納したいHashSetがあり、オブジェクトのプロパティAと等しいプロパティAを使用してそれらを取得できるようにしたい」.
「HashSet」を「コレクション」に、「オブジェクト」を「値」に、「プロパティA」を「キー」に置き換えると、文は次のようになります。
「MyClass値を格納したいコレクションがあり、オブジェクトのキーと等しいキーを使用してそれらを取得できるようにしたい」.
記述されているのは辞書です。実際の質問は「HashSetを辞書として扱えないのはなぜですか?」です。
答えは、それらが同じものに使用されていないということです。セットを使用する理由は、その個々のコンテンツの一意性を保証するためです。それ以外の場合は、リストまたは配列を使用できます。質問で説明されている動作は、辞書の目的です。すべての言語デザイナーは失敗しませんでした。それらがgetメソッドを提供しないのは、オブジェクトがあり、それがセット内にある場合、それらは同等です。つまり、同等のオブジェクトを「取得」することになります。同等であると定義した非同等オブジェクトを「取得」できるようにHashSetを実装する必要があると主張することは、言語がそれを可能にする他のデータ構造を提供する場合、スターターではありません。
OOP and equality comments/answersに関する注記。マッピングのキーをディクショナリに格納された値のプロパティ/メンバーにすることは問題ありません。たとえば、Guidをキーと、equalsメソッドに使用されるプロパティも完全に妥当です。妥当でないのは、残りのプロパティに異なる値を設定することです。その方向に向かっている場合、おそらくクラス構造を再考する必要があることがわかります。
イコールをオーバーライドしたらすぐに、ハッシュコードをオーバーライドすることをお勧めします。これを実行するとすぐに、「インスタンス」が内部状態を再び変更することはありません。
イコールをオーバーライドせず、ハッシュコードVMオブジェクトIDを使用してイコールが決定されます。このオブジェクトをセットに入れると、再び見つけることができます。
等しいかどうかを判断するために使用されるオブジェクトの値を変更すると、ハッシュベースの構造でこのオブジェクトを追跡できなくなります。
したがって、Aのセッターは危険です。
今、あなたは平等に参加していないBを持っていません。ここでの問題は、意味的には技術的なものではありません。技術的にBを変更することは、平等という事実にとって中立だからです。意味的には、Bは「バージョン」フラグのようなものでなければなりません。
ポイントは:
Aと等しいがBとは異なる2つのオブジェクトがある場合、これらのオブジェクトの1つが他のオブジェクトよりも新しいと仮定します。 Bにバージョン情報がない場合、この想定はアルゴリズムで非表示になり、セット内でこのオブジェクトを「上書き/更新」することを決定します。これが発生するこのソースコードの場所は明らかではない可能性があるため、開発者は、BのXとは異なるオブジェクトXとオブジェクトYの関係を特定するのに苦労します。
Bにバージョン情報がある場合は、以前はコードから暗黙的にのみ導出可能であったという仮定を公開します。オブジェクトYはXの新しいバージョンです。
あなた自身について考えてください:あなたのアイデンティティはあなたの一生のままです、おそらくいくつかの特性が変化します(例えばあなたの髪の色;-))。確かに、茶色の髪の写真と灰色の髪の写真の2枚の写真がある場合、茶色の髪の写真の方が若い可能性があります。しかし、多分あなたはあなたの髪を着色しましたか?問題は、あなたはあなたがあなたの髪を着色したことを知っているかもしれません。他にできますか?これを有効なコンテキストに入れるには、プロパティの年齢(バージョン)を導入する必要があります。次に、あなたは意味的に明示的で明確です。
「古いオブジェクトを新しいオブジェクトに置き換える」という非表示の操作を回避するには、Setにget-Methodを含めないでください。このような動作が必要な場合は、古いオブジェクトを削除して新しいオブジェクトを追加することで明示的にする必要があります。
ところで、取得したいオブジェクトと同じオブジェクトを渡すとはどういう意味ですか?それは意味がありません。技術的に誰もあなたを邪魔することはありませんが、セマンティクスをクリーンに保ち、これを行わないでください。
必要なプロパティがセットに含まれている主要な言語があります。
C++では、_std::set
_は順序付きセットです。これには、指定した順序演算子_.find
_またはバイナリbool(T,T)
関数に基づいて要素を検索する_<
_メソッドがあります。 findを使用して、必要なget操作を実装できます。
実際、提供するbool(T,T)
関数に特定のフラグ(_is_transparent
_)がある場合、その関数が持つdifferentタイプのオブジェクトを渡すことができますオーバーロード。つまり、「ダミー」データを2番目のフィールドに挿入する必要はありません。使用する順序付け操作が、ルックアップタイプとセットに含まれるタイプの間で順序付けできることを確認してください。
これにより、効率がよくなります。
_std::set< std::string, my_string_compare > strings;
strings.find( 7 );
_
ここで、_my_string_compare
_は、最初に整数を文字列に変換せずに(潜在的なコストで)整数と文字列を並べ替える方法を理解します。
_unordered_set
_(C++のハッシュセット)の場合、同等の透過フラグは(まだ)ありません。 T
を_unordered_set<T>.find
_メソッドに渡す必要があります。追加することもできますが、順序付けだけが必要な順序付きセットとは異なり、ハッシュには_==
_とハッシュが必要です。
一般的なパターンは、コンテナがルックアップを実行し、コンテナ内のその要素への「イテレータ」を提供することです。その時点で、セット内の要素を取得したり、要素を削除したりできます。
つまり、すべての言語の標準コンテナに、あなたが説明する欠陥があるわけではありません。 C++標準ライブラリのイテレータベースのコンテナは存在せず、少なくとも一部のコンテナは、説明した他の言語の前に存在し、get さらに効率的にを実行する機能は、説明した方法よりもも追加されました。デザインに問題はなく、その操作を望んでいます。使用しているセットの設計者は、単にそのインターフェースを提供していませんでした。
アセンブリで効率的に記述する方法と一致するように設計された、同等の手作業でローリングされたCコードの低レベルの操作を明確にラップするように設計されたC++標準コンテナー。そのイテレータは、Cスタイルのポインタを抽象化したものです。あなたが言及する言語はすべて、概念としてポインタから離れてしまったので、イテレータ抽象化を使用しませんでした。
C++にこの欠陥がないという事実は、設計上の偶然である可能性があります。イテレータ中心のパスとは、連想コンテナ内のアイテムを操作するために、最初に要素へのイテレータを取得し、次にそのイテレータを使用してコンテナ内のエントリについて話すことを意味します。
コストは、追跡する必要のある反復無効化ルールがあり、一部の操作では1つのステップではなく2つのステップが必要なためです(これにより、クライアントのコードが煩雑になります)。利点は、堅牢な抽象化により、API設計者が当初考えていたものよりも高度な使用が可能になることです。