Compartir a través de


Árboles de expresión: datos que definen código

Un árbol de expresión es una estructura de datos que define el código. Los árboles de expresión se basan en las mismas estructuras que un compilador usa para analizar código y generar la salida compilada. A medida que lea este artículo, observará bastante similitud entre árboles de expresión y los tipos usados en las API de Roslyn para compilar analizadores y codefijos. (Los analizadores y CodeFixes son paquetes NuGet que realizan análisis estáticos en el código y sugieren posibles correcciones para un desarrollador). Los conceptos son similares y el resultado final es una estructura de datos que permite examinar el código fuente de forma significativa. Sin embargo, los árboles de expresión se basan en un conjunto diferente de clases y API que las API de Roslyn. Esta es una línea de código:

var sum = 1 + 2;

Si analiza el código anterior como un árbol de expresiones, el árbol contiene varios nodos. El nodo más externo es una instrucción de declaración de variable con asignación (var sum = 1 + 2;). Ese nodo más externo contiene varios nodos hijos: una declaración de variable, un operador de asignación y una expresión que representa el lado derecho del signo igual. Esa expresión se subdivide aún más en expresiones que representan la operación de suma, así como en operandos izquierdo y derecho de dicha suma.

Vamos a profundizar un poco más en las expresiones que constituyen el lado derecho del signo igual. La expresión es 1 + 2, una expresión binaria. Más concretamente, es una expresión de suma binaria. Una expresión de adición binaria tiene dos elementos secundarios, que representan los nodos izquierdo y derecho de la expresión de suma. Aquí, ambos nodos son expresiones constantes: el operando izquierdo es el valor 1y el operando derecho es el valor 2.

Visualmente, toda la instrucción es un árbol: puede empezar en el nodo raíz y viajar a cada nodo del árbol para ver el código que constituye la instrucción:

  • Instrucción de declaración de variable con asignación (var sum = 1 + 2;)
    • Declaración de tipo de variable implícita (var sum)
      • Palabra clave var implícita (var)
      • Declaración de nombre de variable (sum)
    • Operador de asignación (=)
    • Expresión de suma binaria (1 + 2)
      • Operando izquierdo (1)
      • Operador de suma (+)
      • Operando derecho (2)

El árbol anterior puede parecer complicado, pero es muy poderoso. Después del mismo proceso, descompone expresiones mucho más complicadas. Tenga en cuenta esta expresión:

var finalAnswer = this.SecretSauceFunction(
    currentState.createInterimResult(), currentState.createSecondValue(1, 2),
    decisionServer.considerFinalOptions("hello")) +
    MoreSecretSauce('A', DateTime.Now, true);

La expresión anterior también es una declaración de variable con una asignación. En este caso, el lado derecho de la asignación es un árbol mucho más complicado. No va a descomponer esta expresión, pero tenga en cuenta cuáles podrían ser los distintos nodos. Hay llamadas de método que usan el objeto actual como receptor, uno que tiene un receptor explícito this , uno que no lo hace. Hay llamadas de método mediante otros objetos receptores, hay argumentos constantes de diferentes tipos. Y por último, hay un operador de suma binaria. Según el tipo de valor devuelto de SecretSauceFunction() o MoreSecretSauce(), ese operador binario de suma puede ser una llamada de método a un operador de suma invalidado, que se resuelva en una llamada de método estático al operador binario de suma definido para una clase.

A pesar de esta complejidad percibida, la expresión anterior crea una estructura de árbol que navega tan fácilmente como la primera muestra. Siga recorriendo los nodos secundarios para buscar nodos hoja en la expresión. Los nodos padre tienen referencias a sus hijos, y cada nodo tiene una propiedad que describe qué tipo de nodo es.

La estructura de un árbol de expresión es muy coherente. Una vez que haya aprendido los conceptos básicos, comprende incluso el código más complejo cuando se representa como un árbol de expresiones. La elegancia de la estructura de datos explica cómo el compilador de C# analiza los programas de C# más complejos y crea una salida adecuada a partir de ese código fuente complicado.

Una vez que esté familiarizado con la estructura de árboles de expresión, encontrará que el conocimiento que ha adquirido rápidamente le permite trabajar con muchos escenarios más avanzados. Los árboles de expresiones ofrecen posibilidades increíbles.

Además de traducir algoritmos para que se ejecuten en otros entornos, los árboles de expresión facilitan la escritura de algoritmos que inspeccionan el código antes de ejecutarlo. Escribe un método cuyos argumentos son expresiones y, a continuación, examina esas expresiones antes de ejecutar el código. El árbol de expresiones es una representación completa del código: verá valores de cualquier subexpresión. Ves nombres de métodos y propiedades. Verá el valor de cualquier expresión constante. Convierte un árbol de expresión en un delegado ejecutable y ejecuta el código.

Las API de árboles de expresión permiten crear árboles que representan casi cualquier construcción de código válida. Sin embargo, para mantener las cosas lo más simples posible, algunas expresiones de C# no se pueden crear en un árbol de expresiones. Un ejemplo es el uso de expresiones asincrónicas mediante las palabras clave async y await. Si sus necesidades requieren algoritmos asincrónicos, tendría que manipular los Task objetos directamente, en lugar de confiar en la compatibilidad del compilador. Otro consiste en crear bucles. Normalmente, estos bucles se crean mediante for, foreach, while o do. Como verá más adelante en esta serie, las API para árboles de expresión admiten una expresión de bucle única, con break expresiones y continue que controlan la repetición del bucle.

Lo único que no se puede hacer es modificar un árbol de expresiones. Los árboles de expresión son estructuras de datos inmutables. Si desea mutar (cambiar) un árbol de expresiones, debe crear un nuevo árbol que sea una copia del original, pero con los cambios deseados.