sábado, 17 de enero de 2015

Proyecciones en LinQ, La cláusula Select y SelectMany






Dentro del mundo de LinQ, las proyecciones, determinan el tipo de datos que será devuelto o proyectado cuando sea aplicado a uno de nuestras secuencias IEnumerable<T>.

 En este apartado estudiaremos los operadores de consulta Select y SelectMany. Hay otros autores y expertos en la materia que les gusta añadir en el apartado de proyecciones las cláusulas Join, GroupBy y GroupJoin, pero en este caso yo he preferido crear un apartado específico para cada una de ellas y así intentar no excederme demasiado en cada una de las entradas para hacerlas más comprensibles y dar más énfasis a cada una de ellas, aplicando casos prácticos de uso.







Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.



Select


El operador Select se utiliza para crear una secuencia de salida de un tipo de datos a partir de una secuencia de entrada de otro tipo de elementos. Los tipos de entrada y salida no tienen por qué ser del mismo tipo de datos. En definitiva el operador Select transforma unos datos de entrada en unos datos de salida.

Vamos a ver un ejemplo muy sencillo, para ir después desgranando toda la chicha que lleva dentro. Para ello utilizaremos una clase de mi tipo preferido para hacer ejemplos, la clase Persona:

public class Persona
{
    public int      Id              { get; set; }
    public string   Nombre          { get; set; }
    public DateTime FechaNacimiento { get; set; }
    public decimal  Ingresos        { get; set; }      
}

Y aquí el ejemplo:

static void Main(string[] args)
{
 
    var personas = new List<Persona>
    {
        new Persona {Id = 0, Nombre = "Pepito" , FechaNacimiento = new DateTime(1980, 1, 1), Ingresos = 20000m},
        new Persona {Id = 1, Nombre = "Jorgito", FechaNacimiento = new DateTime(1978, 2, 2), Ingresos = 30000m},
        new Persona {Id = 2, Nombre = "Luisito", FechaNacimiento = new DateTime(1981, 3, 3), Ingresos = 40000m},
        new Persona {Id = 3, Nombre = "Laurita", FechaNacimiento = new DateTime(1982, 4, 4), Ingresos = 50000m}
    };
 
    /// Oper. Consulta
    var nombresConsulta = from p in personas
                            select p.Nombre;
 
    /// Lambda
    var nombresLambda = personas.Select(p => p.Nombre);
 
    foreach (var nombre in nombresLambda) /// Podríamos haber utilizado nombresConsulta
    {
        Console.WriteLine(nombre);
    }
 
 
    Console.Read();
}

Resultado:
















Como vemos en este ejemplo tan sencillo, tenemos un List<Persona> como secuencia de entrada y mediante el uso del operador Select la transformamos en un IEnumerable<string> como secuencia de salida.

Dentro de la comparativa inevitable entre LinQ y SQL, hay una práctica muy utilizada dentro del mundo de las bases de datos, que es la selección de un número determinado de campos de una tabla, normalmente siempre inferior a la totalidad de los campos de la misma, una consulta de este tipo:

string consultaSql = "SELECT NOMBRE, INGRESOS " +
                     "FROM PERSONAS";


Algo completamente común y extremadamente sencillo en SQL. Esto también lo podemos hacer en LinQ, y aquí es donde entran en juego la utilización de los tipos anónimos y la asignación implícita de tipos, que ya estudiamos en entradas anteriores. En SQL, esto lo hace de manera trasparente el motor de base de datos.

Vamos a ver ese ejemplo en LinQ:

static void Main(string[] args)
{
 
    var personas = new List<Persona>
    {
        new Persona {Id = 0, Nombre = "Pepito" , FechaNacimiento = new DateTime(1980, 1, 1), Ingresos = 20000m},
        new Persona {Id = 1, Nombre = "Jorgito", FechaNacimiento = new DateTime(1978, 2, 2), Ingresos = 30000m},
        new Persona {Id = 2, Nombre = "Luisito", FechaNacimiento = new DateTime(1981, 3, 3), Ingresos = 40000m},
        new Persona {Id = 3, Nombre = "Laurita", FechaNacimiento = new DateTime(1982, 4, 4), Ingresos = 50000m}
    };
 
    /// Oper. Consulta
    var personasConsulta = from p in personas
                            select new {p.Nombre, p.Ingresos};
 
    /// Lambda
    var personasLambda = personas.Select(p => new { p.Nombre, p.Ingresos });
 
    foreach (var persona in personasConsulta) /// Podríamos haber utilizado personasLambda
    {
        Console.WriteLine("{0} - {1}", persona.Nombre, persona.Ingresos);
    }
 
    Console.WriteLine(Environment.NewLine);
 
    Type tipoDatos = personasConsulta.First().GetType();
 
    Console.WriteLine("El tipo de datos de personaConsulta es : {0}", tipoDatos.Name);
 
    Console.Read();
}

Dando como resultado:


















Como podemos comprobar el tipo de datos de resultado es <>F__AnonymousType0`2  que es el nombre del tipo de datos que le asigna el compilador de manera automática, ahorrándonos tener que crear uno para la causa.

A modo de resumen, comentar que si dentro de nuestro código, necesitáramos hacer algo más con esta colección de resultado de tipo anónimo, como pasarlo por parámetros a un método consumidor o devolverlo como return de una función, no nos quedaría más remedio que crear un tipo ‘conocido’ de resultado, como por ejemplo:

public class SubPersona
{
    public string  Nombre   { get; set; }
    public decimal Ingresos { get; set; }
}


Y dentro de la cláusula o el método extensor Select, cambiaríamos así las llamadas:

/// Oper. Consulta
var personasConsulta = from p in personas
                       select new SubPersona { Nombre = p.Nombre, Ingresos = p.Ingresos };
 
/// Lambda
var personasLambda = personas.Select(p => new SubPersona { Nombre = p.Nombre, Ingresos = p.Ingresos });

También podemos darle otro uso alejado de la analogía con SQL y utilizarlo como transformador de datos dentro de lo que podría ser una de nuestras tareas del día a día, como en este caso:


static void Main(string[] args)
{
 
    var ficheros = System.IO.Directory.EnumerateFiles(@"C:\");
 
    ///// Oper. Consulta
    var ficherosConsulta = from f in ficheros
                            select new System.IOFileInfo(f);
 
    ///// Lambda
    var ficherosLambda = ficheros.Select(f => new System.IOFileInfo(f));
 
    foreach (var ficheroInfo in ficherosLambda)
    {
            Console.WriteLine("{0} - {1} - {2}", ficheroInfo.Name, ficheroInfo.CreationTime, ficheroInfo.IsReadOnly);
    }

    Console.Read();
}

Simplemente listaremos los ficheros de la unidad C: y crearemos FileInfos a partir de su ruta, para tener acceso a todos sus atributos, con el siguiente resultado:
















Y para finalizar, vamos a ver cómo podríamos hacer nuestro propio método extensor Select y lo fácil que sería hacer una implementación de éste:


public static IEnumerable<S> NuestroSelect<T, S>(this IEnumerable<T> source, Func<T, S> selector)
{
    foreach (var s in source)
    {
        yield return selector(s);
    }
}

Si tenéis algún tipo de duda sobre la sintaxis de los métodos extensores, os dejo aquí un enlace donde podréis ver la entrada anterior del blog y donde se explican ampliamente.

Lo primero que vemos es que es un método genérico que contiene 2 variables de tipo T y S. T será el tipo de datos de entrada y S el tipo de datos de salida. Nuestro método atesora como parámetro extensor source, una secuencia de elementos de tipo T, IEnumerable<T> que será nuestra colección de entrada y devuelve una secuencia de datos de tipo S, IEnumerable<S> que será la secuencia de salida o resultado. Solo nos queda hablar del parámetro selector que se trata de un delegado anónimo Func, que precisará un objeto de tipo T para dar como resultado un objeto de tipo S, aquí será donde indicaremos la forma de realizar la conversión. 

 Como con el operador where, el operador Select, también tiene una sobrecarga que nos suministra otro parámetro con el índice del dato tratado:

Método extensor (tener en cuenta que es una emulación y es código creado de forma didáctica, el de Microsoft, es otra historia), aunque el objetivo y el resultado sea completamente el mismo, eso sí, sin tener en cuenta el rendimiento y el control claro está:


public static IEnumerable<S> NuestroSelect<T, S>(this IEnumerable<T> source, Func<T, int, S> selector)
{
    for(int i = 0; i < source.Count(); i++)
    {
        yield return selector(source.ElementAt(i), i);
    }
}


Y aquí consumiéndolo:


static void Main(string[] args)
{
    var personas = new List<Persona>
    {
        new Persona {Id = 0, Nombre = "Pepito" , FechaNacimiento = new DateTime(1980, 1, 1), Ingresos = 20000m},
        new Persona {Id = 1, Nombre = "Jorgito", FechaNacimiento = new DateTime(1978, 2, 2), Ingresos = 30000m},
        new Persona {Id = 2, Nombre = "Luisito", FechaNacimiento = new DateTime(1981, 3, 3), Ingresos = 40000m},
        new Persona {Id = 3, Nombre = "Laurita", FechaNacimiento = new DateTime(1982, 4, 4), Ingresos = 50000m}
    };
 
    /// Lambda
    var personasLambda = personas.NuestroSelect((p, i) => new { Indice = i, Nombre = p.Nombre, Ingresos = p.Ingresos });
 
    foreach (var persona in personasLambda) /// solo podemos utilizar personasLambda
    {
        Console.WriteLine("{0} - {1}", persona.Indice, persona.Nombre);
    }
 
    Console.Read();
}


Es muy importante apreciar, que en este ejemplo solo lo he utilizado Lambdas, y es que esta sobrecarga carece de caso de uso con expresiones de consulta.

He utilizado nuestro método, pero podría haber hecho la llamada al método de Microsoft con el mismo modo uso y el mismo resultado:


var personasLambda = personas.Select((p, i) => new { Indice = i, Nombre = p.Nombre, Ingresos = p.Ingresos });


Resultado:













Considero bastante importante el valor didáctico de construir nuestra propia implementación del método, ya que esto nos abre la mente para un futuro, a la hora de necesitar de algún operador que no tengamos disponible entre los que nos ofrece LinQ, siéndonos de gran ayuda en nuestro objetivo de no repetir código.



SelectMany

El otro operador a tratar dentro de esta entrada será SelectMany. A mi parecer la palabra que mejor describe el funcionamiento de este operador es DESAGRUPAR. Se encarga de proveernos una colección de datos de salida, a partir de una fuente de datos formada, por lo que llamamos comúnmente una colección de colecciones, o por utilizar otro símil con BD, una relación de uno a n (1 – n).

Vamos a ver unos ejemplos, que irán subiendo de dificultad y darán un poco de sentido a tanta palabra.

Nuestras clases bases serán las siguientes:

public class Equipo
{
    public string        Nombre         { get; set; }
    public DateTime      FechaFundacion { get; set; }
    public List<Jugador> Jugadores      { get; set; }
}
 
public class Jugador
{
    public string      Nombre  { get; set; }
    public Demarcacion Posicon { get; set; }
    public decimal     Salario { get; set; }
}
 
public enum Demarcacion
{
    Portero,
    Defensa,
    Centrocampista,
    Delantero
}

Ahora vamos a hacer un método que cargue Equipos:


public static List<Equipo> CargarEquipos()
{
    return new List<Equipo>()
    {
        new Equipo()
        {
            Nombre = "Atlético de Madrid",
            FechaFundacion = new DateTime(1903, 4, 26),
            Jugadores = new List<Jugador>()
            {
                new Jugador()
                {
                    Nombre = "Koke",
                    Posicon = Demarcacion.Centrocampista,
                    Salario = 5000000m
                },
                new Jugador()
                {
                    Nombre = "Godín",
                    Posicon = Demarcacion.Defensa,
                    Salario = 3000000m
                }
            }
        },
 
 
        new Equipo()
        {
            Nombre = "Getafe",
            FechaFundacion = new DateTime(1983, 7, 8),
            Jugadores = new List<Jugador>()
            {
                new Jugador()
                {
                    Nombre = "Diawara",
                    Posicon = Demarcacion.Delantero,
                    Salario = 1000000m
                },
                new Jugador()
                {
                    Nombre = "Pedro León",
                    Posicon = Demarcacion.Centrocampista,
                    Salario = 2000000m
                },
                new Jugador()
                {
                    Nombre = "Codina",
                    Posicon = Demarcacion.Portero,
                    Salario = 1500000m
                }
 
            }
        }
    };
}


Aquí tenemos nuestra colección de colecciones. Si pensamos con nuestro celebro de desarrollador tradicional, esta sería la manera de recorrer los resultados, mediante un bucle anidado:


var equipos = CargarEquipos();
 
foreach (var equipo in equipos)
{
    foreach (var jugador in equipo.Jugadores)
    {
        Console.WriteLine("{0} - {1}", jugador.Nombre, jugador.Posicon);
    }
}


El operador SelectMany, nos permite desagruparlos de esta manera tan sencilla:


ar equipos = CargarEquipos();
 
var jugadoresConsulta = equipos.SelectMany(e => e.Jugadores) // aquí indicamos el campo que contine la colección a desgrupar
                        .ToList();
 
foreach (var jugador in jugadoresConsulta)
{
    Console.WriteLine("{0} - {1}", jugador.Nombre, jugador.Posicon);
}


El resultado para ambos sería el mismo:











Este es el ejemplo más sencillo, y si vemos la firma de su método extensor, es básicamente igual al del select:

public static IEnumerable<S> SelectMany<T, S>(this IEnumerable<T> surce, Func<T, IEnumerable<S>> selector) 

La única diferencia está en el parámetro selector, que tiene como tipo de retorno en vez de un objeto de tipo S, un objeto IEnumerable<S>. Esto consta de todo el sentido del mundo ya que por cada uno de los objetos T que recibe, debe poseer una propiedad que sea una colección, y que señalaremos para desagrupar, por lo que el resultado será una colección de elementos y no un único elemento.

Viendo este ejemplo, supongo que a muchos se os pasará por la cabeza, que ocurriría en caso de tener que imprimir algo referente al Equipo, como por ejemplo el nombre del Club o la fecha de fundación, como en este ejemplo:

var equipos = CargarEquipos();
 
foreach (var equipo in equipos)
{
    foreach (var jugador in equipo.Jugadores)
    {
        Console.WriteLine("{0} - {1} - {2}", equipo.Nombre, jugador.Nombre, jugador.Posicon);
    }
}


Una de las sobrecargas de SelectMany, también nos permite tener acceso a los datos de los clubes, de la siguiente forma:


var equipos = CargarEquipos();
 
var jugadoresEquipos =
    equipos.SelectMany(e => e.Jugadores, // aqui indicamos la propiedad que tiene la colección
        (equipo, jugador) =>  // Abrimos lambda con 2 parámetros uno para la info del equipo y otro par la del jugador
            new
            {
                Club          = equipo.Nombre,
                FechaCreacion = equipo.FechaFundacion,
                NombreJugador = jugador.Nombre,
                Posicion      = jugador.Posicon,
                Salario       = jugador.Salario
            }).ToList();
 
 
foreach (var jugadorEquipo in jugadoresEquipos)
{
 
    Console.WriteLine("{0,-20} - {1,-10} - {2}", jugadorEquipo.Club, jugadorEquipo.NombreJugador, jugadorEquipo.Posicion);
                
}



En este segundo ejemplo, al igual que en el primero, se le indica como primer parámetro la propiedad que tendrá la colección a desagrupar, y una nueva lambda como segundo parámetro que nos facilita 2 argumentos, uno para el Equipo y otro para cada uno de los jugadores, en el que tendremos que construir el tipo de datos que tendrá cada uno de los elementos de nuestro enumerador de resultado.

El resultado vendría a ser el mismo que si estuviéramos lanzando una consulta Join a 2 tablas de nuestra base de datos Equipos y Jugadores, con una relación de 1 a n.

Más adelante, veremos el operador GroupJoin, que realizará exactamente el caso contrario al operador SelectMany y que desde la visión y el punto de vista del desarrollador, es la forma más óptima y más clara de trabajar.

Resultado:













Vamos a ver cómo sería la firma de esta segunda sobrecarga:

Todo esto desglosado, quedaría así:

Tipos …

T à Es el tipo de datos del Objeto principal, el que tiene la colección en nuestro caso Equipo.
K à Es el tipo de datos de cada uno de los objetos secundarios o de la colección o agrupación, en nuestro caso Jugador.
S à Es el tipo de datos de resultado (puede ser un tipo conocido o un tipo anónimo).

IEnumerable<S> à Es el resultado de la función y devolverá una colección de objetos del tipo de resultado.

this IEnumerable<T> à Colección de objetos principales, que a su vez contiene cada uno de ellos otra colección de objetos y que como indica la palabra reservada this, será el tipo que extenderemos.

Func<T, IEnumerable<S>> à Este parámetro precisa de objeto del tipo principal (Equipo) y da como resultado una colección de objetos secundarios (Jugadores), es donde señalamos la propiedad de nuestro objeto principal que tiene a su vez la colección de objetos secundarios.


Func<T, K, S> à En este último parámetro, simplemente recibiremos, el objeto de tipo principal y el secundario, para formar uno de tipo resultado.

Puedo parecer un poco pesado intentando desmigar este tipo de firmas, pero entender todo esto, te hace ver el desarrollo en general de otra forma y dar una potencia y dinamismo al lenguaje sin precedentes.


Aparte de estas 2 sobrecargas, el operador SelectMany, cuenta con 2 más, exactamente iguales, pero añadiendo un índice, con el número de orden del registro dentro de la colección, igual que veíamos en el operador Select o Where.


2 comentarios :

  1. Excelente explicación!!!!
    Muchas gracias por el aporte.

    ResponderEliminar
  2. La mejor explicación que encontré en internet, muchas gracias y felicitaciones!!

    ResponderEliminar