の重要性についてGetHashCode
他の人は、カスタムIEqualityComparer<T>
実装には実際にGetHashCode
メソッドを含める必要があるという事実についてすでにコメントしています。しかし、その理由を詳しく説明する人は誰もいません。
理由は次のとおりです。あなたの質問では、LINQ 拡張メソッドについて具体的に言及しています。これらのほとんどすべては、効率のために内部でハッシュ テーブルを使用するため、適切に機能するためにハッシュ コードに依存しています。
たとえばDistinct
、使用されるすべてがメソッドである場合、この拡張メソッドの意味を検討してくださいEquals
。しか持っていない場合、アイテムがシーケンスで既にスキャンされているかどうかをどのように判断しますかEquals
? 既に調べた値のコレクション全体を列挙し、一致するかどうかを確認します。これにより、 O(N) アルゴリズムではなくDistinct
、最悪の O(N 2 ) アルゴリズムが使用されることになります。
幸いなことに、そうではありません。を使用するだけでDistinct
はありません。それも使います。実際、適切な を提供する がないと、正しく動作しません。以下は、これを説明する不自然な例です。Equals
GetHashCode
IEqualityComparer<T>
GetHashCode
次のタイプがあるとします。
class Value
{
public string Name { get; private set; }
public int Number { get; private set; }
public Value(string name, int number)
{
Name = name;
Number = number;
}
public override string ToString()
{
return string.Format("{0}: {1}", Name, Number);
}
}
ここList<Value>
で、 があり、個別の名前を持つすべての要素を検索したいとします。Distinct
これは、カスタム等値比較子を使用するのに最適なユース ケースです。それでは、 Akuの回答Comparer<T>
のクラスを使用しましょう:
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Value
さて、同じプロパティを持つ要素がたくさんある場合Name
、それらはすべて によって返される 1 つの値に折りたたまれDistinct
ますよね? どれどれ...
var values = new List<Value>();
var random = new Random();
for (int i = 0; i < 10; ++i)
{
values.Add("x", random.Next());
}
var distinct = values.Distinct(comparer);
foreach (Value x in distinct)
{
Console.WriteLine(x);
}
出力:
×:1346013431
×:1388845717
×:1576754134
×:1104067189
×:1144789201
×:1862076501
×:1573781440
×: 646797592
×: 655632802
×:1206819377
うーん、うまくいきませんでしたね。
どうGroupBy
ですか?それを試してみましょう:
var grouped = values.GroupBy(x => x, comparer);
foreach (IGrouping<Value> g in grouped)
{
Console.WriteLine("[KEY: '{0}']", g);
foreach (Value x in g)
{
Console.WriteLine(x);
}
}
出力:
[キー = 'x: 1346013431']
×:1346013431
[キー = 'x: 1388845717']
×:1388845717
[キー = 'x: 1576754134']
×:1576754134
[キー = 'x: 1104067189']
×:1104067189
[キー = 'x: 1144789201']
×:1144789201
[キー = 'x: 1862076501']
×:1862076501
[キー = 'x: 1573781440']
×:1573781440
[キー = 'x: 646797592']
×: 646797592
[キー = 'x: 655632802']
×: 655632802
[キー = 'x: 1206819377']
×:1206819377
繰り返しますが、うまくいきませんでした。
考えてみれば、 がDistinct
a HashSet<T>
(または同等のもの) を内部で使用し、 forが内部でGroupBy
a のようなものを使用するのは理にかなっていDictionary<TKey, List<T>>
ます。これで、これらの方法が機能しない理由を説明できますか? これを試してみましょう:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
出力:
×:1346013431
×:1388845717
×:1576754134
×:1104067189
×:1144789201
×:1862076501
×:1573781440
×: 646797592
×: 655632802
×:1206819377
ええ... 意味がわかり始めましたか?
GetHashCode
これらの例から、IEqualityComparer<T>
実装に適切なものを含めることが非常に重要である理由が明確になることを願っています。
元の答え
oripの答えを拡張する:
ここで行うことができる改善がいくつかあります。
- まず、;の
Func<T, TKey>
代わりにa を使用します。Func<T, object>
これにより、実際の値型キーのボックス化が防止されkeyExtractor
ます。
- 次に、実際に
where TKey : IEquatable<TKey>
制約を追加します。Equals
これにより、呼び出しでのボックス化が防止されます (パラメーターobject.Equals
を受け取ります。ボックス化せずにパラメーターを取得するには、実装object
が必要です)。明らかに、これは厳しすぎる制限をもたらす可能性があるため、制約なしで基本クラスを作成し、それを使用して派生クラスを作成できます。IEquatable<TKey>
TKey
結果のコードは次のようになります。
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
protected readonly Func<T, TKey> keyExtractor;
public KeyEqualityComparer(Func<T, TKey> keyExtractor)
{
this.keyExtractor = keyExtractor;
}
public virtual bool Equals(T x, T y)
{
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
public int GetHashCode(T obj)
{
return this.keyExtractor(obj).GetHashCode();
}
}
public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
where TKey : IEquatable<TKey>
{
public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
: base(keyExtractor)
{ }
public override bool Equals(T x, T y)
{
// This will use the overload that accepts a TKey parameter
// instead of an object parameter.
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
}