.net - net - Ajustar un delegado en un IEqualityComparer
linq lambda c# (13)
Varias funciones Linq.Enumerable toman un IEqualityComparer<T>
. ¿Existe una clase contenedora conveniente que adapte un delegate(T,T)=>bool
para implementar IEqualityComparer<T>
? 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).
Sobre la importancia de GetHashCode
Otros ya han comentado sobre el hecho de que cualquier IEqualityComparer<T>
personalizada de IEqualityComparer<T>
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<T>
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<Value>
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<T>
de la respuesta de Aku :
var comparer = new Comparer<Value>((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<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:
[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<T>
(o equivalente) internamente, y que GroupBy
use algo como un Dictionary<TKey, List<T>>
internamente. ¿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?
Es de esperar que a partir de estos ejemplos quede claro por qué es tan importante incluir un GetHashCode
apropiado en cualquier IEqualityComparer<T>
.
Respuesta original
Ampliando la respuesta de Orip :
Hay un par de mejoras que se pueden hacer aquí.
- Primero, tomaría un
Func<T, TKey>
lugar deFunc<T, object>
; esto evitará el encajonamiento de las claves de tipo de valor en elkeyExtractor
propiamente dicho. - En segundo lugar, realmente agregaría una
where TKey : IEquatable<TKey>
; esto evitará el boxeo en la llamadaEquals
(object.Equals
toma un parámetro deobject
; necesita unaIEquatable<TKey>
para tomar un parámetroTKey
sinTKey
). 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<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));
}
}
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<string> { "abc", "de", "DE" };
// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) );
La clase 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();
}
}
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<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 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<TSource> Union<TSource, TComparable>(
this IEnumerable<TSource> source1,
IEnumerable<TSource> source2,
Func<TSource, TComparable> keySelector)
{
return source1.Union(source2,
new LambdaEqualityComparer<TSource, TComparable>(keySelector));
}
}
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())
Lo mismo que la respuesta de Dan Tao, pero con algunas mejoras:
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 implementadoIEquatable<>
.Desde
EqualityComparer<>.Default
utilizado, no explota ennull.Equals(something)
.Proporcionó un contenedor estático alrededor de
IEqualityComparer<>
que tendrá un método estático para crear la instancia de comparer - facilita la llamada. CompararEquality<Person>.CreateComparer(p => p.ID);
con
new EqualityComparer<Person, int>(p => p.ID);
Se agregó una sobrecarga para especificar
IEqualityComparer<>
para 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; }
}
Me temo que no hay tal envoltorio listo para usar. 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));
No sé de una clase existente, pero algo así 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 compilé y no ejecuté esto, por lo que podría haber un error tipográfico u otro error.
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<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 );
}
}
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<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 olvide sobrecargar los métodos GetHashCode () y Equals () en su objeto.
Este post me ayudó: c # comparar dos valores genéricos
Sushil
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 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<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();
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 ) );
}
}
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 hace que sea posible seleccionar una propiedad con lambda como esta:. .Select(y => y.Article).Distinct(x => x.ArticleID);