Sentencias en LINQ (IV): Particionado. Delegados y expresiones lambda.


El particionado es una característica que nos permite recuperar conjuntos concretos de una consulta a partir de un índice. Su funcionamiento sería similar al filtrado de no ser porque no hace uso de los datos en sí, sino del orden en el que éstos se almacenan.

Los métodos de particionado más útiles son los siguientes:

Take(n)

La sentencia Take devolverá los n primeros registros de la consulta. Así, si realizamos la siguiente consulta:

            var consulta = (from producto in DataLists.ListaProductos
                           select producto).Take(5);

Obtendremos los cinco primeros elementos de la consulta. Como podemos observar, el funcionamiento de este método es bastante sencillo.

Como punto a destacar: no se trata de una sentencia, sino de un método de la interfaz IEnumerable. Por tanto, puede aplicarse directamente a una lista sin necesidad de realizar la consulta LINQ que acabamos de hacer ahora mismo:

            var consulta = DataLists.ListaProductos.Take(5);

Skip(n)

Si Take indica a nuestro conjunto de datos que quiere un número concreto, Skip le dice a partir de qué elemento ha de empezar a recuperar. Nuevamente se trata de un método de IEnumerable, por lo que lo aplicaremos directamente a una lista.

Si quisiéramos recuperar los productos sin tener en cuenta los ocho primeros, escribiríamos lo siguiente:

            var consulta = DataLists.ListaProductos.Skip(8);

En el resultado, como podemos observar, no se mostrarán los elementos con ID del 1 al 8.

Paginación mediante Take y Skip

Estos dos métodos se crearon con un objetivo claro: facilitar el proceso de paginación en los listados de elementos. Hasta la aparición de LINQ, en .NET no quedaba otro remedio que realizar la paginación a mano, bien recuperando todos los elementos potenciales de la consulta e ir mostrándolos poco a poco a través del control adecuado (lo cual provocaba terribles consecuencias a nivel de sobrecarga de servidor y rendimiento del cliente), bien recuperando los elementos poco a poco (lo cual implicaba codificar un algoritmo de recuperación que emulase el funcionamiento de Take y Skip).

El algoritmo genérico para realizar la paginación es el siguiente:

  • Ignorar los registros hasta (página actual -1) * tamaño de página
  • Recuperar los (tamaño de página) registros siguientes

Por lo tanto, de forma genérica, si quisiéramos paginar un listado, podríamos crear la siguiente función que lo hiciera por nosotros:

        public static IEnumerable<object> paginar(IEnumerable<object> lista, int paginaActual, int tamanoPagina)
        {
            return lista.Skip((paginaActual - 1) * tamanoPagina).Take(tamanoPagina);
        }

Esto partiendo de la base de que la primera página es “1”. En caso de que nuestra paginación comenzara por la página “0”, cambiaríamos ligeramente el algoritmo (no teniendo que restarle una unidad a la página actual a la hora de calcular el salto):

        public static IEnumerable<object> paginar(IEnumerable<object> lista, int paginaActual, int tamanoPagina)
        {
            return lista.Skip(paginaActual * tamanoPagina).Take(tamanoPagina);
        }

Por lo tanto, si establecemos un tamaño de página de 5 e invocamos la función como se muestra a continuación

            var consulta = paginar(DataLists.ListaProductos, 1, 5);
            var consulta = paginar(DataLists.ListaProductos, 2, 5);
            var consulta = paginar(DataLists.ListaProductos, 3, 5);

El resultado será, respectivamente:

Aunque nos adelantemos un poco a la parte de LINQ a SQL, no viene de más apuntar que, si tratamos con una fuente de datos SQL, el tipo a recuperar será IQueryable en lugar de IEnumerable.

TakeWhile(Func<bool, TSource>). Delegados y expresiones lambda.

Si el método Take(n) nos devolvía los n primeros elementos, el método TakeWhile(Func<bool, TSource>) devolverá todos los elementos que cumplan la condición del método. Sin embargo, hay algo extraño en el parámetro que toma el método. ¿Cómo le paso una función al método? ¿Invocando un método en el parámetro? No. Lo que en realidad necesitaremos pasarle a este método será un delegado.

Un delegado es lo que en C/C++ sería un puntero a función. Se trata de un artefacto que encapsula una referencia a un método, pudiendo a partir de ese momento ser pasado a otro método que a su vez puede referenciar la función sin conocer en tiempo de ejecución qué método será invocado.

A diferencia de en C y C++, los delegados son objetos. Son, además, la base de los eventos.

Para declarar un delegado es preciso (en realidad no es realmente necesario, pero servirá para nuestro ejemplo), en primer lugar, declarar una función a referenciar. Por ejemplo:

        private bool masBaratoDeTresEuros(Producto producto)
        {
            return producto.Precio < 3;
        }

Si ahora quisiéramos declarar un delegado de esta clase, bastaría con declarar un objeto de la clase Func<tipoParametro1, tipoParametro2, …, tipoParametroN, tipoRetorno>, es decir, con los tipos de cada uno de los parámetros en primer lugar y dejando para el final el tipo de retorno. En nuestro caso en particular:

        private Func<Producto, bool> delegado = masBaratoDeTresEuros;

Ahora ya podremos usar este delegado como parámetro del método TakeWhile(). Únicamente podremos usarlo sobre una lista de productos, ya que el tipo del parámetro de entrada del delegado debe ser igual al del tipo del listado que lo invoca. Si lo invocamos desde un listado de objetos de la clase Producto, el delegado pasado como parámetro debe recibir un parámetro de tipo Producto.

            var consulta = DataLists.ListaProductos.TakeWhile(delegado);

Esto hará que durante la iteración sobre los elementos de la lista se invoque el método apuntado por el delegado (masBaratoDeTresEuros) y que el delegado devuelva un booleano indicando si se cumple o no la condición (en este caso, verdadero si producto.Precio < 3 y falso en caso contrario. La llamada devolverá la siguiente salida:

Es posible, además, forzar la invocación del delegado pasado como parámetro mediante el método invoke. Así, imaginemos que tenemos la siguiente función que calcula la diferencia de precio de dos productos y un delegado que apunta a ella:

       private static float diferenciaPrecio(Producto p1, Producto p2)
        {
            return p1.Precio - p2.Precio;
        }

        public static Func<Producto, Producto, float> delegadoDiferencia = diferenciaPrecio;

Además, supongamos que tenemos una función que permite recibir como parámetro de entrada un delegado de estas características y que se encargue de realizar la invocación y mostrar el resultado por pantalla:

        public static void funcionRecibeDelegado(Func<Producto, Producto, float> f)
        {
            float diferencia = f.Invoke(DataLists.ListaProductos[0], DataLists.ListaProductos[1]);
            Console.WriteLine("La diferencia de precio es: " + diferencia);
        }
        funcionRecibeDelegado(delegadoDiferencia);

Como vemos, el método invoke equivale, precisamente, a invocar la función de manera nativa. Recibirá los mismos parámetros que la función original y devolverá el mismo tipo de resultado. La salida por pantalla será la siguiente:

No es necesario crear una referencia para pasar un delegado a una función. Ésta puede recibir directamente el nombre de la función referenciada, por lo que el siguiente código también sería válido:

        funcionRecibeDelegado(diferenciaPrecio);

En realidad, cuando utilicemos las funciones de extensión de LINQ apenas veremos delegados en la práctica. Están pensadas especialmente para ser usadas con expresiones lambda. De momento nos basta saber que el método TakeWhile() obtendrá todos aquellos registros que cumplan la condición del método pasado entre paréntesis.

Si anteriormente vimos la conveniencia de utilizar tipos anónimos, las expresiones lambda es el equivalente funcional. Una expresión lambda es una función anónima utilizada principalmente para la creación de delegados.

A continuación mostraré la expresión lambda equivalente a la función que hemos creado. Basta decir que el primer elemento (antes del operador =>, que no es una comparación igual o mayor, sino que significa “se dirige a”) simboliza el parámetro. El segundo parámetro, el código a evaluar:

    var consulta = DataLists.ListaProductos.TakeWhile(producto => producto.Precio < 3);

En este caso, la expresión lambda podría leerse como “el parámetro de entrada producto se pasa a una función anónima que devolverá true si producto.Precio < 3“.

Supongo que a estas alturas entenderemos el porqué de las expresiones lambda. Imaginemos tener que crear un método (y su delegado) por cada una de las comparaciones a realizar en una aplicación que haga uso de LINQ. Sería absurdo. Por ello, este tipo de expresiones permiten crear funciones “de usar y tirar” que no necesitan declaración.

Por lo tanto, la definición de una expresión lambda será:

  • Parámetros de entrada => Expresión a evaluar

Sobra decir que los parámetros de entrada pueden ser múltiples (en el ejemplo actual sólo hemos mostrado uno). La sintaxis para utilizar más de un parámetro dentro de la expresión sería la siguiente:

            // Recibe los parámetros x e y.
            // Devuelve como resultado x elevado a y
            (x, y) => Math.Pow(y);

Funciones, acciones y expresiones Lambda

Una función es un método que devuelve un valor. Un procedimiento es un método que no devuelve ningún valor. Hasta aquí nada nuevo, ¿verdad? A la hora de tratar con métodos anónimos, nos encontraremos con una clasificación parecida:

  • Func<parametros>: método anónimo que devuelve un valor
  • Action<parametros>: método anónimo que no devuelve ningún valor

Dado que las expresiones lambda se consideran delegados, sería perfectamente posible declarar una referencia de este tipo y asignarle una expresión lambda:


        Action accionSinParametros = () => Console.WriteLine("Sin retorno");
        Action<int> accionUnParametro = entero => Console.WriteLine((entero + 1));
        Action<int, string> accionDosParametros = (entero, cadena) => Console.WriteLine("Entero {0}\tCadena {1}", entero, cadena);

        Func<int>       funcionDevolverUno = () => 1;                   // DevolveraUno
        Func<int, int> funcionDevolverMasUno = entero => entero + 1;    // Devolverá entero + 1
        Func<int, int, string> funcionDevolverSumaCadena = (x, y) => "El valor de la suma es " + (x + y);   // Devolverá la cadena con la suma

Como observamos, al declarar un delegado de tipo Func<>, el último de los tipos declarados (obligatorio) será el del valor de retorno, siendo el resto de tipos los de los parámetros que tomará como entrada. En el caso del último elemento, recibirá dos parámetros enteros y devolverá una cadena de texto.

Invocar los métodos será tan sencillo como hacerlo con un método tradicional.

            accionSinParametros();
            accionUnParametro(2);
            accionDosParametros(3, "Hola, mundo");

            int i = funcionDevolverUno();
            int j = funcionDevolverMasUno(1);
            string retorno = funcionDevolverSumaCadena(i, j);

SkipWhile(Func<bool, TSource>)

Comienza a recuperar elementos a partir del primero que cumpla con la propiedad. En este caso, dado que tenemos los siguientes productos:

        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 }
        };

Vemos que el primer producto que no cumple la condición (precio < 3) es aquel con Id = 4, cuyo valor es mayor de 3 (3.55). Por lo tanto, si hacemos uso de SkipWhile con esta condición, recuperará todos los elementos a partir del cuarto.

            var consultaLambda   = DataLists.ListaProductos.SkipWhile(producto => producto.Precio < 3);
            var consultaDelegado = DataLists.ListaProductos.SkipWhile(delegado);

Nuevamente, podríamos realizar la consulta haciendo uso de un delegado o de una expresión lambda, como hemos podido ver en el código anterior.

Y con esto concluimos la introducción a las extensiones LINQ para realizar operaciones de particionado. Continuaremos con otras operaciones como ordenación y agregación, en las que profundizaremos un poco más en las expresiones lambda, ampliamente utilizadas en LINQ.

Anuncios

Responder

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. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s