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:
1 2 |
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:
1 |
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:
1 |
var consulta =
DataLists.ListaProductos.Skip(8); |
En el
resultado, como podemos observar, no se mostrarán los elementos con ID del 1 al
8.
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:
1 2 3 4 |
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:
1 |
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.
1 |
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:
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 |
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:
1 |
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:
1 |
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:
1 2 3 |
// 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:
1 2 3 4 5 6 7 |
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.
1 2 3 4 5 6 7 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
1 2 |
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.