Arboles de expresión y reflexión estática
Arboles de expresión
Un árbol de expresión es una expresión lambda sin compilar. Cuando utilizamos un árbol de expresión, el código de la expresión lambda se mantiene en memoria en forma de estructura de árbol binario. Se almacenan las partes de la expresión y las operaciones entre las partes por separado en una estructura de árbol. En este estructura los nodos representan operadores y las hojas representan valores. De esta forma es posible representar cualquier operación como una jerarquía. Veamos un ejemplo de árbol de expresión binario muy simple:En el ejemplo, si partimos de las hojas hacia la raíz nos encontramos primero con los valores 20 y 5 cuyo nodo común es el operador suma, por lo que el operador podría sustituirse por el valor resultado de la suma: 25.
A continuación tendríamos dos hojas con los valores 25 y 10 con un nodo común con la operación multiplicar, por lo que el resultado de ambos será de 25 multiplicado por 10: 250. Al ser la raíz del árbol este sería el valor del resultado de la expresión.
Las expresiones suelen ser mucho más complejas que en el ejemplo y en vez de valores constantes podemos tener variables y llamadas a métodos. También tendremos en el árbol una rama que representará los parámetros de entrada de la expresión.
Veamos ahora un ejemplo de una expresión lambda y su árbol de expresión equivalente. Como se puede observar, para invocar la función del árbol de expresión es necesario compilarla primero para que devuelva su delegado equivalente. El resultado de la invocación es el mismo en ambos casos:
Aunque ambas expresiones representan la misma función, la primera de ellas se compila en tiempo de compilación y no guarda ninguna información de la expresión origen. Se limita a ejecutar la función para la que ha sido creada. Se trata simplemente de un delegado que apunta a un método. Sin embargo, la segunda guarda la expresión tal cual, es decir, podemos estudiar por separado cada nodo y cada hoja de la expresión en tiempo de ejecución. También podemos compilar la expresión en tiempo de ejecución e invocarla como si fuera una expresión lambda normal.
Es muy habitual que un método nos solicite un árbol de expresión como parámetro y que la expresión nunca sea compilada ni ejecutada. En estos casos el parámetro tiene el único propósito de informar en detalle cómo está estructurada la función. Más adelante veremos algunos ejemplos donde se entenderá mejor este aspecto.
Mientras una expresión lambda ejecuta código, un árbol de expresión describe lo que hace ese código.
Reflexión estática
Un árbol de expresión tiene la ventaja de poder ser analizada miembro a miembro utilizando la reflexión estática. Veamos cómo convierte el compilador un árbol de expresión en una estructura de datos.En el siguiente ejemplo se muestra un árbol de expresión y a continuación la forma en que el compilador la descompondrá en miembros de una estructura. En realidad las dos formas son válidas para declarar un árbol de expresión, sin embargo, la forma más rápida e intuitiva es la primera donde se utiliza la sintaxis de expresión lambda: [parámetros] => [cuerpo de la expresión]. La segunda forma nos ofrecería la capacidad de "montar al vuelo" una función con estructura de árbol binario, permitiendo crear métodos dinámicos de una forma muy simple.
La expresión anterior se compone de distintos miembros formando la siguiente estructura de árbol:
De forma inversa, a partir de un árbol de expresión, podemos descomponerla en sus miembros individuales y analizar cada miembro por separado.
A esta forma de análisis, muy similar a la reflexión dinámica que utilizamos para el análisis de tipos, se le llama reflexión estática. El siguiente código realiza una simple inspección de la expresión
EsVip
utilizada en el ejemplo anterior: Resultado: Tipo expression: Lambda
Tipo de volor devulto: System.Boolean
Nombre parámetro: c
Tipo parámetro: Clientes
Expresión izda: c.Volumen
Expresión dcha: 20000
Tipo de nodo: GreaterThan_
Ventajas y usos de la reflexión estática
La reflexión estática recopila información inspeccionando un árbol de expresión. La reflexión dinámica permite obtener información de los ensamblados y los tipos definidos dentro de ellos.La principal ventaja que ofrece la reflexión estática es que utiliza código tipado. De esta forma nos aseguramos tener siempre un código válido. Por el contrario, la reflexión dinámica utiliza cadenas de texto "magic strings" que dan pie a errores de tipografía. Además, la reflexión estática permite utilizar Intellisense de Visual Studio para mayor productividad y comodidad así como realizar refactorizaciones automáticas sin preocuparnos de romper el código.
Examinemos los dos tipos de reflexión con el siguiente ejemplo que simplemente comprueba si una propiedad está marcada o no con el atributo
ObsoleteAttribute
:En el primer método necesitamos pasar un "magic string" ya que el método emplea reflexión dinámica. Además, al pasarle un simple
string
, no tiene información relativa al objeto sobre el que queremos examinar la propiedad. Podemos solucionarlo añadiendo un parámetro más con el objeto a examinar o, tal y como hemos hecho en el ejemplo, utilizando un método genérico donde el tipo genérico establece el tipo de objeto.El segundo método acepta un árbol de expresión, ofreciendo un tipado seguro. En este caso estamos pasando en un mismo parámetro toda la información necesaria para examinar la propiedad.
Este sería el código que podría usarse dentro de cada uno de los métodos anteriores:
Ejemplos de uso de las expresiones de árbol
Proveedores de consultas
Tanto Linq to SQL como Linq to Entities (Entity Framework) necesitan transformar expresiones descritas en código .Net a su expresión equivalente en SQL antes de enviar la consulta al servidor. Examinemos por ejemplo la siguiente expresión utilizada en Entity Framework sobre el métodoWhere
donde podemos especificar un predicado para obtener una colección filtrada de una entidad: El método Where
acepta un árbol de expresión con un delegado de tipo Func
con un parámetro de entrada del tipo de la entidad sobre la que estamos invocando el Where
, en nuestro caso Clientes
, y con un parámetro de salida de tipo booleano. Entity Framework nos devolverá los clientes cuya función devuelva un valor true. ¿Cómo es capaz Entity Framework de generar la consulta SQL necesaria para cualquier función que nosotros le indiquemos? La respuesta está en el árbol de expresión. Entity Framework no necesita compilar ni ejecutar la expresión que le hemos pasado. Lo que necesita es convertir los miembros descritos en la expresión en una cadena de consulta propia del lenguaje SQL. El caso anterior lo que hace es convertir el árbol de expresión en la cosulta: El siguiente código, al añadir el método
Select
y realizar una proyección sobre un sólo campo, la consulta que utiliza Entity Framework se simplifica:La librería estándar de Linq trabaja con colecciones que implementan la interfaz
IEnumerable
, y Linq to SQL y Linq to Entities trabajan con colecciones que implementan la interfaz IQuereyable
. A su vez, los métodos de extensión de
IEnumerable
esperan parámetros delegados y los métodos de extensión de IQuereyable
esperan parámetros de expresiones de árbol.Estas dos son las firmas del método
Where
de una colección IEnumerable
y de una colección IQuereyable
. Como se puede apreciar son equivalentes: Así como IEnumerable
garantiza que una colección puede ser tratada como tal e iterada en un bucle foreach
, la interfaz IQuereyable
, además de implementar también la interfaz IEnumerable
, es capaz de realizar consultas por medio de un objeto Provider
proporcionado por la interfaz. El proveedor examinará el árbol de expresión y deberá "traducirla" a su consulta equivalente comprensible por el proveedor. Métodos de extensión de HtmlHelper en ASP Net MVC
Observemos las siguientes líneas de código muy habituales en vistas razor para ASP Net MVC:@Html
es un objeto de tipo HtmlHelper
que nos ayuda a generar código Html en una vista razor. Este objeto cuenta con una serie de métodos de extensión que aceptan parámetros de tipo árbol de expresión como por ejemplo: DisplayNameFor
, DisplayFor
, CheckBoxFor
, HiddenFor
, DisplayForModel
, etc. Estos métodos, además de ofrecernos un tipado fuerte sin opción a errores, proporcionan al objeto HtmlHelper
una información extra en el árbol de expresión. Además de poder compilar e invocar el método para obtener el valor de la propiedad
Nombre
del modelo, a través del árbol de expresión, analizará tanto el parámetro de entrada (model
) como la propiedad seleccionada en el cuerpo de la expresión (model.Nombre
). De esta manera será capaz de analizar por reflexión los atributos especificados en las propiedades del objeto Cliente.
Esto es muy útil para la representación en la vista de las etiquetas, sus valores e incluso para añadir al código Html todos los atributos necesarios de tipo data-* para la validación de campos de los formularios. ASP Net MVC, junto a la librería de jQuery Unobstrusive Validation ofrecen funcionalidad para la representación de los atributos necesarios para realizar la validación en el cliente.
Veamos un ejemplo con el siguiente modelo y vista de
Cliente
: Gracias a los métodos DisplayNameFor
y DisplayFor
, que aceptan un árbol de expresión como parámetro, obtenemos una representación como la siguiente:Como se puede observar se ha utilizado el atributo
DisplayName
del modelo para la representación de las etiquetas de cada campo.Sin embargo, lo mejor viene para las vistas de edición donde tenemos un formulario con entrada de datos. Continuando con el modelo
Cliente
del mismo ejemplo, veamos la siguiente vista de edición: Gracias a los atributos utilizados en el modelo (DisplayName
, Required
, Range
...) y los métodos de extensión LabelFor
, EditorFor
y ValidationMessageFor
, los cuales aceptan un árbol de expresión como parámetro, obtenemos el código Html necesario para la representación de las etiquetas, la validación en cliente y los textos de error producidos en la validación.Este sería el código generado de forma totalmente automática para el campo de texto de IdCliente:
Cuando el usuario pulse sobre el botón para enviar el formulario, la librería jQuery Unobstrusive Validation se encargará de analizar los datos introducidos con ayuda de los atributos data-* proporcionados.
Si queremos personalizar la forma en que se representa el código Html recomiendo implementar nuestro propio método de extensión para que se pueda usar desde la vista. Podemos personalizar nuestros propios atributos y aplicar lógica adicional para moldear la representación final. Incluso podemos hacer que envuelva a uno de los métodos proporcionados por ASP Net MVC y decorarlo con nuestra lógica.
El siguiente ejemplo muestra un método de extensión del objeto
HtmlHelper
en el que comprobamos la existencia del atributo personalizado AlineamientoAttribute
en la propiedad de nuestro modelo y según el valor de su propiedad Alinamiento
añadimos una clase css distinta al código Html generado:El atributo personalizado podría ser como el siguiente:
Written on December 2, 2017