デリゲートをIEqualityComparerでラップする


127

いくつかのLinq.Enumerable関数はIEqualityComparer<T>delegate(T,T)=>boolを実装するために適応する便利なラッパークラスはありIEqualityComparer<T>ますか?それを書くのは簡単です(正しいハッシュコードの定義で無視する問題がある場合)が、すぐに使える解決策があるかどうか知りたいのですが。

具体的には、Dictionaryメンバーシップを定義するためにキーのみを使用して(さまざまなルールに従って値を保持しながら)、sでセット演算を実行したいと考えています。

回答:


44

通常、私は回答に@Samをコメントすることでこれを解決します(元の投稿を少し編集して、動作を変更せずに少し整理しました)。

以下は、デフォルトのハッシュポリシーに対する[IMNSHO]の重要な修正を含む、@ Samの回答の私のリフです。

class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => 0 ) // NB Cannot assume anything about how e.g., t.GetHashCode() interacts with the comparer's behavior
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

    public bool Equals( T x, T y )
    {
        return _comparer( x, y );
    }

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}

5
私に関する限り、これはある正しい答えが。除外されたものはすべてIEqualityComparer<T>GetHashCodeまっすぐに壊れています。
Dan Tao

1
@ジョシュア・フランク:ハッシュの等価性を使用して等価性を暗示することは有効ではありません-逆のみが真です。つまり、@ Dan Taoの発言は完全に正しいものであり、この回答は、この事実を以前は不完全だった回答に適用したものです
Ruben Bartelink

2
@Ruben Bartelink:明確にしていただきありがとうございます。しかし、私はまだt => 0のハッシュポリシーを理解していません。すべてのオブジェクトが常に同じもの(ゼロ)にハッシュする場合、@ Dan Taoのポイントに従って、obj.GetHashCodeを使用するよりさらに壊れていませんか?呼び出し側に常に良いハッシュ関数を提供するように強制しないのはなぜですか?
ジョシュアフランク

1
したがって、提供されたFuncの任意のアルゴリズムが、ハッシュコードが異なっていてもtrueを返すことができないと想定することは合理的ではありません。常にゼロを返すことがハッシュではないというあなたの主張は本当です。これが、プロファイラーから検索が十分に効率的でないと通知されたときにハッシュFuncを使用するオーバーロードが存在する理由です。このすべての唯一のポイントは、デフォルトのハッシュアルゴリズムを使用する場合、100%の時間で機能し、表面的には危険な動作を行わないアルゴリズムでなければならないということです。そしてパフォーマンスに取り掛かります!
Ruben Bartelink 2010

4
つまり、カスタムコンパレータを使用しているため、デフォルトコンパレータに関連するオブジェクトのデフォルトハッシュコードとは関係がないため、使用できません。
ピートブリッツ

170

の重要性について GetHashCode

他の人たちは、カスタムIEqualityComparer<T>実装にGetHashCode必ずメソッドを含める必要があるという事実についてすでにコメントしています。しかし、理由を詳細に説明することに煩わされる人はいません。

これが理由です。あなたの質問は、特にLINQ拡張メソッドについて言及しています。これらのほとんどすべてが効率的にハッシュテーブルを使用するため、適切に機能するためにハッシュコードに依存しています。

テイクDistinct例えば、。それが利用したすべてがメソッドだった場合、この拡張メソッドの影響を考慮してくださいEquals。アイテムがすでにスキャンされている場合、そのアイテムがすでにスキャンされているかどうかをどのように判断しますかEquals?すでに確認した値のコレクション全体を列挙し、一致するかどうかを確認します。これにより、DistinctO(N )アルゴリズムではなく、最悪の場合のO(N 2)アルゴリズムが使用されます。

幸い、そうではありません。単に使用するだけでDistinctはありません。それも使用します。実際、適切なを提供するがなければ、絶対に正しく機能しません。以下はこれを説明する不自然な例です。EqualsGetHashCodeIEqualityComparer<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カスタムの等値比較子を使用するのに最適な使用例です。それではComparer<T>Akuの回答のクラスを使用してみましょう。

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);
}

出力:

x:1346013431
x:1388845717
x:1576754134
x:1104067189
x:1144789201
x:1862076501
x:1573781440
x:646797592
x:655632802
x: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']
x:1346013431
[キー= 'x:1388845717']
x:1388845717
[キー= 'x:1576754134']
x:1576754134
[キー= 'x:1104067189']
x:1104067189
[キー= 'x:1144789201']
x:1144789201
[キー= 'x:1862076501']
x:1862076501
[キー= 'x:1573781440']
x:1573781440
[キー= 'x:646797592']
x:646797592
[キー= 'x:655632802']
x:655632802
[キー= 'x:1206819377']
x:1206819377

繰り返しますが、動作しませんでした。

あなたが考えてみれば、それがために理にかなってDistinct使用するようにHashSet<T>内部的に(または同等の)、および用GroupByなどの使用に何かDictionary<TKey, List<T>>内部的。これらの方法が機能しない理由をこれで説明できますか?これを試してみましょう:

var uniqueValues = new HashSet<Value>(values, comparer);

foreach (Value x in uniqueValues)
{
    Console.WriteLine(x);
}

出力:

x:1346013431
x:1388845717
x:1576754134
x:1104067189
x:1144789201
x:1862076501
x:1573781440
x:646797592
x:655632802
x:1206819377

ええ...理にかなっていますか?

これらの例からうまくいけば、適切なものGetHashCodeIEqualityComparer<T>実装に含めることがなぜそれほど重要なのかは明らかです。


元の答え

oripの答えを拡張する:

ここでできる改善点がいくつかあります。

  1. まず、Func<T, TKey>代わりにを使用しFunc<T, object>ます。これにより、実際の値タイプのキーのボックス化が回避されkeyExtractorます。
  2. 次に、実際に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));
    }
}

1
あなたのStrictKeyEqualityComparer.Equals方法はと同じようKeyEqualityComparer.Equalsです。いTKey : IEquatable<TKey>制約メイクTKey.Equalsは異なる仕事を?
Justin Morgan

2
@JustinMorgan:はい-最初のケースでは、TKey任意の型である可能性があるため、コンパイラは仮想メソッドObject.Equalsを使用します。これには、値型パラメーターのボクシングが必要になりますint。ただし、後者の場合TKeyはの実装IEquatable<TKey>に制約があるため、TKey.Equalsボクシングを必要としないメソッドが使用されます。
Dan Tao

2
非常に興味深い、情報をありがとう。これらの回答を見るまで、GetHashCodeがこれらのLINQに影響を与えるとは知りませんでした。今後の使用のために知っておくと便利です。
Justin Morgan

1
@JohannesH:たぶん!StringKeyEqualityComparer<T, TKey>あまりにも必要性を排除したでしょう。
Dan Tao

1
+1 @DanTao:.Netで同等性を定義するときにハッシュコードを無視してはならない理由を説明してくれて、本当にうれしく思います。
Marcelo Cantos 2013年

118

等価性チェックをカスタマイズする場合、99%は比較自体ではなく、比較するキーの定義に関心があります。

これはエレガントな解決策になる可能性があります(Pythonのリストソートメソッドからの概念)。

使用法:

var foo = new List<string> { "abc", "de", "DE" };

// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) );

KeyEqualityComparerクラス:

public class KeyEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, object> keyExtractor;

    public KeyEqualityComparer(Func<T,object> keyExtractor)
    {
        this.keyExtractor = keyExtractor;
    }

    public bool Equals(T x, T y)
    {
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }

    public int GetHashCode(T obj)
    {
        return this.keyExtractor(obj).GetHashCode();
    }
}

3
これは、akuの答えよりもはるかに優れています。
SLaks

間違いなく正しいアプローチ。私の考えでは、いくつかの改善が可能です。私は自分の回答で述べました。
Dan Tao

1
これは非常にエレガントなコードですが、質問には答えません。そのため、代わりに@akuの回答を受け入れました。Func <T、T、bool>のラッパーが必要でした。キーは辞書ですでに分離されているため、キーを抽出する必要はありません。
Marcelo Cantos 2010

6
@マルセロ:それで結構です、あなたはそれをすることができます。ただし、@ akuのアプローチを採用する場合は、値のハッシュコードを提供するために本当にを追加する必要があることに注意してください(たとえば、Rubenの回答で提案されています)。そうでなければ、あなたが残している実装は、特に LINQ拡張メソッドでのその有用性に関して、かなり壊れています。これがなぜであるかについての議論については私の答えを見てください。Func<T, int>TIEqualityComparer<T>
Dan Tao

これは便利ですが、選択されているキーが値タイプの場合、不要なボックス化が発生します。おそらく、キーを定義するためのTKeyがある方が良いでしょう。
Graham Ambrose

48

申し訳ありませんが、そのようなラッパーはすぐには利用できません。ただし、作成するのは難しくありません。

class Comparer<T>: IEqualityComparer<T>
{
    private readonly Func<T, T, bool> _comparer;

    public Comparer(Func<T, T, bool> comparer)
    {
        if (comparer == null)
            throw new ArgumentNullException("comparer");

        _comparer = comparer;
    }

    public bool Equals(T x, T y)
    {
        return _comparer(x, y);
    }

    public int GetHashCode(T obj)
    {
        return obj.ToString().ToLower().GetHashCode();
    }
}

...

Func<int, int, bool> f = (x, y) => x == y;
var comparer = new Comparer<int>(f);
Console.WriteLine(comparer.Equals(1, 1));
Console.WriteLine(comparer.Equals(1, 2));

1
ただし、GetHashCodeの実装には注意してください。実際にある種のハッシュテーブルで使用する場合は、もう少し堅牢なものが必要になります。
thecoop 2009年

46
このコードには深刻な問題があります!この比較子に関しては同じであるが、異なるハッシュコードを持つ2つのオブジェクトを持つクラスを簡単に思い付くことができます。
empi

10
これを修正するには、クラスに別のメンバーprivate readonly Func<T, int> _hashCodeResolverが必要です。このメンバーもコンストラクターに渡して、GetHashCode(...)メソッドで使用する必要があります。
ヘルツマイスター

6
気になる:obj.ToString().ToLower().GetHashCode()代わりにを使用しているのはなぜobj.GetHashCode()ですか?
Justin Morgan

3
IEqualityComparer<T>この実装では、バックグラウンドで常にハッシュを使用するフレームワーク内の場所(たとえば、LINQのGroupBy、Distinct、Except、Joinなど)とハッシュに関するMSコントラクトが壊れています。MSのドキュメントの抜粋は次のとおりです「Equalsメソッドが2つのオブジェクトxとyに対してtrueを返す場合、xのGetHashCodeメソッドによって返される値がyに対して返される値と等しくなるようにするための実装が必要です。」参照:msdn.microsoft.com/en-us/library/ms132155
devgeezer 2012

22

Dan Taoの回答と同じですが、いくつかの改良点があります。

  1. 依存しEqualityComparer<>.Default、それは値型(のためのボクシングを回避するように比較実際の操作を行うためにstruct実装された複数の)IEquatable<>

  2. EqualityComparer<>.Default中古なので爆発しませんnull.Equals(something)

  3. IEqualityComparer<>Comparerのインスタンスを作成する静的メソッドを持つ静的ラッパーが提供されます-呼び出しが簡単になります。比較する

    Equality<Person>.CreateComparer(p => p.ID);

    new EqualityComparer<Person, int>(p => p.ID);
  4. IEqualityComparer<>キーに指定するオーバーロードを追加しました。

クラス:

public static class Equality<T>
{
    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
    {
        return CreateComparer(keySelector, null);
    }

    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, 
                                                         IEqualityComparer<V> comparer)
    {
        return new KeyEqualityComparer<V>(keySelector, comparer);
    }

    class KeyEqualityComparer<V> : IEqualityComparer<T>
    {
        readonly Func<T, V> keySelector;
        readonly IEqualityComparer<V> comparer;

        public KeyEqualityComparer(Func<T, V> keySelector, 
                                   IEqualityComparer<V> comparer)
        {
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");

            this.keySelector = keySelector;
            this.comparer = comparer ?? EqualityComparer<V>.Default;
        }

        public bool Equals(T x, T y)
        {
            return comparer.Equals(keySelector(x), keySelector(y));
        }

        public int GetHashCode(T obj)
        {
            return comparer.GetHashCode(keySelector(obj));
        }
    }
}

次のように使用できます。

var comparer1 = Equality<Person>.CreateComparer(p => p.ID);
var comparer2 = Equality<Person>.CreateComparer(p => p.Name);
var comparer3 = Equality<Person>.CreateComparer(p => p.Birthday.Year);
var comparer4 = Equality<Person>.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase);

Personは単純なクラスです。

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

3
キーの比較子を提供できる実装を提供するための+1。これにより、柔軟性が高まるだけでなく、比較とハッシュの両方で値型のボックス化が回避されます。
devgeezer 2012

2
これが最も具体的な答えです。nullチェックも追加しました。コンプリート。
nawfal 2013

11
public class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => t.GetHashCode())
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

    public bool Equals( T x, T y )
    {
        return _comparer( x, y );
    }

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}

拡張機能付き:-

public static class SequenceExtensions
{
    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer ) );
    }

    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer, Func<T, int> hash )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer, hash ) );
    }
}

@Sam(このコメントの時点で存在しなくなりました):動作を調整せずにコードをクリーンアップ(および+1)しました。stackoverflow.com/questions/98033/…に
Ruben Bartelink

6

oripの答えは素晴らしいです。

これをさらに簡単にするための小さな拡張メソッド:

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, object>    keyExtractor)
{
    return list.Distinct(new KeyEqualityComparer<T>(keyExtractor));
}
var distinct = foo.Distinct(x => x.ToLower())

2

私自身の質問に答えます。辞書をセットとして扱うための最も簡単な方法は、セット操作をdict.Keysに適用してから、Enumerable.ToDictionary(...)を使用して辞書に戻すことです。


2

(ドイツ語のテキスト)での実装ラムダ式を使用したIEqualityCompareの実装 を使用したは、null値が考慮され、拡張メソッドを使用してIEqualityComparerが生成されます。

LinqユニオンでIEqualityComparerを作成するには、次のように記述するだけです。

persons1.Union(persons2, person => person.LastName)

比較子:

public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource>
{
  Func<TSource, TComparable> _keyGetter;

  public LambdaEqualityComparer(Func<TSource, TComparable> keyGetter)
  {
    _keyGetter = keyGetter;
  }

  public bool Equals(TSource x, TSource y)
  {
    if (x == null || y == null) return (x == null && y == null);
    return object.Equals(_keyGetter(x), _keyGetter(y));
  }

  public int GetHashCode(TSource obj)
  {
    if (obj == null) return int.MinValue;
    var k = _keyGetter(obj);
    if (k == null) return int.MaxValue;
    return k.GetHashCode();
  }
}

また、型推論をサポートする拡張メソッドを追加する必要があります

public static class LambdaEqualityComparer
{
       // source1.Union(source2, lambda)
        public static IEnumerable<TSource> Union<TSource, TComparable>(
           this IEnumerable<TSource> source1, 
           IEnumerable<TSource> source2, 
            Func<TSource, TComparable> keySelector)
        {
            return source1.Union(source2, 
               new LambdaEqualityComparer<TSource, TComparable>(keySelector));
       }
   }

1

ただ1つの最適化:委任するのではなく、すぐに使えるEqualityComparerを値の比較に使用できます。

実際の比較ロジックは、すでにオーバーロードされているかもしれないGetHashCode()とEquals()に留まるので、これにより実装がよりクリーンになります。

これがコードです:

public class MyComparer<T> : IEqualityComparer<T> 
{ 
  public bool Equals(T x, T y) 
  { 
    return EqualityComparer<T>.Default.Equals(x, y); 
  } 

  public int GetHashCode(T obj) 
  { 
    return obj.GetHashCode(); 
  } 
} 

オブジェクトのGetHashCode()およびEquals()メソッドをオーバーロードすることを忘れないでください。

この投稿は私を助けました: c#2つの一般的な値を比較する

スシル


1
:stackoverflow.com/questions/98033/…のコメントで特定されたのと同じ問題-obj.GetHashCode()が理にかなっていると仮定できない
Ruben Bartelink

4
これの目的はわかりません。デフォルトの等値比較子と同等の等値比較子を作成しました。それでは、なぜそれを直接使用しないのですか?
CodesInChaos

1

オリップの答えは素晴らしいです。oripの答えを拡張する:

ソリューションの鍵は「拡張メソッド」を使用して「匿名型」を転送することだと思います。

    public static class Comparer 
    {
      public static IEqualityComparer<T> CreateComparerForElements<T>(this IEnumerable<T> enumerable, Func<T, object> keyExtractor)
      {
        return new KeyEqualityComparer<T>(keyExtractor);
      }
    }

使用法:

var n = ItemList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList();
n.AddRange(OtherList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList(););
n = n.Distinct(x=>new{Vchr=x.Vchr,Id=x.Id}).ToList();

0
public static Dictionary<TKey, TValue> Distinct<TKey, TValue>(this IEnumerable<TValue> items, Func<TValue, TKey> selector)
  {
     Dictionary<TKey, TValue> result = null;
     ICollection collection = items as ICollection;
     if (collection != null)
        result = new Dictionary<TKey, TValue>(collection.Count);
     else
        result = new Dictionary<TKey, TValue>();
     foreach (TValue item in items)
        result[selector(item)] = item;
     return result;
  }

これにより、次のようなラムダを使用してプロパティを選択できるようになります。 .Select(y => y.Article).Distinct(x => x.ArticleID);


-2

私は既存のクラスを知りませんが、次のようなものです:

public class MyComparer<T> : IEqualityComparer<T>
{
  private Func<T, T, bool> _compare;
  MyComparer(Func<T, T, bool> compare)
  {
    _compare = compare;
  }

  public bool Equals(T x, Ty)
  {
    return _compare(x, y);
  }

  public int GetHashCode(T obj)
  {
    return obj.GetHashCode();
  }
}

注:実際にはまだコンパイルして実行していないため、タイプミスやその他のバグがある可能性があります。


1
:stackoverflow.com/questions/98033/…のコメントで特定されたのと同じ問題-obj.GetHashCode()が理にかなっていると仮定できない
Ruben Bartelink
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.