Firmas invitadas |
Los árboles de expresiones en C# 3.0
Publicado el 06/Ene/2007
|
1. IntroducciónEl fin de año ha estado más atareado de lo previsto, lo que ha demorado la aparición de esta segunda entrega para elguille.info. Pero, por fin, ¡aquí está! En nuestra entrega anterior [4] presentamos las expresiones lambda de C# 3.0 y vimos cómo éstas tienen dos facetas diferentes de representación y utilización estrechamente relacionadas entre sí: como código (en forma de métodos anónimos, bloques de código ILAsm directamente ejecutables) y como datos (en forma de árboles de expresiones, estructuras de datos capaces de representar de una manera eficiente el algoritmo de evaluación de la expresión). En aquella ocasión hicimos un mayor énfasis en la primera faceta, y esta vez nos concentrarnos en los árboles de expresiones.
2. Los árboles de expresiones como representación de expresionesComo ya hemos visto, las expresiones lambda se compilan como código o como datos en dependencia del contexto en que se utilizan. Por ejemplo, si se asigna una expresión lambda (como ejemplo hemos utilizado una que implementa la conocida fórmula para calcular el área de un círculo) a una variable de tipo delegado: Func<double, double> circleArea = (radius) => Math.PI * radius * radius; el compilador generará en línea el código ILAsm correspondiente, de modo que la definición anterior es equivalente a la asignación del siguiente método anónimo: Func<double, double> circleArea = delegate (double radius) { return Math.PI * radius * radius; }; Pero si asignamos la expresión lambda a una variable del tipo genérico Expression, el compilador no la traducirá a código ejecutable, sino que generará una estructura de datos en memoria que representa a la expresión en sí. Estas estructuras de datos se denominan en C# 3.0 árboles de expresiones. Continuando con el ejemplo anterior, si utilizamos la expresión lambda que calcula el área de un círculo de la siguiente forma: static Expression<Func<double, double>> circleAreaExpr = (radius) => Math.PI * radius * radius; lo que estamos expresando equivale a la siguiente secuencia de asignaciones: static ParameterExpression parm = Expression.Parameter(typeof(double), "radius"); static Expression<Func<double, double>> circleAreaExpr = Expression.Lambda<Func<double, double>>( Expression.Multiply( Expression.Constant(Math.PI), Expression.Multiply( parm, parm)), parm);
Mediante la primera de las dos asignaciones anteriores se crea un objeto de la clase ParameterExpression que representa al único parámetro (variable, hablando en términos matemáticos) de la expresión lambda (función, de nuevo utilizando la jerga de las matemáticas). En la segunda sentencia, por otra parte, es donde realmente se construye el árbol de expresión correspondiente a la expresión lambda, utilizando el objeto-parámetro obtenido en el paso anterior. Observe el estilo de programación funcional que se aplica a la hora de definir mediante código un árbol de expresión – un estilo que se hará más común con la aparición de C# 3.0 y LINQ (muy particularmente LINQ To XML, la extensión de LINQ para el trabajo con documentos XML). Una vez que se ha construido un árbol de expresión, éste puede ser manipulado como cualquier otro objeto .NET – modificado, seriado para su almacenamiento o transmisión a través de la red, etc. En particular, la clase Expression ofrece el mecanismo necesario para compilar un árbol de expresión al código ILAsm necesario para su evaluación: Func<double, double> area = circleAreaExpr.Compile(); Console.WriteLine("The area of a circle with radius 5 is " + area(5));
3. La jerarquía de clases de expresionesLas clases necesarias para el trabajo con árboles de expresiones están implementadas en el ensamblado System.Query.dll, y su espacio de nombres es System.Expressions (esto puede cambiar en un futuro). Lo primero a señalar es que hay dos clases llamadas Expression: la genérica que hemos utilizado anteriormente y otra no genérica en la que ésta se apoya y que es el verdadero “caballo de batalla” en el que descansa todo el mecanismo de representación de expresiones. La primera es de más alto nivel e impone el mecanismo de fuerte control de tipos necesario para, por ejemplo, compilar un árbol de expresión a un delegado anónimo. La segunda es más relajada en lo que a control de tipos se refiere y se apoya más en características como la reflexión para ofrecer una mayor libertad de implementación. Normalmente, un objeto de la primera clase se construye a partir de una instancia de la segunda. La versión no genérica de Expression es similar a las típicas clases que se obtienen al representar estructuras de datos recursivas. Es en sí misma una clase abstracta, y de ella hereda toda una serie de clases tanto abstractas como “concretas” mediante las cuales se representa a los diversos tipos de elementos que pueden aparecer en una expresión. Algunas de las principales clases descendientes de Expression se listan en la siguiente tabla.
Lo más destacable de estas subclases de Expression es que sus constructores no son públicos; por esta razón, para instanciarlas es necesario hacer uso de los métodos-fábrica estáticos incluidos en la propia clase Expression, como se desprende de la sentencia que construye el árbol correspondiente al área del círculo. Estos métodos fábrica, por supuesto, incluyen uno o más parámetros de tipo Expression, para permitir el anidamiento recursivo de expresiones. Como otro ejemplo más complejo, vea cómo se construiría el árbol de expresión correspondiente a la hipotenusa de un triángulo rectángulo, que introdujimos en nuestra entrega anterior: static ParameterExpression px = Expression.Parameter(typeof(double), "x"); static ParameterExpression py = Expression.Parameter(typeof(double), "y"); static ParameterExpression[] parms = { px, py }; static Expression<Func<double, double, double>> hypotenuseExpr2 = Expression.Lambda<Func<double, double, double>>( Expression.Call( typeof(Math).GetMethod("Sqrt"), null, new Expression[] { Expression.Add( Expression.Multiply(px, px), Expression.Multiply(py, py)) }), parms); static void Main(string[] args) { Console.WriteLine(hypotenuseExpr2); // prints '(x, y) => Sqrt(Add(Multiply(x, x), Multiply(y, y)))' Func<double, double, double> hypo = hypotenuseExpr2.Compile(); Console.WriteLine("Hypotenuse(3, 4) = " + hypo(3, 4)); }
4. El ejemplo perfectoEn mi opinión, el ejemplo perfecto para mostrar las posibilidades que ofrecen los árboles de expresiones de C# 3.0 consiste en desarrollar un intérprete de expresiones matemáticas. Un intérprete de expresiones es una aplicación que recibe del usuario una cadena de caracteres que representa una expresión matemática (como 2 * sin(x) * cos(x), por ejemplo) y la traduce a una representación interna que permite la posterior evaluación de la expresión para diferentes valores de las variables (x en este caso). Un intérprete de expresiones basado en los árboles de expresiones de C# 3.0 utilizaría los árboles de expresiones para la representación interna de las mismas. Este artículo no incluye una implementación de un intérprete así, simplemente por el hecho de que alguien se me adelantó. Mientras yo me dedicaba a otras tareas (mucho menos interesantes, se lo aseguro :-), mi colega MVP Bart De Smet [5] ha hecho una implementación excelente en una serie de entradas en su blog, que pueden servir como complemento a este artículo y que recomiendo sinceramente.
5. ConclusionesLos árboles de expresiones son otra de las principales novedades que incluirá la próxima versión 3.0 de C#. En este artículo hemos visto cómo los árboles de expresiones permiten representar como datos de una manera eficiente las expresiones lambda (funciones, en última instancia), y cómo estos árboles pueden ser transformados en código y evaluados en el momento oportuno. En una próxima entrega describiremos con más detalle cómo se utiliza esta posibilidad en LINQ (y específicamente en LINQ To SQL) para posponer la evaluación de una expresión de consulta de LINQ hasta el preciso instante en que se dispone de toda la información necesaria para su óptima ejecución. El código fuente del ejemplo utilizado en el artículo está disponible para su descarga. Para poder compilarlo y ejecutarlo satisfactoriamente, se deberá instalar inicialmente la Presentación Preliminar de LINQ de Mayo de 2006, disponible en [1].
6. Referencias
|
Código de ejemplo (ZIP): |
Fichero con el código de ejemplo:
octavio_Arboles.zip - 10.70 KB
|