Compartir a través de


Crear árboles de expresión

El compilador de C# creó todos los árboles de expresión que ha visto hasta ahora. Ha creado una expresión lambda asignada a una variable de tipo Expression<Func<T>> o un tipo similar. En muchos escenarios, se crea una expresión en memoria en tiempo de ejecución.

Los árboles de expresión son inmutables. Inmutable significa que debe crear el árbol desde las hojas hasta la raíz. Las API que se usan para compilar árboles de expresión reflejan este hecho: los métodos que se usan para compilar un nodo toman todos sus elementos secundarios como argumentos. Veamos algunos ejemplos para mostrar las técnicas.

Creación de nodos

Comience con la expresión de adición con la que ha estado trabajando en estas secciones:

Expression<Func<int>> sum = () => 1 + 2;

Para crear ese árbol de expresión, primero cree los nodos hoja. Los nodos hoja son constantes. Use el Constant método para crear los nodos:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

A continuación, compile la expresión de adición:

var addition = Expression.Add(one, two);

Una vez que haya compilado la expresión de adición, cree la expresión lambda:

var lambda = Expression.Lambda(addition);

Esta expresión lambda no contiene argumentos. Más adelante en esta sección, verá cómo asignar argumentos a parámetros y crear expresiones más complicadas.

Para las expresiones como esta, puede combinar todas las llamadas en una sola instrucción:

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

Creación de un árbol

En la sección anterior se muestran los conceptos básicos de la creación de un árbol de expresión en memoria. Los árboles más complejos suelen significar más tipos de nodo y más nodos en el árbol. Vamos a ejecutar un ejemplo más y se muestran dos tipos de nodo más que normalmente se compilan al crear árboles de expresión: los nodos de argumento y los nodos de llamada de método. Vamos a crear un árbol de expresiones para crear esta expresión:

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

Para empezar, cree expresiones de parámetro para x y y:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

La creación de las expresiones de multiplicación y suma sigue el patrón que ya ha visto:

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

A continuación, debe crear una expresión de llamada de método para la llamada a Math.Sqrt.

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

La GetMethod llamada podría devolverse null si no se encuentra el método . Lo más probable es que se deba a que ha escrito mal el nombre del método. De otro modo, podría significar que el ensamblado necesario no se carga. Por último, coloque la llamada al método en una expresión lambda y asegúrese de definir los argumentos en la expresión lambda:

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

En este ejemplo más complicado, verá un par de técnicas más que a menudo necesita para crear árboles de expresión.

En primer lugar, debe crear los objetos que representan parámetros o variables locales antes de usarlos. Una vez creados esos objetos, puede usarlos en el árbol de expresiones siempre que necesite.

En segundo lugar, debe usar un subconjunto de las APIs de reflexión para crear un System.Reflection.MethodInfo objeto y así crear un árbol de expresiones para acceder a ese método. Debe limitarse al subconjunto de las API de reflexión que están disponibles en la plataforma de .NET Core. De nuevo, estas técnicas se extienden a otros árboles de expresión.

Desarrollar código en profundidad

No está limitado en lo que puede compilar mediante estas API. Sin embargo, cuanto más complicado sea el árbol de expresiones que desee construir, más difícil será administrar y leer el código.

Vamos a crear un árbol de expresiones equivalente a este código:

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

El código anterior no ha compilado el árbol de expresiones, sino simplemente el delegado. Con la clase Expression no puede crear expresiones lambda de instrucción. Este es el código necesario para crear la misma funcionalidad. No hay una API para crear un while bucle, sino que debe crear un bucle que contenga una prueba condicional y un destino de etiqueta para interrumpir el bucle.

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

El código para construir el árbol de expresiones para la función factorial es bastante más largo, más complicado, y está plagado de etiquetas, instrucciones break y otros elementos que nos gustaría evitar en nuestras tareas de codificación diarias.

En esta sección, ha escrito código para visitar todos los nodos de este árbol de expresiones y escribir información sobre los nodos que se crean en este ejemplo. Puede ver o descargar el código de ejemplo en el repositorio de GitHub dotnet/docs. Experimente por sí mismo mediante la compilación y ejecución de los ejemplos.

Asignación de construcciones de código a expresiones

En el ejemplo de código siguiente se muestra un árbol de expresiones que representa la expresión num => num < 5 lambda mediante la API.

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

La API de árboles de expresión también admite asignaciones y expresiones de flujo de control, como bucles, bloques condicionales y try-catch bloques. Mediante la API, puede crear árboles de expresión más complejos que los que el compilador de C# puede crear a partir de expresiones lambda. En el ejemplo siguiente se muestra cómo crear un árbol de expresión que calcula el factorial de un número.

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Para obtener más información, vea Generar métodos dinámicos con árboles de expresión en Visual Studio 2010, que también se aplica a versiones posteriores de Visual Studio.