En este post, hablaremos sobre la implementación genérica
para IEqualityComparer.
Este tipo de implementaciones, son ideales, para gente vaga,
y que le gusta aprovechar más su tiempo realizando otro tipo de tareas, que con
copy/pastes prácticamente iguales.
IEqualityComparer es una de las interfaces, más importantes dentro del mundo de LinQ. Muchos de sus métodos más
importantes, toman una sobrecarga con un parámetro de este tipo, o de una clase
que deriva de esta interfaz. Ejemplos
de ella son: Contains, Distinct, Except, Intersect,
GrouBy, GroupJoin, Join,
SecuenceEqual, ToDictionary, ToLookUp
y Union.
El objetivo principal de IEqualityComparer, es romper la igualdad de las clases complejas, dicho con otras palabras, tratar de añadir una regla, por la cual dos objetos del mismo tipo se diferencian.
Generar interfaces IEqualityComparer, puede llegar a ser una tarea un poco tediosa y muy poco agradecida, por lo que nuestra clase genérica puede ayudarnos a hacer esto más llevadero y sencillo
- Implementación clásica IEqualityComparer (por campos)
- Implementación genérica IEqualityComparer (por campos)
- Implementación clásica IEqualityComparer (por expresión)
- Implementación genérica IEqualityComparer (por expresión)
- Code GenericIEqualityComparer class
Tenéis el código fuente disponible en github.
Implementación clásica
IEqualityComparer (por campos)
Tomaremos como ejemplo, una secuencia de objetos Customer
(Cliente).
Clase
Customer:
public class Customer { public int ID { get; set; } public string Name { get; set; } public decimal Sales { get; set; } public string City { get; set; } public static IEnumerable<Customer> GetData() { return new List<Customer>() { new Customer { ID = 1, Name = "Philips" , Sales = 2000000m, City = "Madrid" }, new Customer { ID = 2, Name = "Pionner" , Sales = 1000000m, City = "Berlin" }, new Customer { ID = 3, Name = "Renault" , Sales = 2000000m, City = "Paris" }, new Customer { ID = 4, Name = "Sony Music", Sales = 500000m, City = "London" }, new Customer { ID = 5, Name = "Sony SCEE" , Sales = 2000000m, City = "Tokio" }, new Customer { ID = 6, Name = "Pepsi" , Sales = 9000000m, City = "New York" }, new Customer { ID = 6, Name = "LG" , Sales = 2000000m, City = "Rome" } }; } }
Nota
Fíjate que los dos últimos elementos tienen el mismo ID.
Si llamamos al método de extension Distinct,
nuestro desenlace sería una secuencia de 7 elementos, no debería de encontrar ningún
elemento diferente. Esto es así porque Distinct
compara instancias (punteros)
por defecto y todos son dispares.
using System; using System.Linq; using DAL; using static System.Console; namespace ConsoleClient { class Program { static void Main(string[] args) { var customers = Customer.GetData(); WriteLine($"There are {customers.Count()} customers."); var customersDifferents = customers.Distinct(); WriteLine($"There are {customersDifferents.Count()} DIFFERENT customers."); Console.Read(); } } }
Ahora nos tocará pensar, en el campo o el conjunto de campos
por el que nuestro objeto Customer, se va a diferenciar. El campo ID
por ejemplo. IEqualityComparer
nos ayuda a gestionar y mejorar la eficiencia:
public class CustomerForIDEqualityComparer : IEqualityComparer<Customer> { public bool Equals(Customer x, Customer y) { bool result = x.ID == y.ID; return result; } public int GetHashCode(Customer obj) { return obj.ID.GetHashCode(); } }
Para más información sobre el uso y la implementación
clásica de IEqualityComparer: Spanish
- English
El nombre de la clase que implementa IEqualityComparer nos
brinda información acerca del campo utilizado para la distinción de objetos, el
campo ID. Esta no tiene que ser la única forma de calcular
la disconformidad entre objetos IEqualityComparer
, ya que podríamos tener varias clases
con diferentes reglas (PorCity,
PorSales, etc), para utilizarlas
según las requiriéramos.
Ponemos en uso nuestra clase IEqualityComparer:
static void Main(string[] args) { var customers = Customer.GetData(); WriteLine($"There are {customers.Count()} customers."); var customersDifferents = customers.Distinct(new CustomerForIDEqualityComparer()).ToList(); WriteLine($"There are {customersDifferents.Count()} DIFFERENT customers."); Console.Read(); }
Buen trabajo, el resultado es bueno ya que nos muestra que
hay 2 clientes iguales (con el mismo ID);
Implementación genérica
IEqualityComparer (por campos)
Es el momento de descargar GenericEqualityComparer de nuget a nuestro proyecto.
Tenemos 2 posibilidades:
Mediante el menú AñadirReferncia:
O por Package Manager Console:
Install-Package MoralesLarios.GenericEqualityComparer
El espacio de nombres de la clase sería el siguiente:
using MoralesLarios.Generics;
Implementación genérica
IEqualityComparer (por campos)
Vamos a repetir el mismo ejemplo anterior, pero con la clase
genérica GenericEqualityComparer.
Recuerda que esta clase no solo es válida para la elección de un único campo de
cualquier clase, es válida también para la elección de cualquier grupo de
campos por clase. Esto es complicado de explicar con palabras, pero viene a
decir que podemos precisar la desigualdad de nuestros objetos, no solo por el
campo ID, podríamos
necesitar que fuera por ID,
Name, Sales, por nombrar algunos.
La clase GenericEqualityComparer
tiene una sobrecarga con un argumento Func<T,
object> este tipo de parámetro es el mismo que el que tiene el
método Select (System.LinQ).
Ejemplo:
static void Main(string[] args) { var customers = Customer.GetData(); // Field Generic Comparator Func<Customer, object> fieldComparator = customer => customer.ID; // Instanct GenericIEqualityComparerClass GenericEqualityComparer<Customer> genericCustomerIEqualityComparer = new GenericEqualityComparer<Customer>(fieldComparator); WriteLine($"There are {customers.Count()} customers."); var customersDifferents = customers.Distinct(genericCustomerIEqualityComparer).ToList(); WriteLine($"There are {customersDifferents.Count()} DIFFERENT customers."); Console.Read(); }
El primer paso es crear FieldComparator
(Func<T, object>). Con esta
variable diremos el campo(s) que es/son nuestra virtual PK.
El Segundo paso es instanciar la clase GenericEqualityComparer. Nuestro Parametro de tipo será
de tipo Customer and inyectaremos en nuestro constructor el argumento FieldComparator, generado en el paso
anterior.
Resultado:
El mismo resultado, pero mucho más barato.
Mismo código, pero en versión genérica:
Implementación clásica
IEqualityComparer (por expresión)
Vamos a escribir una implementación clásica de IEqualityComparer,
pero en este caso, no utilizaremos un campo(s) de la clase para realizar la
diferenciación, utilizaremos una expresión, entenderemos expresión cualquier
tipo de modificación, extensión, o implementación de código sobre el tipo de
objeto que queramos, en nuestro ejemplo Customer.
Dos customers son diferentes, si la primera letra del campo Name es diferente.
Esta es nuestra clase IEqualityComparer
por expresión:
public class Customer1stCharNameComparer : IEqualityComparer<Customer> { public bool Equals(Customer x, Customer y) { bool result = x.Name?[0] == y.Name?[0]; return result; } public int GetHashCode(Customer obj) { return obj.Name == null ? 0 : obj.Name[0].GetHashCode(); } }
Realizaremos el mismo ejemplo IEqualityComparer, ahora por expresión:
static void Main(string[] args) { var customers = Customer.GetData(); WriteLine($"There are {customers.Count()} customers."); var customersDifferents = customers.Distinct(new Customer1stCharNameComparer()).ToList(); WriteLine($"There are {customersDifferents.Count()} DIFFERENT customers."); Console.Read(); }
Resultado:
Cuatro clientes diferntes: P, R, S and L.
Implementación genérica
IEqualityComparer (por Expression)
El caso por expression, tiene un ámbito mucho más amplio, y
no tiene fronteras de implementación.
La clase GenericEqualityComparer
poseé una sobrecarga que adminte un Func<T, T, bool>, este
argumento almacena la expresión de comparación.
Mismo ejemplo anterior con GenericEqualityComparer:
static void Main(string[] args) { var customers = Customer.GetData(); // Field Generic Comparator Func<Customer, Customer, bool> expressionComparator = (customer1, customer2) => customer1.Name?[0] == customer2.Name?[0]; // Instanct GenericIEqualityComparerClass GenericEqualityComparer<Customer> genericCustomerIEqualityComparer = new GenericEqualityComparer<Customer>(expressionComparator); WriteLine($"There are {customers.Count()} customers."); var customersDifferents = customers.Distinct(genericCustomerIEqualityComparer).ToList(); WriteLine($"There are {customersDifferents.Count()} DIFFERENT customers."); Console.Read(); }
El resultado es:
Como para el caso de por campos, obtenemos el mismo ejemplo
pero mucho más económico.
Una
comparativa:
Código de la clase genérica
GenericIEqualityComparer
Vamos a tratar de explicar un poco el código de GenericEqualityComparer.
using System; using System.Collections.Generic; namespace CodComun { public class GenericEqualityComparer<T> : IEqualityComparer<T> { private Func<T, object> _virtualFieldComparator; private Func<T, T, bool> _virtualFilterComparator; public GenericEqualityComparer(Func<T, object> virtualFieldComparator) { if (virtualFieldComparator == null) throw new ArgumentNullException(nameof(virtualFieldComparator), $"{nameof(virtualFieldComparator)} doesn't be null"); Reset(); this._virtualFieldComparator = virtualFieldComparator; } public GenericEqualityComparer(Func<T, T, bool> virtualFilterComparator) { if (virtualFilterComparator == null) throw new ArgumentNullException(nameof(virtualFilterComparator), $"{nameof(virtualFilterComparator)} doesn't be null"); Reset(); this._virtualFilterComparator = virtualFilterComparator; } private void Reset() { _virtualFieldComparator = null; _virtualFilterComparator = null; } public bool Equals(T x, T y) { bool result = false; if (_virtualFieldComparator != null) result = _virtualFieldComparator(x).Equals(_virtualFieldComparator(y)); else result = _virtualFilterComparator(x, y); return result; } public int GetHashCode(T obj) { int result = 0; if (_virtualFieldComparator != null) result = _virtualFieldComparator(obj).GetHashCode(); else result = _virtualFilterComparator(obj, obj).GetHashCode(); return result; } } }
De primeras, comentar que esta clase, implmenta IEqualityComparer<T>, como sus
implementaciones clásicas.
Posee dos campos privados. Estos campos almacenan (mediante
inyección de dependencias) el valor para cada uno de los argumentos soportados
por cada uno de sus dos constructores, que representan los dos modos de la
clase: For Field or For Expression, por lo que siempre
será obligatorio, utilizar uno u otro, el que no se utilice siempre será null.
Insistimos en el concepto, pero es importante señalar que
según utilicemos un constructor u otro, habilitaremos el modo For Field o el modo For Expression .
El método privado Restet,
resetea ambos modos.
Finalmente los dos métodos públicos, Equals y GetHashCode
implementan IEqualityComparer<T>,
y encapsulan las acciones de sus campos privados.
Espero que este artículo te ayude con las implementaciones de IEqualityComparer<T>, y haga que sean más fáciles en general. Espero también preguntas y sugerencias de todo tipo, ya que mi objetivo es mejorar y no solo en esto, EN TODO.
Muchas gracias a Santiago Sánchez García por su inglés, su ayuda y sus consejos.
No hay comentarios :
Publicar un comentario