miércoles, 16 de noviembre de 2016

Generic IEqualityComparer -S-





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: PRS 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.