Como hemos mencionado en otros posts y en otras ocasiones,
la librería principal de LinQ,
System.Linq, está formada por un
conjunto de métodos extensores que en la mayoría de los casos expande la
funcionalidad del tipo
IEnumerable<T>. Por este motivo, y por facilidad de generación
de este tipo de métodos, alargar, dilatar, mejorar o incluso moldear muchos de
ellos, se vuelve una tarea muy simple y en ocasiones bastante gratificante y
útil.
Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.
Construyendo un nuevo Operador en
LinQ
La característica más importante cuando expandimos la
librería de LinQ, es crear
métodos extensores sobre el tipo IEnumerable<T>.
Con esto quiero argumentar que no debemos, aunque en nuestro trabajo diario
trabajemos con tipos más experimentados como List<T>, ObservableCollection<T>
ó Arrays, extender estas clases, ya que estaríamos limitando mucho el ámbito d
trabajo de los mismos.
Podemos agrupar los tipos de operadores que podemos extender
en 4 tipos principales:
- OPERADOR QUE DEVUELVA UN SENCILLO ELEMENTO .- Este tipo de operador, devolverá un elemento del tipo de nuestra secuencia ‘T’. Ejemplos de este tipo son First, Last, etc.
- OPERADOR QUE DEVUELVA UNA SECUENCIA DE ELEMENTOS .- Este tipo de operador, devolverá una secuencia de elementos de tipo ‘T’. Ejemplos de este tipo son Where, Select, Skip o Take.
- OPERADOR QUE DEVUELVA UN VALOR TOTALIZADOR (Aggregate) .- Este tipo de operador, devolverá un valor como resultado de una totalización o conjunción de registros de una secuencia. Ejemplos de este tipo son Sum, Count o el propio Aggregate.
- OPERADOR QUE DEVUELVA UNA SECUENCIA DE ELEMENTOS AGRUPADOS .- Este tipo de operador, devolverá una secuencia de elementos agrupados de tipo ‘T’. Ejemplos de este tipo son GroupBy o GroupJoin.
Vamos a construir uno de cada tipo, para poder observar su
fórmula de creación con más detalle.
Clase utilizada para los ejemplos:
public class Persona { public string DNI { get; set; } public string Nombre { get; set; } public decimal Ingresos { get; set; } public DateTime FechaNacimiento { get; set; } public static IEnumerable<Persona> ConstruirPersonas() { return new List<Persona>() { new Persona { DNI = "11111111A", Nombre = "Ana", Ingresos = 25000m, FechaNacimiento = new DateTime(1990, 03,15) }, new Persona { DNI = "22222222B", Nombre = "Luis", Ingresos = 35000m, FechaNacimiento = new DateTime(1980, 08,15) }, new Persona { DNI = "33333333C", Nombre = "Marta", Ingresos = 30000m, FechaNacimiento = new DateTime(1992, 10,10) }, new Persona { DNI = "44444444D", Nombre = "Miguel", Ingresos = 55000m, FechaNacimiento = new DateTime(1978, 06,28) }, new Persona { DNI = "55555555E", Nombre = "Enrique", Ingresos = 90000m, FechaNacimiento = new DateTime(1987, 01,15) }, new Persona { DNI = "66666666F", Nombre = "Pakkko", Ingresos = 25000m, FechaNacimiento = new DateTime(1992, 12,01) }, new Persona { DNI = "77777777G", Nombre = "Juan", Ingresos = 1 , FechaNacimiento = new DateTime(1990, 06,20) } }; } }
Tipo Elemento Sencillo
Este tipo de extensión, debe como es común en todos,
extender el tipo IEnumerable<T> y
devolver un único elemento de este tipo genérico, al igual que lo hacen Last, Single o FirstOrDefault,
en general para los Operadores
de Elemento.
En este primer tipo, trataremos de hacer un caso amplio, con
varias versiones del método, que nos sirvan de ejemplo para las siguientes
implementaciones de tipos de extensión.
Vamos a crear un ejemplo muy simple, pero a la vez muy
didáctico en el que se pueda conseguir el objetivo de forma clara y concisa.
Nuestra extensión elegida para este caso va a ser Second. Este método como su propio nombre indica va a devolver
el segundo elemento de una secuencia. Iremos dando pasadas para irlo mejorando.
Vamos con la primera versión de nuestro método extensor Second:
public static T Second<T>(this IEnumerable<T> source) { if (source.Count() < 2) throw new ArgumentException($"El parametro {nameof(source)}, debe de tener al menos 2 elementos", nameof(source)); T resultado = source.Skip(1).Take(1).First(); return resultado; }
Esta sería la versión inicial, como se puede apreciar tiene
una implementación muy sencilla y en ella se emplean otros operadores de la
librería de LinQ.
Dentro del uso extensivo de LinQ, cuando lo aplicamos sobre colecciones con un número muy
alto de elementos, se estima que este tiene una perdida en torno a 10% sobre el
manejo de bucles generales, por
este motivo y para intentar generar el método extensor lo más óptimo posible,
vamos a sustituir las llamadas a los métodos de la librería por bucles.
Este es el resultado de la modificación del código:
public static T Second<T>(this IEnumerable<T> source) { if (source.Count() < 2) throw new ArgumentException($"El parametro {nameof(source)}, debe de tener al menos 2 elementos", nameof(source)); T resultado = Activator.CreateInstance<T>(); int contador = 0; using(var enumerador = source.GetEnumerator()) { while (enumerador.MoveNext() && contador < 2) { if (contador == 1) { resultado = enumerador.Current; } contador++; } } return resultado; }
Estamos realizando un bucle a bajo nivel, esto está
explicado más detalladamente en la entrada secuencias
y enumeradores.
Con esto hemos ganado rendimiento, pero todavía nos quedaría
dar un pasito más en pos de construir el método extensor más óptimo de la
historia. El paso sucesivo consistiría en comprobar el tipo de secuencia que
está realizando la llamada.
IEnumerable<T>,
podríamos afirmar que es el tipo más primitivo, o el que está en la punta más
alta de la jerarquía, por eso es el tipo elegido para extender. Este método
puede ser consumido por tipos más complejos que implementan IEnumerable<T>, tales como List<T>, T[], etc., que incorporan otro tipo de interfaces que suman más
funcionalidad. Una opción a tener en cuenta es la que aporta la interface IList<T>, y no es otra que la
capacidad de indexación, que en
nuestro ejemplo simple, nos liberaría del trabajo de tener que iterar sobre la
secuencia, ya que podríamos devolver directamente el elemento de índice 1 ( [1] ):
public static T Second<T>(this IEnumerable<T> source) { if (source.Count() < 2) throw new ArgumentException($"El parametro {nameof(source)}, debe de tener al menos 2 elementos", nameof(source)); T resultado = Activator.CreateInstance<T>(); /// Es un IList<T> ?? var list = source as IList<T>; if (list != null) { resultado = list[1]; } else { int contador = 0; using (var enumerador = source.GetEnumerator()) { while (enumerador.MoveNext() && contador < 2) { if (contador == 1) { resultado = enumerador.Current; } contador++; } } } return resultado; }
Para poner la guinda en el pastel y dejar el método como los
chorros del oro, solo nos quedaría finalizar añadiendo un segundo método
extensor: SecondOrDefault, en la
línea del conjunto de operadores de elemnto y que tendría una dificultad
bajísima si reutilizamos el predecesor.
public static T SecondOrDefault<T>(this IEnumerable<T> source) { T resultado = source.Count() >= 2 ? source.Second() : default(T); return resultado; }
La parte más reseñable, es la correspondiente a default(T),
que se encarga de devolver el valor por defecto según el tipo de datos. Esto ya
lo repasamos en la palabra reservada default.
Tipo Secuencia de elementos
Este tipo de extensión se caracteriza porque sus métodos
devuelven una secuencia de datos IEnumerable<T>.
Ejemplos de este tipo son los elementos de consulta estándar, como Where, Select, etc.
Una de las características que debería de implementar este
tipo de operadores, es la capacidad de carga
perezosa o diferida, mediante el operador Yield return y Yield Break.
El método extensor que he pensado para este ejemplo, más que
un ejemplo es un caso real de trabajo, que implementé hace unos años. Este
caso, era la típica situación de una aplicación que generaba un fichero de
salida. Este fichero se iba rellenando con datos de otros ficheros y de otras
tablas, hasta estar completamente finalizado. Algo así:
El ejemplo puede complicarse un poco, pero espero que su coste
práctico le dé más valor.
Tenemos un ‘FICHERO PRINCIAPAL’ que se carga solo con 2
campos de manera inicial, y necesita ir consultando otros ficheros para ir
completando su información .En nuestro ejemplo el ‘SUB-FICHERO 1’ que contiene
datos de Salarios y el ‘SUB-FICHERO 2’ que contiene datos sobre fechas de
nacimiento.
Ya que aquí no nos daría nada de valor generar ficheros y
cargarlos, sustituiremos los éstos, por Secuencias de Datos (Colecciones de
objetos).
Nuestro método extensor tiene que tener 2 parámetros de tipo
T y K, ya que las
colecciones utilizadas, serán de objetos de tipos diferentes.
La estructura del método tendrá 4 parámetros:
- Source .- Secuencia principal sobre la que tomará el tipo para realizar el método de Extensión. Esta secuencia será la que completará sus datos.
- SubSecuencia .- Secuencia secundaria sobre la que se consultarán los datos para completar la secuencia principal con los valores coincidentes.
- FiltroCohesion .- Condición que elegimos mediante la cual cruzamos los datos entre la secuencia principal y la subsecuencia. En otras palabras, este es el criterio que elegimos para comunicarle al método que tienen que campos o características tienen que poseer los registros de una u otra colección para considerarlas iguales.
- AccionRelleno .- Acción que debe de ejecutarse cada vez que se encuentre una coincidencia en cada una de las secuencias. Esto viene a ser de forma reducida la manera de indicar que valor(es) del campo de la subsecuencia debe de tomarse para rellenar otro valor(es) de la principal.
public static void CompletarDatos<T, K>(this IEnumerable<T> source, IEnumerable<K> SubSecuencia, Func<T, K, bool> filtroCohesion, Action<T, K> accionRelleno) { foreach (T s in source) { var coincidencia = SubSecuencia.FirstOrDefault(d => filtroCohesion(s, d)); if (coincidencia != null) { accionRelleno(s, coincidencia); } } }
Para realizar su misión, el método recorre todos los
elementos de la colección principal, y aplica el filtroCohesion sobre la subColeccion,
con el objetivo de buscar el elemento coincidente, que en caso de encontrarlo
le aplica la acción de relleno.
Vamos a verlo en acción.
Construimos los moldes de nuestras 3 colecciones:
public static IEnumerable<Persona> ConstruirColeccionPrincipal() { return new List<Persona>() { new Persona { DNI = "11111111A", Nombre = "Ana" }, new Persona { DNI = "22222222B", Nombre = "Luis" }, new Persona { DNI = "33333333C", Nombre = "Marta" }, new Persona { DNI = "44444444D", Nombre = "Miguel" }, new Persona { DNI = "55555555E", Nombre = "Enrique" }, new Persona { DNI = "66666666F", Nombre = "Pakkko" }, new Persona { DNI = "77777777G", Nombre = "Juan" } }; } public static IEnumerable<Persona> ConstruirSubColeccionIngresos() { return new List<Persona>() { new Persona { DNI = "22222222B", Ingresos = 35000m }, new Persona { DNI = "11111111A", Ingresos = 25000m }, new Persona { DNI = "33333333C", Ingresos = 30000m }, new Persona { DNI = "66666666F", Ingresos = 25000m }, new Persona { DNI = "55555555E", Ingresos = 90000m }, new Persona { DNI = "44444444D", Ingresos = 55000m }, new Persona { DNI = "77777777G", Ingresos = 1 } }; } public static IEnumerable<Persona> ConstruirSubColeccionFechas() { return new List<Persona>() { new Persona { DNI = "66666666F", FechaNacimiento = new DateTime(1992, 12,01) }, new Persona { DNI = "11111111A", FechaNacimiento = new DateTime(1990, 03,15) }, new Persona { DNI = "44444444D", FechaNacimiento = new DateTime(1978, 06,28) }, new Persona { DNI = "22222222B", FechaNacimiento = new DateTime(1980, 08,15) }, new Persona { DNI = "33333333C", FechaNacimiento = new DateTime(1992, 10,10) }, new Persona { DNI = "55555555E", FechaNacimiento = new DateTime(1987, 01,15) }, new Persona { DNI = "77777777G", FechaNacimiento = new DateTime(1990, 06,20) } }; }
Creamos un método para pintar valores:
Ponemos en práctica nuestro método extensor:
public static void PintarValores(IEnumerable<Persona> source) { foreach (var persona in source) { string strIngresos = persona.Ingresos == 0 ? "(null)" : persona.Ingresos.ToString("c"); string strFechaNacimiento = persona.FechaNacimiento == DateTime.MinValue ? "(null)" : persona.FechaNacimiento.ToString("d"); Console.WriteLine("{0:-10} - {1,-8} - {2,10} - {3,-10}", persona.DNI, persona.Nombre, strIngresos, strFechaNacimiento); } }
Ponemos en práctica nuestro método extensor:
static void Main(string[] args) { var coleccionPrincipal = Persona.ConstruirColeccionPrincipal(); var subColeccionIngresos = Persona.ConstruirSubColeccionIngresos(); var subColeccionFechas = Persona.ConstruirSubColeccionFechas(); Console.WriteLine(" **** Datos Principales "); Persona.PintarValores(coleccionPrincipal); Console.WriteLine(" **** Fusionamos la SubColecciónIngresos "); coleccionPrincipal.CompletarDatos(subColeccionIngresos, (principal, subClase) => principal.DNI == subClase.DNI, (principal, subClase) => principal.Ingresos = subClase.Ingresos); Persona.PintarValores(coleccionPrincipal); Console.WriteLine(" **** Fusionamos la SubColecciónFechas "); coleccionPrincipal.CompletarDatos(subColeccionFechas, (principal, subClase) => principal.DNI == subClase.DNI, (principal, subClase) => principal.FechaNacimiento = subClase.FechaNacimiento); Persona.PintarValores(coleccionPrincipal); Console.Read(); }
Cargamos cada una de nuestras colecciones, la principal, la subcolección de Ingresos
y de fechas. Para ello
hemos utilizado secuencias del mismo tipo de datos, pero estas podrían ser
perfectamente de tipos de datos diferentes.
Cargamos la colección
principal y la pintamos por pantalla para mostrar los campos que
están a null y que iremos
rellenando en pasos posteriores. Fusionamos con nuestro método extensor los
valores referentes a los ingresos, advirtiendo que el campo por el que
cruzaremos los datos será el DNI
(siendo este nuestro filtroCohesion)
. El campo a completar será la propiedad ingresos
de la colección principal, mediante
el valor del campo Ingresos
de la colección de Ingresos
(siendo este nuestra accionRelleno)
Pintamos de nuevo para ver que el campo ingresos de la
colección principal ya está relleno. El tercer paso será un calco del segundo
para los datos referentes a Fechas.
Y este será el resultado:
Con esto daríamos por acabado nuestro método extensor de
tipo secuencia de elementos.
Tipo Valor (aggregate) Totalizador
Para construir un método
extensor de este tipo, obligatoriamente tenemos que fijarnos en los operadores
de tipo agregación, tales como min,
max, sum, etc. Todos estos operadores atesoran una característica
común, y es que retornan un dato calculado.
El método que hemos elegido, se va a llamar ContarLetras, y va a consistir en
contar cada una de las letras de los valores de los campos. Si alguno de los
campos no es de tipo cadena,
se llamará a su método ToString
y se contarán las letras de este resultado.
Vamos a añadirle una sobrecarga, para dar la posibilidad de
poder hacer el recuento de una sola columna.
Estas serían las implementaciones:
public static int ContarLetras<T>(this IEnumerable<T> source) { int resultado = 0; var propiedades = typeof(T).GetProperties(); foreach (var item in source) { foreach (var propiedad in propiedades) { object value = propiedad.GetValue(item); resultado += value?.ToString()?.Length ?? 0; } } return resultado; } public static int ContarLetras<T>(this IEnumerable<T> source, Func<T, object> field) { int resultado = 0; var propiedades = typeof(T).GetProperties(); foreach (var item in source.Select(field)) { foreach (var propiedad in propiedades) { object value = propiedad.GetValue(item); resultado += value?.ToString()?.Length ?? 0; } } return resultado; }
De forma generalista, los métodos, recorren los elementos de
las secuencias de datos y con la ayuda de reflexión
obtienen los valores de cada una de las propiedades que contabilizan el nº de
elementos de cada una de sus propiedades pasadas a cadena.
static void Main(string[] args) { var secuenciaDeDatos = Persona.ConstruirPersonas(); int numeroLetrasTotales = secuenciaDeDatos.ContarLetras(); Console.WriteLine($"El nº de letras totales es {numeroLetrasTotales}"); int numeroLetrasTotalesCampoNombre = secuenciaDeDatos.ContarLetras(a => a.Nombre); Console.WriteLine($"El nº de letras totales del campo Nombre es {numeroLetrasTotalesCampoNombre}"); Console.Read(); }
Con el siguiente resultado:
Tipo Secuencia de elementos Agrupados
La característica principal de este caso de extensión, se
centra en que el patrón de elementos devueltos es de tipo agrupado, exactamente
el correspondiente a la interface IGrouping<TKey,
TElement>, que comparte con los operadores
de agrupación, tales como GroupBy,
GroupJoin o ToLookUp.
Vamos a añadir un ejemplo no demasiado práctico, pero sí
bastante pedagógico, ya que nos revela el procedimiento de definición de sus
argumentos y la salida.
El método extensor es algo tan simple, como devolver los
elementos redundantes y agrupados de una secuencia:
public static IEnumerable<IGrouping<TKey, TElement>> RedundantesDuplicados<TKey, TElement>(this IEnumerable<TElement> source, Func<TElement, TKey> agrupacion) { var resultado = source.GroupBy(agrupacion).Where(a => a.Count() > 1); return resultado; }
En el día a día del desarrollador, este va a ser el tipo de
extensión que menos se utiliza, ya que el tipo IGrouping<TKey, TElement> es muy poco flexible, no permite generar
instancias compatibles del mismo con facilidad y la agrupación tiene que
generarse por uno o varios campos del tipo de datos principal. Normalmente
cuando realizamos algún método extensor que devuelve datos agrupados, se opta
por crearlo en base a una clase POCO,
con un elemento de TKey,
de tipo conocido y una lista de elementos de TElement. Esto es mucho más flexible y te libera de ataduras de
tipos y restricciones, con el mismo resultado. Ambos no dejan de ser una Lista de Listas o una colección de colecciones.
Este podría ser un ejemplo de una POCO sin las restricciones
de IGrouping<TKey, TElement> :
public class DatoAgrupado<TKey, TElement> { public TKey Clave { get; private set; } public IEnumerable<TElement> Grupo { get; private set; } public DatoAgrupado(TKey clave, IEnumerable<TElement> grupo) { Clave = clave; Grupo = grupo; } }
U olvidándonos de Generics,
esta otra, mucho más simple, pero de valor mucho más acotado:
public class DatoAgrupado { public string Clave { get; private set; } public IEnumerable<MiTipo> Grupo { get; private set; } public DatoAgrupado(string clave, IEnumerable<MiTipo> grupo) { Clave = clave; Grupo = grupo; } }
Esto rompería las cadenas de IGrouping<TKey, TElement> , permitiéndonos crear
extensiones de PseudoAgrupación, como esta:
public static IEnumerable<DatoAgrupado<int, TElement>> Paginar<TElement>(this IEnumerable<TElement> source, int elementosPorGrupo) { IList<DatoAgrupado<int, TElement>> resultado = new List<DatoAgrupado<int, TElement>>(); int numeroDeGrupos = source.Count() / elementosPorGrupo; for (int i = 0; i <= numeroDeGrupos; i++) { var datosGrupo = source.Skip(i * elementosPorGrupo).Take(elementosPorGrupo); resultado.Add ( new DatoAgrupado<int, TElement>(i, datosGrupo) ); } return resultado; }
Un método extensor que realiza paginación de datos,
eligiendo el nº de elementos por grupo. Como podemos apreciar, la clave de
nuestros elementos agrupados son de tipo int,
y este no forma parte de nuestro TElement,
que era una de las limitaciones que tenía IGrouping<TKey,
TElement> .
Así lo usaríamos:
static void Main(string[] args) { Console.WriteLine("{0}{0}{0}", Environment.NewLine); var personas = Persona.ConstruirPersonas(); var personasPaginadas = personas.Paginar(3); foreach (var item in personasPaginadas) { Console.WriteLine($"**** Grupo {item.Clave + 1}"); foreach (var personaEnGrupo in item.Grupo) { Console.WriteLine($" {personaEnGrupo.Nombre}"); } } Console.Read(); }
Y este sería su resultado:
Y de aquí, sin límites, solo los que nosotros nos queramos
poner, plena libertad de creación, como siempre en estos casos según la dedicación
claro, jejeje.
No hay comentarios :
Publicar un comentario