Sentencias en LINQ (I): Selecciones simples. Tipado implícito y tipos anónimos.


Antes de profundizar un poco más en las posibilidades que ofrece LINQ para acceso a distintas fuentes de datos, conviene hablar un poco de las operaciones que podemos realizar con él.

LINQ, como buen lenguaje de consultas, permite la realización de múltiples operaciones sobre conjuntos de datos, tales como proyecciones, ordenaciones, particiones (útiles para realizar paginaciones) o agregaciones.

A continuación construiremos un pequeño conjunto de ejemplos que nos servirán como punto de inicio para comprender el funcionamiento de la sintaxis de este lenguaje. Comenzaremos creando en primer lugar un pequeño conjunto de datos sobre los que operar. Crearemos para ello cuatro entidades relacionadas entre sí. Asumiremos que tratamos de crear una aplicación para una papelería que quiere llevar un registro de clientes y ventas de productos, por lo que haremos uso de las siguientes entidades:

  • Cliente: simboliza un cliente de la papelería.
    • Id (int)
    • Nombre (string)
    • FechaNac (DateTime)
  • Producto: simboliza cada uno de los materiales que la papelería venderá a los clientes.
    • Id (int)
    • Descripcion (string)
    • Precio (float)
  • Pedido: simboliza un conjunto de productos proporcionados a un cliente concreto, por lo que se compondrá de una o más líneas de pedido.
    • Id (int)
    • IdCliente (int)
    • FechaPedido (DateTime)
  • LineaPedido: simboliza la venta de un número de un único producto concreto a un cliente en particular. Se asocia a un único pedido.
    • Id (int)
    • IdPedido (int)
    • IdProducto (int)
    • Cantidad (int)

Hemos definido las relaciones pese a que para este ejemplo no vamos a hacer uso de una base de datos relacional. Sin embargo, nuestro objetivo es simular esta opción (veremos cómo acceder a una fuente de datos relacional en posteriores artículos), por lo que haremos algo que nunca, bajo ninguna circunstancia, debería hacerse: codificar los datos en una clase de forma manual. Todo sea por el afán didáctico J

Como hemos dicho, crearemos una clase estática que contenga una lista de cada una de estas clases. El código de éstas será el siguiente:


    public class Cliente
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public DateTime FechaNac { get; set; }
    }
    public class Producto
    {
        public int Id { get; set; }
        public string Descripcion { get; set; }
        public float Precio { get; set; }
    }
    public class Pedido
    {
        public int Id { get; set; }
        public int IdCliente { get; set; }
        public DateTime FechaPedido { get; set; }
    }
    public class LineaPedido
    {
        public int Id { get; set; }
        public int IdPedido { get; set; }
        public int IdProducto { get; set; }
        public int Cantidad { get; set; }
    }

Ahora que hemos modelado las entidades, crearemos una clase estática que contenga, también de forma estática, un listado de los objetos que acabamos de decir (niños, no hagáis esto en producción).


    public static class DataLists
    {
        private static List<Cliente> _listaClientes = new List<Cliente>() {
            new Cliente { Id = 1,   Nombre = "Carlos Gonzalez Rodriguez",   FechaNac = new DateTime(1980, 10, 10) },
            new Cliente { Id = 2,   Nombre = "Luis Gomez Fernandez",        FechaNac = new DateTime(1961, 4, 20) },
            new Cliente { Id = 3,   Nombre = "Ana Lopez Diaz ",             FechaNac = new DateTime(1947, 1, 15) },
            new Cliente { Id = 4,   Nombre = "Fernando Martinez Perez",     FechaNac = new DateTime(1981, 8, 5) },
            new Cliente { Id = 5,   Nombre = "Lucia Garcia Sanchez",        FechaNac = new DateTime(1973, 11, 3) }
        };

        private static List<Producto> _listaProductos = new List<Producto>()
        {
            new Producto { Id = 1,      Descripcion = "Boligrafo",          Precio = 0.35f },
            new Producto { Id = 2,      Descripcion = "Cuaderno grande",    Precio = 1.5f },
            new Producto { Id = 3,      Descripcion = "Cuaderno pequeño",   Precio = 1 },
            new Producto { Id = 4,      Descripcion = "Folios 500 uds.",    Precio = 3.55f },
            new Producto { Id = 5,      Descripcion = "Grapadora",          Precio = 5.25f },
            new Producto { Id = 6,      Descripcion = "Tijeras",            Precio = 2 },
            new Producto { Id = 7,      Descripcion = "Cinta adhesiva",     Precio = 1.10f },
            new Producto { Id = 8,      Descripcion = "Rotulador",          Precio = 0.75f },
            new Producto { Id = 9,      Descripcion = "Mochila escolar",    Precio = 12.90f },
            new Producto { Id = 10,     Descripcion = "Pegamento barra",    Precio = 1.15f },
            new Producto { Id = 11,     Descripcion = "Lapicero",           Precio = 0.55f },
            new Producto { Id = 12,     Descripcion = "Grapas",             Precio = 0.90f }
        };

        private static List<Pedido> _listaPedidos = new List<Pedido>()
        {
            new Pedido { Id = 1,     IdCliente = 1,  FechaPedido = new DateTime(2013, 10, 1) },
            new Pedido { Id = 2,     IdCliente = 1,  FechaPedido = new DateTime(2013, 10, 8) },
            new Pedido { Id = 3,     IdCliente = 1,  FechaPedido = new DateTime(2013, 10, 12) },
            new Pedido { Id = 4,     IdCliente = 1,  FechaPedido = new DateTime(2013, 11, 5) },
            new Pedido { Id = 5,     IdCliente = 2,  FechaPedido = new DateTime(2013, 10, 4) },
            new Pedido { Id = 6,     IdCliente = 3,  FechaPedido = new DateTime(2013, 7, 9) },
            new Pedido { Id = 7,     IdCliente = 3,  FechaPedido = new DateTime(2013, 10, 1) },
            new Pedido { Id = 8,     IdCliente = 3,  FechaPedido = new DateTime(2013, 11, 8) },
            new Pedido { Id = 9,     IdCliente = 3,  FechaPedido = new DateTime(2013, 11, 22) },
            new Pedido { Id = 10,    IdCliente = 3,  FechaPedido = new DateTime(2013, 11, 29) },
            new Pedido { Id = 11,    IdCliente = 4,  FechaPedido = new DateTime(2013, 10, 19) },
            new Pedido { Id = 12,    IdCliente = 4,  FechaPedido = new DateTime(2013, 11, 11) }
        };

        private static List<LineaPedido> _listaLineasPedido = new List<LineaPedido>()
        {
            new LineaPedido() { Id = 1,  IdPedido = 1,  IdProducto = 1,     Cantidad = 9 },
            new LineaPedido() { Id = 2,  IdPedido = 1,  IdProducto = 3,     Cantidad = 7 },
            new LineaPedido() { Id = 3,  IdPedido = 1,  IdProducto = 5,     Cantidad = 2 },
            new LineaPedido() { Id = 4,  IdPedido = 1,  IdProducto = 7,     Cantidad = 2 },
            new LineaPedido() { Id = 5,  IdPedido = 2,  IdProducto = 9,     Cantidad = 1 },
            new LineaPedido() { Id = 6,  IdPedido = 2,  IdProducto = 11,    Cantidad = 15 },
            new LineaPedido() { Id = 7,  IdPedido = 3,  IdProducto = 12,    Cantidad = 2 },
            new LineaPedido() { Id = 8,  IdPedido = 3,  IdProducto = 1,     Cantidad = 4 },
            new LineaPedido() { Id = 9,  IdPedido = 4,  IdProducto = 2,     Cantidad = 3 },
            new LineaPedido() { Id = 10, IdPedido = 5,  IdProducto = 4,     Cantidad = 2 },
            new LineaPedido() { Id = 11, IdPedido = 6,  IdProducto = 1,     Cantidad = 20 },
            new LineaPedido() { Id = 12, IdPedido = 6,  IdProducto = 2,     Cantidad = 7 },
            new LineaPedido() { Id = 13, IdPedido = 7,  IdProducto = 1,     Cantidad = 4 },
            new LineaPedido() { Id = 14, IdPedido = 7,  IdProducto = 2,     Cantidad = 10 },
            new LineaPedido() { Id = 15, IdPedido = 7,  IdProducto = 11,    Cantidad = 2 },
            new LineaPedido() { Id = 16, IdPedido = 8,  IdProducto = 12,    Cantidad = 2 },
            new LineaPedido() { Id = 17, IdPedido = 8,  IdProducto = 3,     Cantidad = 9 },
            new LineaPedido() { Id = 18, IdPedido = 9,  IdProducto = 5,     Cantidad = 11 },
            new LineaPedido() { Id = 19, IdPedido = 9,  IdProducto = 6,     Cantidad = 9 },
            new LineaPedido() { Id = 20, IdPedido = 9,  IdProducto = 1,     Cantidad = 4 },
            new LineaPedido() { Id = 21, IdPedido = 10, IdProducto = 2,     Cantidad = 7 },
            new LineaPedido() { Id = 22, IdPedido = 10, IdProducto = 3,     Cantidad = 2 },
            new LineaPedido() { Id = 23, IdPedido = 10, IdProducto = 11,    Cantidad = 10 },
            new LineaPedido() { Id = 24, IdPedido = 11, IdProducto = 12,    Cantidad = 2 },
            new LineaPedido() { Id = 25, IdPedido = 12, IdProducto = 1,     Cantidad = 20 }
        };

        // Propiedades de los elementos privados
        public static List<Cliente> ListaClientes { get { return _listaClientes; } }
        public static List<Producto> ListaProductos { get { return _listaProductos; } }
        public static List<Pedido> ListaPedidos { get { return _listaPedidos; } }
        public static List<LineaPedido> ListaLineasPedido { get { return _listaLineasPedido; } }
    }
}

Con esto ya poseeremos un pequeño set de datos sobre el que operar.

Tipado implícito

Antes de continuar, es hora de añadir un nuevo elemento del que hasta ahora habíamos prescindido y que .NET introdujo a partir del Framework 3.0. Se denomina tipado implícito, y consiste en ignorar el tipo de la variable que declaramos dejando al compilador que lo haga por nosotros, utilizando para ello la palabra reservada var. A diferencia de lo que pueda parecer a simple vista, el uso de esta palabra reservada no implica realizar un tipado débil, ya que el tipado se realiza en tiempo de compilación y no podremos modificar ese tipo a lo largo del desarrollo del programa. No he hablado hasta ahora de esta posibilidad porque no me considero partidario de usarlo extensivamente, ya que según mi opinión, aunque facilita la programación, dificulta su posterior lectura (es mucho más intuitivo para un desarrollador encargado de un mantenimiento encontrarse una variable declarada como Dictionary<String, System.Reflection.TypeInfo> que encontrarse un simple var). Por lo tanto, a ojos del Framework .NET 3.0+, las siguientes líneas de código serían equivalentes:


            Dictionary<String, System.Reflection.MemberInfo> diccionarioMetodos = obtenerDiccionario(o);

            var diccionarioMetodos                                              = obtenerDiccionario(o);

La diferencia, por lo tanto, sería que en el primer caso seríamos nosotros quienes especificaríamos el tipo de diccionarioMetodos, mientras que en el segundo caso sería el compilador el encargado de realizar esta acción a partir del tipo de retorno del método obtenerDiccionario.

El hecho de que en LINQ se use este tipado con frecuencia se debe a que en la mayoría de los casos se trata de un tipo IEnumerable<T>, y el objetivo de este objeto será iterar sobre él. Por ello «levantaremos la mano» en estos ejemplos y haremos uso de var para declarar variables que sean el resultado de una proyección LINQ.

Consultas

Obtener una lista de los objetos a consultar

La forma más sencilla de realizar una consulta sobre LINQ es obtener una lista del elemento que queremos recuperar. Así, si quisiéramos obtener una lista de Clientes, la consulta adecuada para ello sería la siguiente:


    var listaClientes = from c in DataLists.ListaClientes
                                         select c;

La cláusula from de LINQ es más fácil de entender si la interpretamos como si fuera un foreach. De hecho, el siguiente fragmentos de código realizaría más o menos la misma tarea: declarar una variable de tipo Cliente e iterar sobre la lista DataLists.ListaClientes:


            // Declaramos una lista de clientes
            IEnumerable<Cliente> listaClientes = new List<Cliente>();

            // Declaramos una variable de rango "c" que iterará sobre DataLists.ListaClientes
            foreach( Cliente c in DataLists.ListaClientes) // <<-- Equivaldrá a “from c in DataLists.ListaClientes”
            {
                ((List<Cliente>)listaClientes).Add(c);     // <<-- Equivaldrá a “select c”;
            }

Por lo tanto, la razón por la que el from va antes del select es poder declarar la variable de rango c y tener la posibilidad de hacer uso de ella en posteriores cláusulas, como select, orderby

Obtener un campo concreto del objeto

Como acabamos de decir, la sentencia select permite hacer uso de la variable de rango definida en la sentencia from. Por lo tanto, ¿sería posible, en lugar de proyectar un objeto completo, hacer que nuestra lista se componga de los elementos contenidos en un campo específico en lugar de hacerlo con el objeto entero? Definitivamente, sí. Aquí está el ejemplo:


            var listaNombresClientes = from c in DataLists.ListaClientes
                                       select c.Nombre;

Si analizamos el resultado, veremos que no estamos recorriendo un listado de objetos Cliente, sino que la lista contiene simples cadenas de texto, correspondientes

Obtener varios campos del objeto: tipos anónimos

Quizás alguien se esté preguntando ahora: ¿y si quiero obtener más de un campo, digamos el nombre del cliente y su fecha de nacimiento? ¿Podría hacer algo como lo siguiente?:


            var listaNombreFechaClientes = from c in DataLists.ListaClientes
                                           select c.Nombre, c.FechaNac;

Desgraciadamente, no. LINQ se parece a SQL, pero no es SQL. Si queremos recuperar más de un elemento deberemos hacer uso bien de una clase o estructura que declaremos explícitamente para ello (por ejemplo, creando un nuevo Cliente), bien haciendo uso de un tipo anónimo.

El Framework 3.0 relativiza la necesidad de codificar un conjunto de constructores, cada cual con unos parámetros distintos, permitiendo crear un único constructor que aglutine las tareas básicas de inicialización y dejando al programador que añada manualmente las propiedades que estime oportunas a la hora de instanciar el objeto. Así, si quisiéramos devolver los campos «Nombre» y «FechaNac» de la clase «Cliente», podríamos hacer lo siguiente:


            var listaClientesIncompletos = from c in DataLists.ListaClientes
                                           select new Cliente { Nombre = c.Nombre, FechaNac = c.FechaNac };

Fabuloso, ¿verdad? Hemos creado un nuevo objeto Cliente y le hemos asignado valor a sus propiedades Nombre y FechaNac de forma dinámica sin necesidad de un constructor específico que espere esos parámetros. Podemos realizar esta operación con cualquier objeto que disponga de propiedades públicas.

Sin embargo, esto acarrea un problema. Echemos un vistazo al contenido de uno de los objetos pertenecientes a la lista devuelta por la sentencia LINQ:

Hemos instanciado un nuevo objeto de la clase Cliente, pero hacer esto implica que los tipos básicos se inicialicen con su valor por defecto. En este caso, la propiedad entera Id se inicializa con el valor 0, cuando obviamente sabemos que este valor no se corresponde con el Id real de ese campo en nuestra «base de datos». Quizás en nuestro código seamos perfectamente conscientes de este hecho, pero ¿lo sabrá la persona que, en un futuro, tenga que mantener este código?

Descartando utilizar un objeto completo para obtener tan sólo un puñado de campos, nos quedaría la opción de crear una clase específica para alojar estos dos campos. Sin embargo, esta operación puede darse demasiada frecuencia como para tener que crear una clase específica para todas las posibles combinaciones de campos de una lista que queramos recuperar. Esta tarea podría resultar demasiado tediosa. Por ello, el framework nos ofrece un artefacto que solucionará de un plumazo todos los problemas relacionados con esta pequeña disyuntiva: los tipos anónimos.

Un tipo anónimo no es más que un tipo generado de forma dinámica. Fin. No hay más misterio.

Al igual que hacíamos con el «no-constructor» de la clase Cliente pasándole el nombre de las propiedades junto a sus valores a la hora de crearlo, podemos hacer la misma operación sin necesidad de que la clase exista. Hablando en plata, nos inventamos la clase por el camino, y el compilador, a partir del punto en el que hayamos declarado el nuevo tipo, nos permitirá acceder a sus propiedades.

Veámoslo mejor con un ejemplo:


            // Declaramos (y cumplimentamos) el tipo anónimo
            var tipoAnonimo = new { PropiedadEntera = 1, PropiedadCadena = "cadena" };

            // Mostramos por pantalla su contenido
            Console.WriteLine(String.Format("El contenido del tipo anónimo es {0} y {1}",
                tipoAnonimo.PropiedadEntera, tipoAnonimo.PropiedadCadena));

La primera línea crearía un tipo anónimo (nótese que lo declaramos como var) con dos atributos (que se tipan en tiempo de compilación a int y string respectivamente, a partir de los tipos asignados):

  • AnonymousType tipoAnonimo
    • PropiedadEntera (int)
    • PropiedadCadena (string)

De nuevo nos enfrentaremos a la disyuntiva del uso de var en nuestro código: perderemos legibilidad a cambio de ganar versatilidad. Si se pidiera mi opinión, en este caso yo estaría dispuesto a realizar el sacrificio. Por lo tanto, si incluimos un tipo anónimo en la cláusula select de una sentencia LINQ, podríamos hacer lo siguiente:


    var listaNombreFechaClientes = from c in DataLists.ListaClientes
                                   select new { NombreCliente = c.Nombre, FechaNacimiento = c.FechaNac};

Si observamos el contenido del objeto con una inspección, veremos que el listado devuelto será una lista de Anonymous Type compuesto por los campos NombreCliente (string) y FechaNacimiento (DateTime), propiedades que nos acabamos de inventar pertenecientes, también, a un tipo que nos acabamos de inventar.

Si el lector es astuto, puede que piense en un nuevo problema potencial. Hasta ahora, todo muy bonito y muy potente, pero… si el tipo es anónimo y no podemos declarar una clase de su tipo, ¿cómo narices accedemos a sus elementos?

La respuesta puede sonar brusca: «a pelo». Que el tipo sea anónimo no implica que la variable que utilicemos para almacenar los datos no esté fuertemente tipada. Lo único que estamos haciendo es delegar en el compilador la tarea de asignar el tipo, pero una vez que el compilador conoce el tipo del objeto, nos permitirá usarlo sin provocar errores de compilación. Así, podríamos usar el siguiente bucle para acceder a los elementos de la lista devuelta por la sentencia LINQ:


            foreach(var nombreFecha in listaNombreFechaClientes)
                Console.WriteLine(string.Format("El cliente {0} nació el {1}",
                    nombreFecha.NombreCliente, nombreFecha.FechaNacimiento));

El resultado de esta llamada sería el siguiente:

De nuevo, el avispado lector probablemente se haya dado cuenta de otro potencial problema a la hora de utilizar tipos anónimos: ¿y si, en lugar de utilizar la variable que me ha devuelto la sentencia LINQ, quisiera pasar ese listado como parámetro a una función. ¿Cómo nos las arreglamos? Aquí el framework se rinde y pide el cambio, ya que un tipado fuerte es incompatible con esta funcionalidad. Por lo tanto, podemos decir que

  1. Sí, es posible pasar como parámetro un tipo anónimo a una función o método.
  2. A partir de este punto, el framework no se responsabiliza de los posibles daños ocasionados por tal osadía.

Lo que queremos decir es que mientras nos movamos dentro de un único ámbito o scope (por ejemplo, dentro de una única función), el framework será capaz de inferir el tipo de cada uno de los elementos anónimos que tengamos a bien generar. Sin embargo, en el momento en el que creemos una función y ésta reciba un parámetro de tipo anónimo, el framework se lavará las manos. El compilador, en un alarde de magnanimidad, nos permitirá que invoquemos a tantos métodos y propiedades del tipo anónimo, pero dejando a nuestra responsabilidad el hecho de que los elementos invocados existan y que sus parámetros y formatos sean correctos. Entramos en el terreno del tipado débil.

Por lo tanto, si quisiéramos generar una función que reciba una lista de elementos anónimos y realizara la misma operación que vimos más arriba, bastaría con lo siguiente:


        private static void mostrarResultados(IEnumerable<dynamic> listado)
        {
            foreach (var nombreFecha in listado)
                Console.WriteLine(string.Format("El cliente {0} nació el {1}",
                    nombreFecha.NombreCliente, nombreFecha.FechaNacimiento));
        }

Vemos que el parámetro es de tipo IEnumerable<dynamic>. En realidad bastaría con pasar un parámetro de tipo dynamic, que se correspondería con el tipo anónimo en sí, pero dado que sabemos que vamos a recibir una colección de tipos anónimos, seamos amables e indiquémosle a nuestro compilador esta información, ahorrándole unos cuantos ciclos de CPU a la máquina que realizará la compilación y algún que otro dolor de cabeza al colega que tendrá que encargarse en un futuro de mantener nuestra chapuza nuestro código.

Si desde la función original invocamos el método pasándole la consulta generada por LINQ en el ejemplo anterior, todo funcionará como la seda, ya que los nombres y tipos de las propiedades de nuestro tipo anónimo (NombreCliente (string) y FechaNacimiento (DateTime)) coinciden por las que espera nuestro método.

Pero… ¿y si no es así? ¿Y si en lugar de recibir un campo NombreCliente recibimos un campo llamado Nombre a secas? ¿Qué ocurriría? La respuesta es fácil de imaginar: Pum.

Por lo tanto, tal y como aconsejan las autoridades sanitarias con el alcohol, podemos decir lo mismo con los tipos anónimos: sí, pero consúmalos con responsabilidad. No permita que un tercero se vea perjudicado por no tomar las medidas oportunas.

Más adelante seguiremos con LINQ, mostrando cómo podemos aplicar filtros, ordenaciones y agrupaciones.

5 comentarios

  1. Hola, en el primer caso de consultas donde pones el ejemplo del foreach para recorrer ListaClientes….. Se pudiera hacer eso mismo si ListaClientes fuera una clase de modelo de datos de Entity Framwork en bd? Se pudiera utilizar por ejemplo:

    foreach( Clientes c in clientesContext.ListaClientes.toList() )
    {
    // hacer lo deseado con los objetos de cada cliente
    }

    Por lo demas muy buenos estos post.
    Saludos desde Cuba.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s