Avvolgere un delegato in un IEqualityComparer


127

Diverse funzioni Linq.Enumerable accettano un IEqualityComparer<T>. Esiste una comoda classe wrapper che adatta un delegate(T,T)=>boola implementare IEqualityComparer<T>? È abbastanza facile scriverne uno (se si ignorano i problemi con la definizione di un hashcode corretto), ma mi piacerebbe sapere se esiste una soluzione pronta all'uso.

In particolare, voglio fare operazioni su Dictionarys, usando solo le chiavi per definire l'appartenenza (mantenendo i valori secondo regole diverse).

Risposte:


44

Di solito, lo risolverei commentando @Sam sulla risposta (ho apportato alcune modifiche al post originale per ripulirlo un po 'senza alterare il comportamento.)

Quello che segue è il mio riff della risposta di @ Sam , con una correzione critica [IMNSHO] alla politica di hashing predefinita: -

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
Per quanto mi riguarda, questa è la risposta corretta . Tutto IEqualityComparer<T>ciò che lascia GetHashCodeè semplicemente rotto.
Dan Tao,

1
@Joshua Frank: non è valido usare l'uguaglianza di hash per implicare l'uguaglianza - solo l'inverso è vero. In breve, @Dan Tao ha perfettamente ragione in ciò che dice, e questa risposta è semplicemente l'applicazione di questo fatto a una risposta precedentemente incompleta
Ruben Bartelink,

2
@Ruben Bartelink: grazie per il chiarimento. Ma non capisco ancora la tua politica di hashing di t => 0. Se tutti gli oggetti hanno sempre la stessa cosa (zero), allora non è ancora più rotto dell'uso di obj.GetHashCode, per il punto di @Dan Tao? Perché non forzare sempre il chiamante a fornire una buona funzione hash?
Joshua Frank,

1
Pertanto non è ragionevole supporre che un algoritmo arbitrario in un Func che è stato fornito non possa restituire vero nonostante i codici hash siano diversi. Il tuo punto che restituire zero per tutto il tempo non è semplicemente hashing è vero. Ecco perché c'è un sovraccarico che prende l'hash Func per quando il profiler ci dice che le ricerche non sono sufficientemente efficienti. L'unico punto in tutto questo è che se hai un algoritmo di hashing predefinito, dovrebbe essere uno che funziona al 100% delle volte e non ha un comportamento superficialmente corretto pericoloso. E poi possiamo lavorare sulla performance!
Ruben Bartelink,

4
In altre parole, poiché si utilizza un comparatore personalizzato, non ha nulla a che fare con il codice hash predefinito dell'oggetto relativo al comparatore predefinito , quindi non è possibile utilizzarlo.
Peet Brits

170

Sull'importanza di GetHashCode

Altri hanno già commentato il fatto che qualsiasi IEqualityComparer<T>implementazione personalizzata dovrebbe davvero includere un GetHashCodemetodo ; ma nessuno si è preso la briga di spiegare il perché in alcun dettaglio.

Ecco perché. La tua domanda menziona specificamente i metodi di estensione LINQ; quasi tutti questi si basano su codici hash per funzionare correttamente, poiché utilizzano le tabelle hash internamente per l'efficienza.

Prendi Distinct, per esempio. Considera le implicazioni di questo metodo di estensione se tutto ciò che utilizzava fosse un Equalsmetodo. Come si determina se un articolo è già stato scansionato in sequenza se solo lo si è Equals? Enumeri sull'intera raccolta di valori che hai già visto e controlli una corrispondenza. Ciò comporterebbe l' Distinctuso di un algoritmo O (N 2 ) nel caso peggiore invece di uno O (N)!

Fortunatamente, non è così. Distinctnon usa soloEquals ; usa GetHashCodeanche. In realtà, non funziona assolutamente correttamente senza IEqualityComparer<T>quello che fornisce un veroGetHashCode . Di seguito è riportato un esempio inventato che illustra questo.

Di 'che ho il seguente tipo:

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

Ora dì che ho un List<Value>e voglio trovare tutti gli elementi con un nome distinto. Questo è un caso d'uso perfetto per l' Distinctutilizzo di un comparatore di uguaglianza personalizzato. Quindi usiamo la Comparer<T>classe dalla risposta di Aku :

var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);

Ora, se abbiamo un gruppo di Valueelementi con la stessa Nameproprietà, dovrebbero tutti collassare in un valore restituito da Distinct, giusto? Vediamo...

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

Produzione:

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

Hmm, non ha funzionato, vero?

Che dire GroupBy? Proviamo che:

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

Produzione:

[KEY = 'x: 1346013431']
x: 1346013431
[KEY = 'x: 1388845717']
x: 1388845717
[KEY = 'x: 1576754134']
x: 1576754134
[KEY = 'x: 1104067189']
x: 1104067189
[KEY = 'x: 1144789201']
x: 1144789201
[KEY = 'x: 1862076501']
x: 1862076501
[KEY = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377

Ancora: non ha funzionato.

Se ci pensate, avrebbe senso Distinctusare un HashSet<T>(o equivalente) internamente e GroupByusare qualcosa come un Dictionary<TKey, List<T>>internamente. Questo potrebbe spiegare perché questi metodi non funzionano? Proviamo questo:

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

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

Produzione:

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

Sì ... cominciando a dare un senso?

Speriamo che da questi esempi sia chiaro perché includere un appropriato GetHashCodein qualsiasi IEqualityComparer<T>implementazione sia così importante.


Risposta originale

Espandendo sulla risposta di orip :

Ci sono un paio di miglioramenti che possono essere fatti qui.

  1. Innanzitutto, prenderei Func<T, TKey>invece di Func<T, object>; questo impedirà l'inscatolamento delle chiavi del tipo di valore nell'effettivo keyExtractorstesso.
  2. In secondo luogo, aggiungerei effettivamente un where TKey : IEquatable<TKey>vincolo; questo eviterà l'inscatolamento nella Equalschiamata ( object.Equalsaccetta un objectparametro; è necessaria IEquatable<TKey>un'implementazione per accettare un TKeyparametro senza inscatolarlo). Chiaramente ciò può comportare una restrizione troppo grave, quindi è possibile creare una classe base senza il vincolo e una classe derivata con essa.

Ecco come potrebbe apparire il codice risultante:

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
Il tuo StrictKeyEqualityComparer.Equalsmetodo sembra essere lo stesso di KeyEqualityComparer.Equals. Il TKey : IEquatable<TKey>vincolo fa TKey.Equalsfunzionare diversamente?
Justin Morgan,

2
@JustinMorgan: Sì - nel primo caso, dato che TKeypuò essere di qualsiasi tipo arbitrario, il compilatore utilizzerà il metodo virtuale Object.Equalsche richiederà l'inscatolamento dei parametri del tipo di valore, ad es int. In quest'ultimo caso, tuttavia, poiché TKeyè vincolato all'implementazione IEquatable<TKey>, TKey.Equalsverrà utilizzato il metodo che non richiederà alcun inscatolamento.
Dan Tao,

2
Molto interessante, grazie per le informazioni. Non avevo idea che GetHashCode avesse queste implicazioni LINQ fino a vedere queste risposte. Ottimo da sapere per un uso futuro.
Justin Morgan,

1
@JohannesH: Probabilmente! Avrebbe eliminato anche la necessità di StringKeyEqualityComparer<T, TKey>.
Dan Tao

1
+1 @DanTao: ringraziamenti tardivi per una grande esposizione del perché non si dovrebbero mai ignorare i codici hash quando si definisce l'uguaglianza in .Net.
Marcelo Cantos,

118

Quando si desidera personalizzare il controllo dell'uguaglianza, il 99% delle volte è interessato a definire le chiavi da confrontare, non il confronto stesso.

Questa potrebbe essere una soluzione elegante (concetto del metodo di ordinamento dell'elenco di Python ).

Uso:

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

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

La KeyEqualityComparerclasse:

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
Questo è molto meglio della risposta di Aku.
SLaks

Sicuramente l'approccio giusto. Ci sono un paio di miglioramenti che possono essere apportati, secondo me, che ho citato nella mia risposta.
Dan Tao,

1
Questo è un codice molto elegante, ma non risponde alla domanda, motivo per cui ho accettato invece la risposta di @ aku. Volevo un wrapper per Func <T, T, bool> e non ho alcun requisito per estrarre una chiave, poiché la chiave è già separata nel mio dizionario.
Marcelo Cantos,

6
@Marcelo: Va bene, puoi farlo; ma tieni presente che se stai per adottare l'approccio di @ aku, dovresti davvero aggiungere a Func<T, int>per fornire il codice hash per un Tvalore (come è stato suggerito, ad esempio, nella risposta di Ruben ). Altrimenti l' IEqualityComparer<T>implementazione che ti rimane è piuttosto rotta, specialmente per quanto riguarda la sua utilità nei metodi di estensione LINQ. Vedi la mia risposta per una discussione sul perché questo sia.
Dan Tao,

Questo è carino ma se la chiave selezionata fosse un tipo di valore ci sarebbe un inscatolamento non necessario. Forse sarebbe meglio avere un TKey per definire la chiave.
Graham Ambrose,

48

Temo che non ci sia un tale involucro fuori dalla scatola. Tuttavia non è difficile crearne uno:

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
Tuttavia, fai attenzione con l'implementazione di GetHashCode. Se lo utilizzerai davvero in una sorta di tabella hash, vorrai qualcosa di più robusto.
thecoop,

46
questo codice ha un problema serio! è facile trovare una classe con due oggetti uguali in termini di questo comparatore ma con codici hash diversi.
empi,

10
Per ovviare a questo, la classe ha bisogno di un altro membro private readonly Func<T, int> _hashCodeResolverche deve anche essere passato nel costruttore ed essere usato nel GetHashCode(...)metodo.
Herzmeister,

6
Sono curioso: perché stai usando obj.ToString().ToLower().GetHashCode()invece di obj.GetHashCode()?
Justin Morgan,

3
I luoghi del framework che IEqualityComparer<T>utilizzano invariabilmente l'hashing dietro le quinte (ad es. GroupBy, Distinct, Except, Join, ecc.) Di LINQ e il contratto MS relativo all'hashing sono stati infranti in questa implementazione. Ecco un estratto della documentazione di MS: "Le implementazioni sono necessarie per garantire che se il metodo Equals restituisce true per due oggetti xey, il valore restituito dal metodo GetHashCode per x deve essere uguale al valore restituito per y". Vedi: msdn.microsoft.com/en-us/library/ms132155
devgeezer

22

Come la risposta di Dan Tao, ma con alcuni miglioramenti:

  1. Si affida EqualityComparer<>.Defaultall'effettivo confronto in modo da evitare la boxe per i tipi di valore structimplementati IEquatable<>.

  2. Da quando EqualityComparer<>.Defaultusato non esplode null.Equals(something).

  3. Fornito un wrapper statico attorno al IEqualityComparer<>quale avrà un metodo statico per creare l'istanza del comparatore - facilita la chiamata. Confrontare

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

    con

    new EqualityComparer<Person, int>(p => p.ID);
  4. Aggiunto un sovraccarico per specificare IEqualityComparer<>la chiave.

La classe:

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

puoi usarlo in questo modo:

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

La persona è una classe semplice:

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

3
+1 per fornire un'implementazione che consente di fornire un comparatore per la chiave. Oltre a dare maggiore flessibilità, questo evita anche i tipi di valore di pugilato sia per i confronti che per l'hash.
devgeezer,

2
Questa è la risposta più raffinata qui. Ho aggiunto anche un controllo null. Completare.
nawfal,

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

Con estensioni: -

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 (che non esiste più a partire da questo commento): ripulito il codice senza modificare il comportamento (e fatto +1). Aggiunto Riff a stackoverflow.com/questions/98033/...
Ruben Bartelink

6

la risposta di orip è ottima.

Ecco un piccolo metodo di estensione per renderlo ancora più semplice:

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

Ho intenzione di rispondere alla mia domanda. Per trattare i dizionari come set, il metodo più semplice sembra essere quello di applicare le operazioni set a dict.Keys, quindi riconvertire in Dictionaries con Enumerable.ToDictionary (...).


2

L'implementazione in (testo tedesco) Implementazione di IEqualityCompare con espressione lambda prende cura dei valori null e utilizza metodi di estensione per generare IEqualityComparer.

Per creare un IEqualityComparer in un'unione Linq devi solo scrivere

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

Il comparatore:

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

È inoltre necessario aggiungere un metodo di estensione per supportare l'inferenza del tipo

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

Solo un'ottimizzazione: possiamo usare EqualityComparer pronto per il confronto dei valori, piuttosto che delegarlo.

Ciò renderebbe anche l'implementazione più pulita poiché la logica di confronto effettiva ora rimane in GetHashCode () e Equals () che potresti avere già sovraccaricato.

Ecco il codice:

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

Non dimenticare di sovraccaricare i metodi GetHashCode () e Equals () sul tuo oggetto.

Questo post mi ha aiutato: c # confronta due valori generici

Sushil


1
NB stesso problema, individuata nel commento alla stackoverflow.com/questions/98033/... - Cant assumere obj.GetHashCode () ha un senso
Ruben Bartelink

4
Non capisco lo scopo di questo. È stato creato un comparatore di uguaglianza equivalente al comparatore di uguaglianza predefinito. Quindi perché non lo usi direttamente?
CodesInChaos,

1

la risposta di orip è ottima. Espandendo sulla risposta di orip:

penso che la chiave della soluzione sia usare "Metodo di estensione" per trasferire il "tipo anonimo".

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

Uso:

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

Ciò consente di selezionare una proprietà con lambda in questo modo: .Select(y => y.Article).Distinct(x => x.ArticleID);


-2

Non conosco una classe esistente ma qualcosa di simile:

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

Nota: non ho ancora compilato ed eseguito questo, quindi potrebbe esserci un errore di battitura o altro bug.


1
NB stesso problema, individuata nel commento alla stackoverflow.com/questions/98033/... - Cant assumere obj.GetHashCode () ha un senso
Ruben Bartelink
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.