Ajustar un delegado en un IEqualityComparer

Varias funciones Linq.Enumerable toman un IEqualityComparer . ¿Existe una clase contenedora conveniente que adapte un delegate(T,T)=>bool para implementar IEqualityComparer ? Es bastante fácil escribir uno (si ignoras los problemas al definir un hashcode correcto), pero me gustaría saber si hay una solución lista para usar.

Específicamente, quiero establecer operaciones en Dictionary s, usando solo las teclas para definir la membresía (conservando los valores según diferentes reglas).

Normalmente, me gustaría resolver esto comentando @Sam en la respuesta (he hecho algunas ediciones en la publicación original para limpiarlo 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 : IEqualityComparer { readonly Func _comparer; readonly Func _hash; public FuncEqualityComparer( Func comparer ) : this( comparer, t => 0 ) // NB Cannot assume anything about how eg, t.GetHashCode() interacts with the comparer's behavior { } public FuncEqualityComparer( Func comparer, Func hash ) { _comparer = comparer; _hash = hash; } public bool Equals( T x, T y ) { return _comparer( x, y ); } public int GetHashCode( T obj ) { return _hash( obj ); } } 

Sobre la importancia de GetHashCode

Otros ya han comentado sobre el hecho de que cualquier IEqualityComparer personalizada de IEqualityComparer realmente debería incluir un método GetHashCode ; pero nadie se molestó en explicar por qué con todo 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.

Tome Distinct , por ejemplo. Considere las implicaciones de este método de extensión si todo lo que utilizó fuera un método Equals . ¿Cómo se determina si un elemento ya se ha escaneado en una secuencia si solo tiene Equals ? Enumera sobre toda la colección de valores que ya ha analizado y busca una coincidencia. ¡Esto daría como resultado Distinct usando el algoritmo O (N 2 ) del peor caso en lugar de O (N) uno!

Afortunadamente, este no es el caso. Distinct no solo usa Equals ; también usa GetHashCode . De hecho, absolutamente no funciona correctamente sin un IEqualityComparer que proporcione un GetHashCode adecuado . 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 diga que tengo una List y quiero encontrar todos los elementos con un nombre distinto. Este es un caso de uso perfecto para Distinct usando un comparador de igualdad personalizado. Así que usemos la clase Comparer de la respuesta de Aku :

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

Ahora, si tenemos un grupo de elementos de Value con la misma propiedad Name , todos deberían colapsar en un valor devuelto por Distinct , ¿no? Veamos…

 var values = new List(); 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 g in grouped) { Console.WriteLine("[KEY: '{0}']", g); foreach (Value x in g) { Console.WriteLine(x); } } 

Salida:

 [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

De nuevo: no funcionó.

Si lo piensas bien, tiene sentido que Distinct use un HashSet (o equivalente) internamente, y que GroupBy use algo como un Dictionary> internamente. ¿Podría esto explicar por qué estos métodos no funcionan? Intentemos esto:

 var uniqueValues = new HashSet(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?

Es de esperar que a partir de estos ejemplos quede claro por qué es tan importante incluir un GetHashCode apropiado en cualquier IEqualityComparer .


Respuesta original

Ampliando la respuesta de Orip :

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

  1. Primero, tomaría un Func lugar de Func ; esto evitará el encajonamiento de las claves de tipo de valor en el keyExtractor propiamente dicho.
  2. En segundo lugar, realmente agregaría una where TKey : IEquatable ; esto evitará el boxeo en la llamada Equals ( object.Equals toma un parámetro de object ; necesita una IEquatable para tomar un parámetro TKey sin TKey ). 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á el código resultante:

 public class KeyEqualityComparer : IEqualityComparer { protected readonly Func keyExtractor; public KeyEqualityComparer(Func 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 : KeyEqualityComparer where TKey : IEquatable { public StrictKeyEqualityComparer(Func 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)); } } 

Cuando desee personalizar la verificación de igualdad, el 99% del tiempo 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 ordenación de listas de Python).

Uso:

 var foo = new List { "abc", "de", "DE" }; // case-insensitive distinct var distinct = foo.Distinct(new KeyEqualityComparer( x => x.ToLower() ) ); 

La clase KeyEqualityComparer :

 public class KeyEqualityComparer : IEqualityComparer { private readonly Func keyExtractor; public KeyEqualityComparer(Func 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(); } } 

Me temo que no hay tal envoltorio listo para usar. Sin embargo, no es difícil crear uno:

 class Comparer: IEqualityComparer { private readonly Func _comparer; public Comparer(Func 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 f = (x, y) => x == y; var comparer = new Comparer(f); Console.WriteLine(comparer.Equals(1, 1)); Console.WriteLine(comparer.Equals(1, 2)); 

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

  1. Se basa en EqualityComparer<>.Default para hacer la comparación real de modo que evite el boxeo para los tipos de valor ( struct s) que ha implementado IEquatable<> .

  2. Desde EqualityComparer<>.Default utilizado, no explota en null.Equals(something) .

  3. Proporcionó un contenedor estático alrededor de IEqualityComparer<> que tendrá un método estático para crear la instancia de comparer – facilita la llamada. Comparar

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

    con

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

La clase:

 public static class Equality { public static IEqualityComparer CreateComparer(Func keySelector) { return CreateComparer(keySelector, null); } public static IEqualityComparer CreateComparer(Func keySelector, IEqualityComparer comparer) { return new KeyEqualityComparer(keySelector, comparer); } class KeyEqualityComparer : IEqualityComparer { readonly Func keySelector; readonly IEqualityComparer comparer; public KeyEqualityComparer(Func keySelector, IEqualityComparer comparer) { if (keySelector == null) throw new ArgumentNullException("keySelector"); this.keySelector = keySelector; this.comparer = comparer ?? EqualityComparer.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.CreateComparer(p => p.ID); var comparer2 = Equality.CreateComparer(p => p.Name); var comparer3 = Equality.CreateComparer(p => p.Birthday.Year); var comparer4 = Equality.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; } } 
 public class FuncEqualityComparer : IEqualityComparer { readonly Func _comparer; readonly Func _hash; public FuncEqualityComparer( Func comparer ) : this( comparer, t => t.GetHashCode()) { } public FuncEqualityComparer( Func comparer, Func 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( this IEnumerable first, IEnumerable second, Func comparer ) { return first.SequenceEqual( second, new FuncEqualityComparer( comparer ) ); } public static bool SequenceEqual( this IEnumerable first, IEnumerable second, Func comparer, Func hash ) { return first.SequenceEqual( second, new FuncEqualityComparer( comparer, hash ) ); } } 

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 Distinct(this IEnumerable list, Func keyExtractor) { return list.Distinct(new KeyEqualityComparer(keyExtractor)); } var distinct = foo.Distinct(x => x.ToLower()) 

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 volver a convertir a Dictionaries con Enumerable.ToDictionary (…).

La implementación en (texto en alemán) La implementación de 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 de Linq, solo tiene que escribir

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

El comparador:

 public class LambdaEqualityComparer : IEqualityComparer { Func _keyGetter; public LambdaEqualityComparer(Func 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 necesita agregar un método de extensión para admitir la inferencia de tipo

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

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

Esto también haría la implementación más limpia ya que la lógica de comparación actual ahora se mantiene en GetHashCode () e Igual () que ya puede haber sobrecargado.

Aquí está el código:

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

No olvide sobrecargar los métodos GetHashCode () y Equals () en su objeto.

Este post me ayudó: c # comparar dos valores generics

Sushil

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

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

  public static class Comparer { public static IEqualityComparer CreateComparerForElements(this IEnumerable enumerable, Func keyExtractor) { return new KeyEqualityComparer(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(); 
 public static Dictionary Distinct(this IEnumerable items, Func selector) { Dictionary result = null; ICollection collection = items as ICollection; if (collection != null) result = new Dictionary(collection.Count); else result = new Dictionary(); foreach (TValue item in items) result[selector(item)] = item; return result; } 

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

No sé de una clase existente, pero algo así como:

 public class MyComparer : IEqualityComparer { private Func _compare; MyComparer(Func 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 compilé y no ejecuté esto, por lo que podría haber un error tipográfico u otro error.