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

18 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 primer post intentamos describir qué eran las expresiones lambda, resumiendo muy brevemente su utilidad, así como los aspectos sintácticos de este tipo de construcciones del lenguaje. El segundo lo enfocamos a explicar el papel de las lambda como funciones anónimas, su estrecha relación con los delegados, y su forma de utilización.

En esta tercera y última entrega de la serie, vamos a centrarnos en otra de las grandes utilidades de las expresiones lambda en .NET framework: la definición de árboles de expresión.

Las lambda como árboles de expresión

Árbol de expresiónLos compiladores de C# y VB.NET pueden, bajo determinadas circunstancias, utilizar las lambdas para crear un árbol de expresión, una estructura en memoria que representa en forma de árbol las operaciones a realizar, y en el orden que hay que hacerlo, para lograr un objetivo. Si lo queréis ver más claro, observad el diagrama adjunto, en el que se muestra el árbol de expresión correspondiente a una función lambda que obtiene la media de dos números.

Esto es muy diferente al caso anterior, donde hablábamos de las lambda como funciones ejecutables que podían ser referenciadas por delegados e invocadas de forma directa. Entonces las expresiones lambda eran transformadas en tiempo de compilación en código ejecutable (de hecho, en métodos estáticos que pueden ser consultados usando Reflector u otros desensambladores), y por ello podíamos utilizarlas de forma directa.

En el caso de los árboles de expresión, la compilación no genera código ejecutable correspondiente a las instrucciones definidas en la lambda, sino el código para crear y llenar el árbol con dicha expresión. Después, en tiempo de ejecución, será posible recorrer dicho árbol, analizarlo, seriarlo para almacenarlo o moverlo a otras capas, e incluso compilarlo para poder lanzar su ejecución.

Pero paremos aquí un momento... según lo dicho, cuando el compilador se encuentra con una expresión lambda, por ejemplo x => x * 2, puede optar por generar una función anónima o por generar el código de llenado del árbol, ¿cómo sabe lo que debe hacer?

Pues bien, el compilador elegirá la opción adecuada dependiendo del tipo de la referencia a la expresión lambda. Si se trata de un tipo de delegado como Func o Action, generará código ejecutable y las referencias a la función serán tratados como delegados; si se usa el tipo Expression, que veremos más adelante, se generará el árbol de expresión y las referencias no se considerarán delegados, sino objetos de este tipo. Por este motivo no podemos utilizar para declarar lambdas variables locales de tipo implícito: el compilador no sabría qué hacer.

var dobla = a => a * 2; // Error: no se puede asignar
// una expresión lambda a una variable
// local de tipo implícito.

 

Definición de árboles con expresiones lambda

Para definir un árbol de expresión a partir de una lambda debemos utilizar el tipo genérico Expression<TipoDelegado>, siendo TipoDelegado un delegado como Action o Func de los descritos en el post anterior. ¿Que esto os parece confuso? Pues esperad a ver el ejemplo... ;-)
  Expression<Func<int, int, int>> media = (a, b) => (a + b) / 2;
 
En realidad, aparte de lo difícil de leer que es una declaración de este tipo, la idea es bastante sencilla. Lo que estamos indicando al compilador es que queremos montar un árbol de una expresión de la que conocemos el tipo de los parámetros de entrada y del valor de retorno, datos que indicamos mediante el delegado que utilizamos como argumento genérico de la clase Expression, la porción que he marcado de amarillo en el ejemplo anterior. Además, este delegado lo utilizaremos cuando queramos convertir el árbol de expresión en código ejecutable, más adelante veremos cómo.

Para que quede claro, ahí van algunos ejemplos más:
  // Árbol de expresión de una función que determina
// si el parámetro que le llega es par

Expression<Func<int, bool>> esPar = a => a%2==0;

// Árbol de expresión de una función
// que retorna el máximo de dos números.

Expression<Func<int, int, int>> maximo =
(a, b) => a > b ? a:b;

// Árbol de expresión de una función que
// retorna un string transformado

Expression<Func<string, string>> TrimMays =
a => a.Trim().ToUpper();

// Árbol de expresión de una acción
// sin parámetros que escribe por consola la fecha y hora.

Expression<Action> accion =
() => Console.WriteLine(DateTime.Now);
 
Fijaos que el tipo de delegado Action o Func, que son los parámetros genéricos de la expresión, son los que definen los tipos de parámetros y retornos de la lambda. En el primer caso, el Func<int, bool> define que la función recibe un entero y retorna un booleano; en el último caso, ni se envía ni se recibe nada, de ahí que utilicemos un delegado Action.

Un detalle importante, antes de que se me olvide comentarlo. Los árboles de expresión sirven, como su nombre indica, para almacenar expresiones. Esto limita el tipo de construcciones de código que podemos usar en las lambdas que los definen: no está permitido utilizar asignaciones, bloques con llaves { }, ni bucles... debe tratarse de una expresión que pueda representarse en una jerarquía de árbol. Si intentamos saltarnos estas restricciones, el compilador generará un error, aunque el tipo de delegado sea correcto y la función lambda también, como en el siguiente ejemplo:
  // Suma los n primeros naturales
Expression<Func<int, int>> expr =
n => {
int t = 0;
for (int i = 1; i <= n; t += i++) ;
return t;
};


// Error CS0834: Una expresión lambda con cuerpo no puede ser
// convertida a árbol de expresión

 
Árbol de expresiónVolvemos un poco atrás ahora para recordar cuando comentaba que la compilación de una lambda no genera código ejecutable correspondiente a las instrucciones definidas en la expresión, sino el código para llenar el árbol de expresión que la representa.

¿Y cómo hace eso? Muy sencillo. El compilador analiza la expresión, genera la secuencia de instrucciones que compone el árbol, las compila, e introduce el resultado en el ensamblado. El siguiente ejemplo muestra cómo crear un árbol de expresión usando una lambda, y una aproximación al código equivalente que genera el compilador, para que nos podamos hacer una idea del trabajo que nos ahorra esta característica del lenguaje:

// Árbol definido con una lambda:
Expression<Func<int, bool>> esPar = a => a % 2 == 0;

// Y el mismo árbol definido a mano,
// lo que genera el compilador automáticamente:
ParameterExpression param = Expression.Parameter(typeof(int), "a");
ConstantExpression dos = Expression.Constant(2, typeof(int));
ConstantExpression cero = Expression.Constant(0, typeof(int));
BinaryExpression modulo = Expression.Modulo(param, dos);
BinaryExpression comparacion = Expression.Equal(modulo, cero);
Expression<Func<int, bool>> esPar2 =
Expression.Lambda<Func<int, bool>>(comparacion, param);
 

Uso de los árboles de expresión

Ya sabemos qué son los árboles de expresión y cómo podemos definirlos, pero aún no le hemos visto sentido a su existencia. Pero lo tiene, vaya si lo tiene ;-)

En primer lugar, el hecho de poder definir el árbol partiendo de una expresión lambda, además de comodidad a la hora de codificar, nos permite aprovechar el tipado fuerte y la potencia del intellisense para evitar errores. En el ejemplo anterior, las probabilidades de que nos equivoquemos creando el árbol de forma manual son muy superiores a que ocurra si utilizamos la sintaxis lambda.

Segundo, fijaos que en ningún momento se está generando código IL o compilando la expresión representada por la lambda. Estamos creando una estructura en memoria. Esto quiere decir que después podemos procesar esta información como estimemos conveniente; podemos, por ejemplo, analizar su contenido, recorrerlo, seriarlo, o transformarlo, en función de nuestras necesidades. Es ideal, por tanto, cuando tengamos interés en interpretar una expresión para realizar alguna acción con ella.

Vamos a ver ahora varios ejemplos para ilustrar el uso de las lambdas y árboles de expresión en el mundo real.

Linq

En el caso de Linq aplicado a un proveedor externo de datos relacional (por ejemplo, las tecnologías Linq to SQL, o Entity Framework), no tiene interés alguno el código ejecutable asociado a una expresión, sino su estructura, pues al final va a ser traducida al lenguaje o tecnología del almacén de información. Supongamos la siguiente consulta para obtener los productos que comienzan por "S":
  // Usando una consulta Linq:
var datos = from p in productos
where p.Nombre.StartsWith("S")
select p;

// O su equivalente usando
// operadores de consulta:

var datos = productos.Where(p=>p.Nombre.StartsWith("S"));

 
La lambda resaltada servirá para crear un árbol de expresión con las condiciones indicadas, pues el método de extensión Where aplicado acepta un predicado de tipo Expression. No se genera ningún método anónimo para la expresión lambda, ni se traduce a IL su contenido: sólo interesa para definir la expresión que será introducida en el árbol. Más adelante, en el momento de extraer realmente la información desde el almacén correspondiente, el componente proveedor de datos recorrerá y analizará la estructura en memoria, generando su equivalente en SQL, que es lo que lanzará al SGBD para obtener los datos.

Por cierto, existen en la actualidad una gran cantidad de proveedores de Linq, capaces de transformar los árboles de expresión en consultas a casi cualquier tipo de almacén. Fijaos que la posibilidad de separar la codificación lambda de la interpretación de la expresión hace posible su utilización en una gran variedad de ámbitos.

Cálculo simbólico

Otro ejemplo que ilustra muy bien las posibilidades de los árboles de expresión, de mano del maestro Octavio Hernández. Se trata del artículo Cálculo simbólico en C# 3.0, publicado en la web de El Guille a principios de 2007.

A lo largo del artículo se realiza la implementación básica de un sistema de cálculo de derivadas de funciones matemáticas partiendo de un árbol de expresiones. El proceso, que el autor va explicando paso a paso, consiste en analizar el árbol, e ir generando otro árbol con el resultado de la derivación de cada expresión encontrada. A continuación se muestra la porción de código donde se realiza la derivación de una operación de suma, utilizando recursividad para derivar además cada uno de los sumandos:
  private static Expression Derive(this Expression e, string paramName)
{
switch (e.NodeType)
{
[...]
// sum rule
case ExpressionType.Add:
{
Expression dleft =
((BinaryExpression) e).Left.Derive(paramName);
Expression dright =
((BinaryExpression) e).Right.Derive(paramName);
return Expression.Add(dleft, dright);
}
[...]
}
}
 
Al final, fijaos que de nuevo no nos interesa en absoluto la lambda como función anónima ni delegado, sino la estructura de la propia expresión, de forma que podamos recorrerla y transformarla en otra expresión, en este caso la función derivada de la original. El siguiente ejemplo muestra el uso de esta clase:
   Expression<Func<double, double>> 
funcion = x => x*x; // f(x)=x^2

Expression<Func<double, double>>
derivada = funcion.Derive(); // f'(x)=2*x

Console.WriteLine(derivada); // Muestra la función derivada:
// x => ((x * 1) + (1 * x))

 
Creedme, vale la pena echarle un vistazo.

ASP.NET MVC

Un último ejemplo, que demuestra la versatilidad del uso de lambdas y árboles de expresión en multitud de escenarios. En el framework ASP.NET MVC, es posible crear enlaces hacia acciones desde dentro de las vistas (páginas .ASPX), que no son sino métodos dentro de unas clases concretas llamadas "controladores". El caso es que para hacer referencia a un método del controlador, pueden utilizarse estas dos vías, de resultado idéntico:
  // Obtiene un enlace al método "ChangePassword"
// de la clase "AccountController":

Html.ActionLink("Cambiar clave", "ChangePassword", "Account")

// Lo mismo, pero usando un árbol de expresión:
Html.ActionLink<AccountController>
(acc=>acc.ChangePassword(), "Cambiar clave")
 
Aunque el resultado es el mismo, la segunda usa un árbol de expresión para realizar la referencia al método ChangePassword de la clase AccountController. La implementación del método ActionLink recorre el árbol generado desde la expresión lambda para obtener el nombre del controlador y del método, por lo que es equivalente a primera fórmula, pero beneficiándose de las ventajas del tipado fuerte y del intellisense en la edición.

Los árboles de expresión como código ejecutable

Hasta ahora siempre me he referido a los árboles de expresión como entidades de almacenamiento. De alguna u otra forma, estaba equiparando su utilidad a la cualquier estructura de datos que permitiera guardar y procesar información, lo cual es cierto pero sólo parcialmente.

Los árboles de expresión aportan una característica adicional: se pueden convertir en código ejecutable. Es decir, es posible compilar un árbol de expresión en tiempo de ejecución, dando lugar a una función anónima a la que podemos tener acceso, es decir, invocarla, a través de sus delegados.

Veámoslo con un caso concreto. Retomando el ejemplo de obtención de derivadas, sería perfectamente posible obtener el valor de la función derivada en un punto ampliando ligeramente el código visto anteriormente:
  Expression<Func<double, double>> 
funcion = x => x*x; // f(x)=x^2

Expression<Func<double, double>>
derivada = funcion.Derive(); // f'(x)=2*x

// Compilamos la función derivada
// y obtenemos un delegado a la misma:

Func<double, double> funcDerivada = derivada.Compile();

double result = funcDerivada(6); // Obtenemos el valor de la función
// invocando al delegado con x=6

Console.WriteLine(result); // Muestra "12"
 
Como se puede observar en el código, la llamada al método Compile() devuelve un delegado del tipo Func indicado en el parámetro genérico de la expresión, que apunta hacia la función anónima creada "al vuelo" a partir de las expresiones contenidas en el árbol. Es decir, desde la definición simbólica contenida en el árbol ¡obtenemos código ejecutable!

Aunque espectacular, en realidad no hay nada mágico en esta característica: se trata de ir recorriendo el árbol y emitiendo el código IL correspondiente a cada expresión, que está perfectamente tipificada y definida. En el namespace System.Linq.Expressions existe una bonita clase interna llamada ExpressionCompiler que se dedica exclusivamente a ello, utilizando herramientas suministradas por System.Reflection.Emit, como los generadores de lenguaje intermedio ILGenerator.

¡Y hasta aquí hemos llegado!

A lo largo de estos tres posts hemos recorrido las principales características y utilidades de las expresiones lambda. Obviamente, han quedado cosas por detrás; no era objetivo de esta serie profundizar demasiado, sino ofrecer una visión suficiente para animar a los desarrolladores a utilizar esta potente característica que nos ofrece C# (y VB.NET).

Espero que os haya sido una lectura útil, al menos tanto como me ha resultado a mí su escritura. Ah, y para consultas, sugerencias o puntualizaciones, por aquí me tenéis.

Publicado en: www.variablenotfound.com.

7 Comentarios:

Anónimo dijo...

Enhorabuena por la "trilogía". Muy buenos los tres artículos que, sin duda alguna, contribuirán a hacer menos temible el enfrentarse a las expresiones lambda, tan al uso últimamente (al menos de forma especialmente visible) en la plataforma .NET

Saludos.

josé M. Aguilar dijo...

Gracias, Miguel!

Espero al menos haber aportado mi granito de arena para ello... :-)

Gerson Mayen dijo...

Excelentes artículos, he usado expresiones lambda por varios meses y gracias a estas lecturas por fin comprendo su contendo y ya imagino usarlas para otros aspectos mas que para simplemente con linq.

Felicitaciones.!!

elperucho dijo...

Que buena lectura, mas que una buena lectura dira que es muy buen articulo. Para los neofitos como yo que nos quedamos unos años atras con .net 2.0

Arturo dijo...

Hola José.

Muchas pero muchas gracias.

Realmente me ha aclarado un poco mas las expresiones lamba sobre todo para usarse como arboles de expresión.

Te felicito por tu blog. Variable no encontrada esta en mis páginas favoritas y lo tengo en mi lector de feed.

Saludos

Makanudo dijo...

Campeón, tu "Trilogía del Lambda" es espectacular. Unos artículos muy amenos, concisos, directos al concepto y muy comprensibles. Quedan guardados en mi disco duro.

Aldo dijo...

Estimado José, agradezco mucho el trabajo que has realizado en tu blog. Excelente explicación de expresiones Lambda, desarrollas los contenidos de una manera muy didáctica. No sabes como me he reído con eso de los "genéricos" y como me ha ayudado a entender este tema. Deberías pensar en escribir un libro!