Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
miércoles, 1 de abril de 2009
LambdaEn el post anterior intentamos realizar una primera aproximación a las expresiones lambda, centrándonos en obtener una definición lo suficientemente cercana, que nos permitiera conocer a grandes rasgos qué son, así como en describir su forma general y sus particularidades sintácticas.

En esta segunda entrega vamos a profundizar un poco en el papel de las expresiones lambda como vía para definir muy rápidamente funciones anónimas y los tipos de delegados con los que podemos referenciarlas, y por tanto, invocarlas.

Ya en el tercer post describiremos el papel de las expresiones lambda como herramienta de generación de árboles de expresión.

Las lambdas como funciones anónimas

Como habíamos insinuado anteriormente, uno de los usos de las expresiones lambda es permitir la definición "en línea" de funciones anónimas. De hecho, en tiempo de compilación las expresiones lambda son convertidas en métodos a los que el compilador establece un nombre único autogenerado, como los ejemplos mostrados a continuación:

Transformación de lamdas en métodos
Las referencias a estas funciones anónimas son transformadas en delegados (punteros) a las mismas, lo que nos permitirá, por ejemplo, invocarlas desde el código. En la práctica esto quiere decir que podemos asignar una lambda a una variable y ejecutarla como muestra el siguiente pseudocódigo:

delegado duplica = x => x * 2;
escribe duplica(2); // Escribe un 4
 
En este primer acercamiento, fijaos que duplica es el nombre del delegado, la función definida en forma de expresión lambda no tiene nombre, será el compilador el que se asigne uno.

Veamos cómo se concreta esta idea en C#. En el siguiente código, la variable duplica apunta hacia una función anónima definida a través de la expresión lambda en cuya implementación lo único que se hace es retornar el doble del valor que le llega como parámetro. Vemos también cómo podemos utilizarla de forma directa:
  Func<int, int> duplica = x => x * 2;
int result = duplica(7); // result vale 14
 
Sólo con objeto de que podáis entender el código anterior, os adelantaré que la porción Func<int, int> es una forma rápida de tipificar el delegado, indicando que duplica apunta a una función que espera un entero como parámetro de entrada, y que su valor de retorno será otro entero. Esto lo veremos dentro de un momento.

De la misma forma que asignamos la expresión lambda a una variable, podemos hacerlo también para indicar el valor de un parámetro a un método que acepte un delegado concreto. Por ejemplo, el siguiente código muestra un método llamado calcula que recibe un valor entero y una referencia a una función, retornando el resultado de efectuar dicha operación sobre el entero proporcionado:
  // Es método ejecuta la función indicada por
// el parámetro operacion, enviándole el valor especificado,
// y retorna el resultado obtenido de la misma.

public int calcula(int valor, Func<int, int> operacion)
{
return operacion(valor); // retorna el resultado de aplicar la
// expresión indicada al valor.

}

// Usos posibles:
int i = calcula(4, x => x / 2); // Le pasamos una referencia a la
// función que estamos definiendo sobre
// la marcha. El resultado es que i=2.


int j = calcula(4, duplica); // Le pasamos la variable "duplica",
// que es una referencia a la lambda
// definida anteriormente. J valdrá 8.

 
<HistoriaDelAbuelete>
Seguro que a los más viejos del lugar esto le recuerda a los Codeblocks que utilizábamos en Clipper a principios de los 90 (uuf, cómo pasa el tiempo...). ¿Todavía reconocéis el siguiente código?
  bDuplica := { |n| n*2 }
? EVAL(bDuplica, 7) // Muestra un 14
 </HistoriaDelAbuelete>

Una consecuencia directa de que las expresiones lambdas sean referenciadas a través de delegados es que podemos utilizarlas en cualquier sitio donde se acepte un delegado, con la única precaución de escribirla teniendo en cuenta el tipo de su retorno y los parámetros que recibe. Un ejemplo claro lo tenemos en la suscripción a eventos, donde la técnica habitual consiste en utilizar un delegado a un método en el que se implementa la lógica del tratamiento de los mismos, algo como:
  // Nos suscribimos al evento MouseMove:
this.MouseMove += new MouseEventHandler(this.Form1_MouseMove);
[...]

// Tratamiento del evento MouseMove:
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
this.Text = e.X + "," + e.Y;
}
 
Como sabemos, podemos suscribirnos al evento MouseMove añadiéndole delegados del tipo MouseEventHandler, definido en System.Windows.Forms, cuya firma indica que recibe un parámetro de tipo object, otro de tipo MouseEventArgs y no retorna ningún valor, exactamente igual que sería un delegado anónimo (C# 2.0) escrito así:
  this.MouseMove += delegate(object sender, MouseEventArgs args)
{
this.Text = args.X + "," + args.Y;
};
 
Y dado que las lambdas pueden sustituir de forma directa a cualquier delegado, podemos utilizarlas para conseguir un código más compacto:
  this.MouseMove += (sender, args) => {
this.Text = args.X + "," + args.Y;
};
 
Llegados a este punto es conveniente aclarar que las expresiones lambda son características introducidas en los lenguajes, y por tanto en sus compiladores, pero no en la plataforma de ejecución (CLR) en sí. Por tanto, todo lo descrito hasta el momento era posible realizarlo antes que las lambda aparecieran por el horizonte, aunque de forma un poco más tediosa, utilizando mecanismos que la versión 2.0 del framework ponía a nuestra disposición, como los delegados y métodos anónimos. En este sentido, el uso de expresiones lambda aportan mucha simplicidad, elegancia y legibilidad al código.

Esto explica, además, que Visual Studio 2008 sea capaz de generar código para .NET 2.0 a partir de código fuente C# 3.0.

Tipos de delegados de expresiones lambda

Antes ya había adelantado que la definición Func<int, int> era simplemente una forma de indicar el tipo del parámetro que recibía la función lambda, así como el tipo del valor de retorno. En realidad, lo único que estábamos haciendo era definir, de forma muy sencilla y rápida, el delegado hacia la función.

Vamos a concretar esto un poco más, pero antes de continuar, una cosa: si para tí un genérico es un tipo de medicamento, mejor que leas algo sobre el tema antes de continuar, pues en caso contrario es posible que te pierdas un poco ;-). Pues probar leyendo una introducción a los generics en c#, o la Guía de programación de C#.

.NET Framework ofrece en el espacio de nombres System un conjunto de definiciones de genéricas de delegados para que podamos utilizarlos para "apuntar" hacia las funciones definidas mediante expresiones lambda, llamados Action y Func.

Utilizaremos los tipos Func para definir referencias a expresiones lambda que retornen un valor, o sea, funciones. De ahí su nombre. Los tipos Action, en cambio, están destinados a referenciar a lambdas que realicen acciones y que no retornen ningún valor. De ahí su nombre también. ;-)

Una de estas definiciones es la que habíamos usado en un ejemplo anterior:
  Func<int, int> duplica = x => x * 2;
 
Como se puede observar, al tratarse de una referencia a una función que retorna un valor, hemos utilizado un tipo Func con dos parámetros genéricos, que corresponde con la siguiente declaración existente en el espacio de nombres System:
  public delegate TResult Func<T, TResult>(T arg);
 
Por ello, cuando declarábamos que la variable duplica era del tipo Func<int, int>, lo que indicábamos era, en primer lugar que el parámetro que necesitaba la lambda era un int, y que ésta nos devolvería también un int, es decir, lo mismo que si hubiéramos definido duplica así, utilizando métodos anónimos de C# 2.0:
  // En el área de declaraciones:
public delegate int Duplicador(int arg);
...
// En el código:
Duplicador duplica = delegate(int k) { return k*2 };
 
Obviamente, la sintaxis lambda es mucho más compacta y expresiva.

En la práctica, lo único que tenemos que tener claro a la hora de referenciar una función lambda es el tipo de cada uno de los parámetros que usa, y el tipo de retorno. Estos se introducen, en ese orden, en los parámetros genéricos de la clase Func y listo. Como esto debe quedar claro, ahí van unos ejemplos de definición y uso:
  // Recibe un entero y retorna un booleano:
Func<int, bool> esPar = x => x%2==0;
Console.WriteLine(esPar(2)); // Muestra "True"

// Recibe dos enteros, retorna otro entero:
Func<int, int, int> suma = (a,b) => a+b;
Console.WriteLine(suma(2,3)); // Muestra "5"

// No recibe nada, retorna un texto:
Func<string> hora = () => "Son las "
+ DateTime.Now.ToShortTimeString();
Console.WriteLine(hora()); // Muestra "Son las 14:21:10"

 
Es importante saber que en el framework están definidos los delegados Func<tipo1, tipo2..., tipoResult> para funciones de hasta cuatro parámetros. Si necesitamos más deberemos definir los delegados a mano, aunque esto es realmente sencillo utilizando una de las declaraciones existentes y añadiéndole el número de parámetros que deseemos. Por ejemplo, para seis parámetros la definición del genérico sería algo así como:
  public delegate 
TResult Func<T1, T2, T3, T4, T5, T6, TResult>
(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5, T6 p6);
 
Pero ahora aparece un pequeño problema: las funciones sin retorno no pueden referenciarse con delegados de tipo Func, puesto que el framework .NET no soporta la instanciación de tipos genéricos utilizando parámetros void (ECMA 335, sección 9.4, pág. 153). Por tanto, no podríamos declarar un delegado como Func<int, void> para apuntar hacia una función que recibe un entero y no devuelve nada. Si lo pensáis un poco, este es el motivo de que no exista ninguna sobrecarga de la clase Func sin parámetros genéricos, pues como mínimo debemos indicar el tipo del valor de retorno.

La clave para cubrir estos casos se encuentra en el tipo Action. Como comentaba unas líneas más arriba, el objeto de estos tipos de delegados es apuntar a expresiones lambda que realicen acciones y que no retornen ningún valor, por lo que sus parámetros genéricos describirán exclusivamente los tipos de los parámetros de la función. En este caso, como es obvio, sí existe una clase no parametrizada Action para apuntar a funciones sin parámetros, además de disponer de genéricos que cubren las acciones de hasta cuatro parámetros. Veamos unos ejemplos:
  // Acción sin parámetros (no genérica):
Action saluda = () => Console.WriteLine("hola");
saluda(); // Muestra "hola";

// Acción que recibe un string
Action<string> apaga = motivo => {
log(motivo);
shutdown();
};
apaga("mantenimiento"); // Apaga el sistema
 
Por último, me parece interesante recordar algo que había comentado en el post anterior, que en las expresiones lambda no era necesario indicar el tipo de los parámetros ni del retorno porque el compilador los infería del contexto. Como podemos ver, lo tiene bastante fácil, puesto que simplemente debe tomar la definición del delegado para conocerlos; por eso no es necesario introducir redundancias como las siguientes:
  Func<int, int> duplica = (int a) => (int)(a * 2); // ¡Redundante!

// Forma más cómoda:

Func<int, int> duplica = a => a * 2; // Ok!
 
En cualquier caso, si por algún motivo es necesario utilizar la forma explícita, sabed que no se permite hacerlo de forma parcial, es decir, o le ponéis los tipos a todo, o no se los ponéis a nada.

Y hasta aquí esta segunda entrega. En el siguiente post, el último de la serie, estudiaremos el uso de las lambda como herramientas de definición de árboles de expresión.

Por supuesto, para cualquier duda o sugerencia, ya sabéis dónde encontrarme. :-)

Publicado en: www.variablenotfound.com.

8 Comentarios:

LgEaObNrAiReDlO dijo...

Excelente José María como siempre, muchas gracias.

josé M. Aguilar dijo...

Gracias a tí, Leonardo, por tus comentarios.

Un saludo.

AcP dijo...

Me mataste con la referencia a Clipper, yo estaba pensando exactamente lo mismo antes de leer el recuadro... ah, viejos tiempos... Summer 87 y Clipper 5... les decíamos "macros" y lo bueno era que eran cadenas, y se podían almacenar en la base de datos para hacer unos sistemas realmente insufribles en el mantenimiento.
Otra que me acuerdo es que, como cadenas que eran, se almacenaban literalmente en el ejecutable (se interpretaban, no compilaban), así que uno podía cambiarlas con un editor y listo! Programa parchado en producción en menos de lo que canta un gallo.
Ah... claro... después me enteré que por hacer ese tipo de cosas me decían "pibe... no hagás macanas".

Tanks por el post, que me ha venido bien para hacer un repaso, que esto de trabajar todos los días con el 2.0 a veces hace que nos olvidemos que en otros proyectos tenemos estos chiches disponibles.

josé M. Aguilar dijo...

Hola, Andrés.

Jejeje, percibo cierta nostalgia en tus palabras ;-)

Clipper dejó huella en los que tuvimos la suerte de conocerlo, lástima que la evolución lo echara de la carretera tan pronto y que nadie supiera darle una continuidad razonable.

Gracias por comentar. :-)

Juan Carlos Heredia Mayer dijo...

Exelente artículo, me ha servido mucho para decir la tecnología correcta a usar en un determinado proyecto Web.

Saludos,

elperucho dijo...

JAjaja.. no soy el unico viejecito que se da golpes de cabeza con estas cosases bueno saber que existen otros

Bueno articulo. la verdad trae mucha nostalgia con Clipper. recurdo que tenia el libro de summer '87 era blaco con listas o rayas verdes..

focl2003 dijo...

Muchas gracias por este excelente artículo, me gustaría hacer un pequeño comentario con respecto a estos temas maravillosos que nos explicas, primero que nada he notado que estas herramientas están basadas en ideas y no son herramientas para tomar y empezar a trabajar de manera inmediata, por lo que mi recomendación sería que explicaras un poco más los beneficios de delegar un método, el poder extra de las expresiones lambda contra los delegados anónimos y las ventajas de estos ante los métodos normales, muchas veces nos sentimos confundidos porque sabemos que la solución de halla también de una forma anterior y no es inmediatamente obvio el beneficio de estas nuevas tecnologías.
Creo sinceramente que invertir en hacer evidente las ventajas no está de más, un ejemplo que a mí me aclaró mucho con respecto a los delegados son los métodos de devolución de llamada que utilizan algunas apis de windows, proponer el preguntar a los lectores ¿qué pasaría si no tuvieramos un delegado como tipo evento en un tipo personalizado? ¿cómo lo solucionarías?, creo que ahí las ventajas se apreciarían más rápido.
He disfrutado enormemente leyendo estos artículos.
Muchas gracias.

josé M. Aguilar dijo...

Hola, focl2003.

A día de hoy, más de dos años después de escribir el post, te puedo asegurar que las lambdas son bastante útiles y se han vuelto algo cotidiano.

Por un lado, la sintaxis funcional lambda es mucho más cómoda a la hora de escribir delegados. De hecho, hace ya bastante que no utilizo delegados tradicionales, todo lo he sustituido por lambdas (cualquier delegado puede ser expresado como lambda).

Pero donde realmente se aprecia su potencia como especificación de árboles de expresión en tecnologías como LINQ.

Desde luego esto da para un post más que para un comentario rápido...

Gracias por comentar!