Share via


Creación de á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 asignaba a una variable de tipo Expression<Func<T>> o de algún tipo similar. En muchos escenarios, 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 usa para crear los árboles de expresión reflejan este hecho: los métodos que usa para crear un nodo toman todos sus elementos secundarios como argumentos. Veamos algunos ejemplos para mostrarle las técnicas.

Creación de nodos

Empezaremos 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 método Constant para crear los nodos:

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

Después, cree la expresión de adición:

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

Una vez que haya creado la expresión de adición, se crea la expresión lambda:

var lambda = Expression.Lambda(addition);

Esta expresión lambda no contiene argumentos. Posteriormente 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 implican normalmente más tipos de nodo y más nodos en el árbol. Vamos a analizar un ejemplo más y a mostrar dos tipos de nodo que crea normalmente al crear árboles de expresión: los nodos de argumentos y los nodos de llamada al método. Vamos a crear un árbol de expresión para crear esta expresión:

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

Comienza creando 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 expresiones de adición y multiplicación 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);

Después, necesita crear una expresión de llamada al 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 llamada GetMethod podría devolver 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 dicha expresión:

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

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

Primero, necesita crear los objetos que representan parámetros o variables locales antes de usarlos. Una vez que haya creado esos objetos, puede usarlos en su árbol de expresión siempre que los necesite.

Después, necesita usar un subconjunto de las API de reflexión para crear un objeto System.Reflection.MethodInfo, de manera que pueda crear un árbol de expresión para tener acceso 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.

Compilación de código en profundidad

No está limitado en lo que puede crear con estas API. En cambio, cuánto más complicado sea el árbol de expresión que quiera crear, más difícil será la administración y la lectura del código.

Vamos a crear un árbol de expresión que sea 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. Aquí se muestra el código necesario para crear la misma función. No existe una API para crear un bucle while. En su lugar necesita crear un bucle que contenga una prueba condicional y un destino de la etiqueta para salir del 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 crear el árbol de expresión para la función factorial es bastante más largo, más complicado y está lleno de etiquetas, instrucciones Break y otros elementos que le gustaría evitar en nuestras tareas de codificación diarias.

En esta sección, ha escrito código para visitar cada nodo de este árbol de expresión 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 dotnet/docs de GitHub. Pruébelo compilando y ejecutando los ejemplos.

Asignación de construcciones de código a expresiones

En el siguiente ejemplo de código se muestra un árbol de expresión que represente la expresión lambda num => num < 5 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 admite también asignaciones y expresiones de flujo de control como bucles, bloques condicionales y bloques try-catch. Con la API, se pueden crear árboles de expresión más complejos que los que pueden crear el compilador de C# a partir de expresiones lambda. En el siguiente ejemplo se indica 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, consulte Generating Dynamic Methods with Expression Trees in Visual Studio 2010 (Generar métodos dinámicos con árboles de expresión en Visual Studio 2010), que también se aplica a las últimas versiones de Visual Studio.