Ejecución de árboles de expresión

Un árbol de expresión es una estructura de datos que representa un código. No es código compilado y ejecutable. Si quiere ejecutar el código de .NET representado mediante un árbol de expresión, debe convertirlo en instrucciones de lenguaje intermedio ejecutables. La ejecución de un árbol de expresión puede devolver un valor o simplemente realizar una acción, como llamar a un método.

Solo se pueden ejecutar los árboles de expresiones que representan expresiones lambda. Los árboles de expresiones que representan expresiones lambda son de tipo LambdaExpression o Expression<TDelegate>. Para ejecutar estos árboles de expresiones, llame al método Compile para crear un delegado ejecutable y, después, invoque el delegado.

Nota:

Si el tipo del delegado es desconocido, es decir, la expresión lambda es de tipo LambdaExpression y no Expression<TDelegate>, llame al método DynamicInvoke en el delegado en lugar de invocarlo directamente.

Si un árbol de expresión no representa una expresión lambda, puede crear una nueva expresión lambda que tenga el árbol de expresión original como su cuerpo llamando al método Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>). Luego puede ejecutar la expresión lambda tal y como se ha descrito anteriormente en esta sección.

Expresiones lambda a funciones

Puede convertir cualquier objeto LambdaExpression o cualquier tipo derivado de LambdaExpression en lenguaje intermedio ejecutable. Otros tipos de expresión no se pueden convertir directamente a código. Esta restricción tiene poco efecto en la práctica. Las expresiones lambda son los únicos tipos de expresiones que podría querer ejecutar mediante la conversión a lenguaje intermedio (IL) ejecutable. (Piense en lo que significaría ejecutar directamente un elemento System.Linq.Expressions.ConstantExpression. ¿Significaría algo útil?) Cualquier árbol de expresión que sea un elemento System.Linq.Expressions.LambdaExpression o un tipo derivado de LambdaExpression se puede convertir en IL. El tipo de expresión System.Linq.Expressions.Expression<TDelegate> es el único ejemplo concreto en las bibliotecas de .NET Core. Se usa para representar una expresión que se asigna a cualquier tipo de delegado. Dado que este tipo se asigna a un tipo de delegado, .NET puede examinar la expresión y generar el lenguaje intermedio de un delegado adecuado que coincida con la firma de la expresión lambda. El tipo de delegado se basa en el tipo de expresión. Debe conocer el tipo de valor devuelto y la lista de argumentos si quiere usar el objeto de delegado de una forma fuertemente tipada. El método LambdaExpression.Compile() devuelve el tipo Delegate. Tiene que convertirlo al tipo de delegado correcto para que las herramientas de tiempo de compilación comprueben la lista de argumentos del tipo de valor devuelto.

En la mayoría de los casos, existe una asignación simple entre una expresión y su delegado correspondiente. Por ejemplo, un árbol de expresión que se representa por Expression<Func<int>> se convertiría a un delegado del tipo Func<int>. Para una expresión lambda con cualquier tipo de valor devuelto y lista de argumentos, existe un tipo de delegado que es el tipo de destino para el código ejecutable representado por esa expresión lambda.

El tipo System.Linq.Expressions.LambdaExpression contiene los miembros LambdaExpression.Compile y LambdaExpression.CompileToMethod que se usarían para convertir un árbol de expresión en código ejecutable. El método Compile crea un delegado. El método CompileToMethod actualiza un objeto System.Reflection.Emit.MethodBuilder con el lenguaje intermedio que representa la salida compilada del árbol de expresión.

Importante

CompileToMethod solo está disponible en .NET Framework, no en .NET Core o .NET 5 y versiones posteriores.

Como opción, también puede proporcionar un System.Runtime.CompilerServices.DebugInfoGenerator que recibe la información de depuración de símbolos para el objeto de delegado generado. DebugInfoGenerator proporciona información de depuración completa sobre el delegado generado.

Para convertir una expresión en un delegado se usaría el siguiente código:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

En el ejemplo de código siguiente se muestran los tipos concretos que se usan al compilar y ejecutar un árbol de expresión.

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

En el ejemplo de código siguiente se muestra cómo ejecutar un árbol de expresión que representa la elevación de un número a una potencia mediante la creación de una expresión lambda y su posterior ejecución. Se muestra el resultado, que representa el número elevado a la potencia.

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

Ejecución y duraciones

El código se ejecuta mediante la invocación del delegado que se crea al llamar a LambdaExpression.Compile(). El código anterior, add.Compile(), devuelve un delegado. Para invocar ese delegado, llame a func(), que ejecuta el código.

Ese delegado representa el código en el árbol de expresión. Se puede conservar el identificador a ese delegado e invocarlo más adelante. No es necesario compilar el árbol de expresión cada vez que se quiera ejecutar el código que representa. (Recuerde que los árboles de expresión son inmutables y que compilar el mismo árbol de expresión más adelante crea un delegado que ejecuta el mismo código).

Precaución

No cree ningún mecanismo de almacenamiento en caché más sofisticado para aumentar el rendimiento evitando llamadas innecesarias de compilación. La comparación de dos árboles de expresión arbitrarios para determinar si representan el mismo algoritmo es una operación que consume mucho tiempo de ejecución. Probablemente el tiempo de proceso que se ahorra al evitar llamadas adicionales a LambdaExpression.Compile() será consumido por el tiempo de ejecución de código que determina si dos árboles de expresión diferentes devuelven el mismo código ejecutable.

Advertencias

Compilar una expresión lambda en un delegado e invocar ese delegado es una de las operaciones más simples que se pueden realizar con un árbol de expresión. Pero incluso con esta sencilla operación, hay advertencias que debe conocer.

Las expresiones lambda crean clausuras sobre las variables locales a las que se hace referencia en la expresión. Debe garantizar que las variables que formarían parte del delegado se pueden usar en la ubicación desde la que se llama a Compile, y cuando se ejecuta el delegado resultante. El compilador garantiza que las variables estén en el ámbito. Pero si la expresión tiene acceso a una variable que implementa IDisposable, es posible que el código deseche el objeto mientras se sigue manteniendo en el árbol de expresión.

Por ejemplo, este código funciona bien porque int no implementa IDisposable:

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

El delegado capturó una referencia a la variable local constant. Esa variable es accesible en cualquier momento posterior, cuando se ejecuta la función devuelta por CreateBoundFunc.

Pero considere la siguiente clase (bastante artificiosa) que implementa System.IDisposable:

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

Si se usa en una expresión como se muestra en el siguiente código, obtiene una System.ObjectDisposedException al ejecutar el código al que hace referencia la propiedad Resource.Argument:

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

El delegado devuelto por este método se clausuró sobre el objeto constant, que se eliminó. (Se eliminó porque se declaró en una instrucción using).

Ahora, al ejecutar el delegado devuelto desde este método, se produce una excepción ObjectDisposedException en el punto de ejecución.

Parece extraño tener un error en tiempo de ejecución que representa una construcción de tiempo de compilación, pero es el mundo al que entra cuando trabaja con árboles de expresión.

Hay numerosas permutaciones de este problema, por lo que resulta difícil ofrecer instrucciones generales para evitarlo. Tenga cuidado al obtener acceso a las variables locales al definir expresiones y al obtener acceso al estado en el objeto actual (representado por this) al crear un árbol de expresión devuelto por una API pública.

El código de la expresión puede hacer referencia a métodos o propiedades de otros ensamblados. Ese ensamblado debe ser accesible cuando se define la expresión, cuando se compila y cuando se invoca el delegado resultante. En los casos en los que no esté presente, se produce una excepción ReferencedAssemblyNotFoundException.

Resumen

Los árboles de expresión que representan expresiones lambda se pueden compilar para crear un delegado que se puede ejecutar. Los árboles de expresión proporcionan un mecanismo para ejecutar el código representado por un árbol de expresión.

El árbol de expresión representa el código que se ejecutaría para cualquier construcción que se cree. Mientras que el entorno donde se compile y ejecute el código coincida con el entorno donde se crea la expresión, todo funciona según lo esperado. Cuando eso no sucede, los errores son predecibles y se detectan en las primeras pruebas de cualquier código que use los árboles de expresión.