Envuelva un delegado en un IEqualityComparer

127

Varias funciones Linq.Enumerable toman un IEqualityComparer<T>. ¿Hay una clase de contenedor conveniente que se adapte delegate(T,T)=>boola implementar IEqualityComparer<T>? Es bastante fácil escribir uno (si ignora los problemas al definir un código hash correcto), pero me gustaría saber si hay una solución lista para usar.

Específicamente, quiero hacer operaciones de configuración en Dictionarys, usando solo las Claves para definir la membresía (al tiempo que conserva los valores de acuerdo con diferentes reglas).

Marcelo Cantos
fuente

Respuestas:

44

Normalmente, resolvería esto comentando @Sam sobre la respuesta (he realizado algunas modificaciones en la publicación original para limpiarla un poco sin alterar el comportamiento).

El siguiente es mi riff de la respuesta de @ Sam , con una solución crítica [IMNSHO] a la política de hashing predeterminada: -

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 );
    }
}
Ruben Bartelink
fuente
55
En lo que a mí respecta, esta es la respuesta correcta . Todo lo IEqualityComparer<T>que queda GetHashCodefuera está simplemente roto.
Dan Tao
1
@Joshua Frank: No es válido usar la igualdad hash para implicar igualdad, solo lo inverso es cierto. En resumen, @Dan Tao es completamente correcto en lo que dice, y esta respuesta es simplemente la aplicación de este hecho a una respuesta previamente incompleta
Ruben Bartelink
2
@Ruben Bartelink: Gracias por aclarar. Pero todavía no entiendo su política de hash de t => 0. Si todos los objetos siempre se combinan con la misma cosa (cero), entonces, ¿no está aún más roto que usar obj.GetHashCode, según el punto de @Dan Tao? ¿Por qué no siempre obligar a la persona que llama a proporcionar una buena función hash?
Joshua Frank
1
Por lo tanto, no es razonable suponer que un algoritmo arbitrario en un Func que se ha suministrado no puede devolver verdadero a pesar de que los códigos hash son diferentes. Su punto de que devolver cero todo el tiempo no es hashing es cierto. Es por eso que hay una sobrecarga que requiere el hash Func para cuando el generador de perfiles nos dice que las búsquedas no son lo suficientemente eficientes. El único punto en todo esto es que si va a tener un algoritmo de hash predeterminado, debe ser uno que funcione el 100% del tiempo y no tenga un comportamiento peligroso superficialmente correcto. ¡Y luego podemos trabajar en el rendimiento!
Ruben Bartelink
44
En otras palabras, dado que está utilizando un comparador personalizado, no tiene nada que ver con el código hash predeterminado del objeto relacionado con el comparador predeterminado , por lo que no puede usarlo.
Peet Brits
170

Sobre la importancia de GetHashCode

Otros ya han comentado el hecho de que cualquier IEqualityComparer<T>implementación personalizada realmente debería incluir un GetHashCodemétodo ; pero nadie se molestó en explicar por qué con ningún detalle.

Este es el por qué. Su pregunta menciona específicamente los métodos de extensión LINQ; Casi todos estos dependen de códigos hash para funcionar correctamente, ya que utilizan tablas hash internamente para mayor eficiencia.

Toma Distinct, por ejemplo. Considere las implicaciones de este método de extensión si todo lo que utilizara fuera un Equalsmétodo. ¿Cómo determina si un elemento ya ha sido escaneado en una secuencia si solo lo ha hecho Equals? Enumera toda la colección de valores que ya ha visto y busca una coincidencia. ¡Esto daría como resultado el Distinctuso de un algoritmo O (N 2 ) en el peor de los casos en lugar de uno O (N)!

Afortunadamente, este no es el caso. Distinctno solo usa Equals; Se usa GetHashCodetambién. De hecho, absolutamente no funciona correctamente sin un IEqualityComparer<T>que proporcione un adecuadoGetHashCode . A continuación se muestra un ejemplo artificial que ilustra esto.

Digamos que tengo el siguiente 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);
    }
}

Ahora di que tengo un List<Value>y quiero encontrar todos los elementos con un nombre distinto. Este es un caso de uso perfecto para Distinctusar un comparador de igualdad personalizado. Entonces usemos la Comparer<T>clase de la respuesta de Aku :

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

Ahora, si tenemos un montón de Valueelementos con la misma Namepropiedad, todos deberían colapsar en un valor devuelto por Distinct, ¿verdad? Veamos...

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

Salida:

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

Hmm, eso no funcionó, ¿verdad?

¿Qué hay de GroupBy? Probemos eso:

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

Salida:

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

De nuevo: no funcionó.

Si lo piensa, tendría sentido Distinctusar un HashSet<T>(o equivalente) internamente, y GroupByusar algo como un Dictionary<TKey, List<T>>interno. ¿Podría esto explicar por qué estos métodos no funcionan? Intentemos esto:

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

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

Salida:

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

Sí ... ¿empieza a tener sentido?

Esperemos que de estos ejemplos quede claro por qué es tan importante incluir un apropiado GetHashCodeen cualquier IEqualityComparer<T>implementación.


Respuesta original

Ampliando la respuesta de orip :

Hay un par de mejoras que se pueden hacer aquí.

  1. Primero, tomaría un en Func<T, TKey>lugar de Func<T, object>; Esto evitará el encajonamiento de las claves de tipo de valor en el keyExtractorpropio.
  2. Segundo, en realidad agregaría una where TKey : IEquatable<TKey>restricción; esto evitará el encajonamiento en la Equalsllamada ( object.Equalstoma un objectparámetro; necesita una IEquatable<TKey>implementación para tomar un TKeyparámetro sin encajonarlo). Claramente, esto puede suponer una restricción demasiado severa, por lo que podría hacer una clase base sin la restricción y una clase derivada con ella.

Así es como se vería el código resultante:

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));
    }
}
Dan Tao
fuente
1
Su StrictKeyEqualityComparer.Equalsmétodo parece ser el mismo que KeyEqualityComparer.Equals. ¿La TKey : IEquatable<TKey>restricción hace que el TKey.Equalstrabajo sea diferente?
Justin Morgan
2
@JustinMorgan: Sí - en el primer caso, ya que TKeypuede ser cualquier tipo arbitrario, el compilador utilizar el método virtual Object.Equalsque requerirá boxeo de parámetros de tipo de valor, por ejemplo, int. En el último caso, sin embargo, dado que TKeyestá obligado a implementar IEquatable<TKey>, TKey.Equalsse utilizará el método que no requerirá ningún boxeo.
Dan Tao
2
Muy interesante, gracias por la información. No tenía idea de que GetHashCode tenía estas implicaciones de LINQ hasta ver estas respuestas. Genial saber para uso futuro.
Justin Morgan
1
@JohannesH: ¡Probablemente! Habría eliminado la necesidad de StringKeyEqualityComparer<T, TKey>también.
Dan Tao
1
+1 @DanTao: tardío gracias por una gran exposición de por qué uno nunca debe ignorar los códigos hash al definir la igualdad en .Net.
Marcelo Cantos
118

Cuando desee personalizar la comprobación de igualdad, el 99% de las veces le interesa definir las claves para comparar, no la comparación en sí.

Esta podría ser una solución elegante (concepto del método de clasificación de listas de Python ).

Uso:

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

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

La KeyEqualityComparerclase:

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();
    }
}
orip
fuente
3
Esto es mucho mejor que la respuesta de aku.
SLaks
Definitivamente el enfoque correcto. Hay un par de mejoras que se pueden hacer, en mi opinión, que he mencionado en mi propia respuesta.
Dan Tao
1
Este es un código muy elegante, pero no responde la pregunta, por eso acepté la respuesta de @ aku. Quería un contenedor para Func <T, T, bool> y no tengo ningún requisito para extraer una clave, ya que la clave ya está separada en mi Diccionario.
Marcelo Cantos
66
@ Marcelo: Eso está bien, puedes hacer eso; pero tenga en cuenta que si va a adoptar el enfoque de @ aku, realmente debe agregar un Func<T, int>para proporcionar el código hash para un Tvalor (como se ha sugerido, por ejemplo, en la respuesta de Ruben ). De lo contrario, la IEqualityComparer<T>implementación que le queda está bastante rota, especialmente con respecto a su utilidad en los métodos de extensión LINQ. Vea mi respuesta para una discusión sobre por qué es esto.
Dan Tao
Esto es bueno, pero si la clave que se seleccionó fuera un tipo de valor, habría un boxeo innecesario. Quizás sería mejor tener una TKey para definir la clave.
Graham Ambrose el
48

Me temo que no hay tal envoltorio fuera de la caja. Sin embargo, no es difícil crear 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));
aku
fuente
1
Sin embargo, tenga cuidado con esa implementación de GetHashCode. Si realmente lo va a usar en algún tipo de tabla hash, querrá algo un poco más robusto.
thecoop
46
¡Este código tiene un problema grave! es fácil llegar a una clase que tenga dos objetos que sean iguales en términos de este comparador pero que tengan códigos hash diferentes.
empi
10
Para remediar esto, la clase necesita otro miembro private readonly Func<T, int> _hashCodeResolverque también debe pasarse en el constructor y usarse en el GetHashCode(...)método.
herzmeister
66
Tengo curiosidad: ¿por qué estás usando en obj.ToString().ToLower().GetHashCode()lugar de obj.GetHashCode()?
Justin Morgan
3
Los lugares en el marco que tienen un IEqualityComparer<T>hash invariablemente usan detrás de escena (por ejemplo, GroupBy, Distinct, Except, Join, etc. de LINQ) y el contrato de MS con respecto al hashing se rompe en esta implementación. Aquí está el extracto de la documentación de MS: "Se requieren implementaciones para garantizar que si el método Equals devuelve verdadero para dos objetos x e y, entonces el valor devuelto por el método GetHashCode para x debe ser igual al valor devuelto para y". Ver: msdn.microsoft.com/en-us/library/ms132155
devgeezer el
22

Igual que la respuesta de Dan Tao, pero con algunas mejoras:

  1. Se basa en EqualityComparer<>.Defaultla comparación real para evitar el boxeo de los tipos de valor structque se han implementado IEquatable<>.

  2. Desde que se EqualityComparer<>.Defaultusa no explota null.Equals(something).

  3. Proporcionó un contenedor estático alrededor del IEqualityComparer<>cual tendrá un método estático para crear la instancia de comparador - facilita la llamada. Comparar

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

    con

    new EqualityComparer<Person, int>(p => p.ID);
  4. Se agregó una sobrecarga para especificar IEqualityComparer<>la clave.

La clase:

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

puedes usarlo así:

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 es una clase simple:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}
ldp615
fuente
3
+1 por proporcionar una implementación que le permite proporcionar un comparador de la clave. Además de dar más flexibilidad, esto también evita los tipos de valores de boxeo tanto para las comparaciones como para el hash.
devgeezer
2
Esta es la respuesta más detallada aquí. También agregué un cheque nulo. Completar.
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 extensiones: -

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 ) );
    }
}
Ruben Bartelink
fuente
@Sam (que ya no existe a partir de este comentario): se limpió el código sin modificar el comportamiento (y +1). Riff agregado en stackoverflow.com/questions/98033/…
Ruben Bartelink
6

La respuesta de orip es genial.

Aquí un pequeño método de extensión para hacerlo aún más fácil:

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())
Bruno
fuente
2

Voy a responder mi propia pregunta. Para tratar los diccionarios como conjuntos, el método más simple parece ser aplicar operaciones de conjunto a dict.Keys, luego convertir de nuevo a Diccionarios con Enumerable.ToDictionary (...).

Marcelo Cantos
fuente
2

La implementación en (texto en alemán) Implementando IEqualityCompare con la expresión lambda se preocupa por los valores nulos y utiliza métodos de extensión para generar IEqualityComparer.

Para crear un IEqualityComparer en una unión Linq solo tienes que escribir

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

El comparador:

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

También debe agregar un método de extensión para admitir la inferencia de tipos

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));
       }
   }
Frito
fuente
1

Solo una optimización: podemos usar el EqualityComparer listo para usar para realizar comparaciones de valores, en lugar de delegarlo.

Esto también haría que la implementación sea más limpia, ya que la lógica de comparación real ahora permanece en GetHashCode () y Equals (), que ya puede haber sobrecargado.

Aquí está el código:

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

No olvides sobrecargar los métodos GetHashCode () y Equals () en tu objeto.

Esta publicación me ayudó: c # compara dos valores genéricos

Sushil

Sushil
fuente
1
NB el mismo problema identificado en el comentario en stackoverflow.com/questions/98033/… - NO PUEDE asumir que obj.GetHashCode () tiene sentido
Ruben Bartelink
44
No entiendo el propósito de este. Creó un comparador de igualdad que es equivalente al comparador de igualdad predeterminado. Entonces, ¿por qué no lo usas directamente?
CodesInChaos
1

La respuesta de Orip es genial. Ampliando la respuesta de orip:

Creo que la clave de la solución es utilizar el "Método de extensión" para transferir el "tipo anónimo".

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

Esto permite seleccionar una propiedad con lambda como esta: .Select(y => y.Article).Distinct(x => x.ArticleID);

Max
fuente
-2

No sé de una clase existente pero algo como:

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: todavía no he compilado y ejecutado esto, por lo que puede haber un error tipográfico u otro error.

Gregg
fuente
1
NB el mismo problema identificado en el comentario en stackoverflow.com/questions/98033/… - NO PUEDE asumir que obj.GetHashCode () tiene sentido
Ruben Bartelink