Compartir a través de


Ejecutar árboles de expresión

Un árbol de expresión es una estructura de datos que representa algún código. No es código compilado ni ejecutable. Si desea ejecutar el código de .NET representado por un árbol de expresiones, debe convertirlo en instrucciones de IL 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 árboles de expresión que representan expresiones lambda. Los árboles de expresión que representan expresiones lambda son de tipo LambdaExpression o Expression<TDelegate>. Para ejecutar estos árboles de expresión, llame al Compile método para crear un delegado ejecutable y, a continuación, invoque el delegado.

Nota:

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

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

Expresiones lambda para funciones

Puede convertir cualquier lambdaExpression o cualquier tipo derivado de LambdaExpression en IL ejecutable. Otros tipos de expresión no se pueden convertir directamente en código. Esta restricción tiene poco efecto en la práctica. Las expresiones lambda son los únicos tipos de expresiones que desea ejecutar mediante la conversión al lenguaje intermedio ejecutable (IL). (Piense en lo que significaría ejecutar directamente un System.Linq.Expressions.ConstantExpression. ¿Significaría algo útil?) Cualquier árbol de expresión que sea , System.Linq.Expressions.LambdaExpressiono un tipo derivado de LambdaExpression se puede convertir en IL. El tipo System.Linq.Expressions.Expression<TDelegate> de expresión es el único ejemplo concreto de las bibliotecas de .NET Core. Se usa para representar una expresión que corresponde a cualquier tipo de delegado. Dado que este tipo se asigna a un tipo delegado, .NET puede examinar la expresión y generar IL para 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 LambdaExpression.Compile() método devuelve el Delegate tipo . 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 correlación simple entre una expresión y su delegado correspondiente. Por ejemplo, un árbol de expresión representado por Expression<Func<int>> se convertirá en un delegado del tipo Func<int>. Para una expresión lambda con cualquier tipo de valor devuelto y lista de argumentos, existe un tipo delegado que es el tipo de destino para el código ejecutable representado por esa expresión lambda.

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

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.

Convertiría una expresión en un delegado mediante el código siguiente:

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 expresiones.

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 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 duración

Ejecute el código invocando el delegado creado al llamar a LambdaExpression.Compile(). El código anterior, add.Compile(), devuelve un delegado. Invoque ese delegado llamando a func(), que ejecuta el código.

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

Precaución

No cree mecanismos de almacenamiento en caché más sofisticados para aumentar el rendimiento evitando llamadas de compilación innecesarias. Comparar dos árboles de expresión arbitrarios para determinar si representan el mismo algoritmo es una operación que consume mucho tiempo. El tiempo de proceso que ahorra evitando llamadas adicionales a LambdaExpression.Compile() es probablemente superado por el tiempo necesario para ejecutar el código que determina si dos árboles de expresión diferentes producen 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. Sin embargo, incluso con esta operación simple, hay advertencias que debe tener en cuenta.

Las expresiones lambda crean cierres 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. Sin embargo, si la expresión tiene acceso a una variable que implementa IDisposable, es posible que el código pueda eliminar el objeto mientras el árbol de expresiones todavía lo mantiene.

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 ha capturado una referencia a la variable local constant. Se accede a esa variable en cualquier momento más adelante, cuando se ejecuta la función devuelta por CreateBoundFunc .

Sin embargo, tenga en cuenta la siguiente clase (algo artificial) 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 lo usa en una expresión como se muestra en el código siguiente, obtendrá un System.ObjectDisposedException al ejecutar el código al que hace referencia la Resource.Argument propiedad :

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.

Sí que parece extraño tener un error en tiempo de ejecución que representa una construcción en tiempo de compilación, pero ese es el mundo en el que entras al trabajar con árboles de expresión.

Hay numerosas permutaciones de este problema, por lo que es difícil ofrecer instrucciones generales para evitarlo. Tenga cuidado con el acceso a variables locales al definir expresiones y tenga cuidado con el acceso al estado en el objeto actual (representado por this) al crear un árbol de expresión devuelto a través de 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 ejecutable. Los árboles de expresión proporcionan un mecanismo para ejecutar el código representado por un árbol de expresión.

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