Partilhar via


Executar árvores de expressão

Uma árvore de expressão é uma estrutura de dados que representa algum código. Não é compilado e código executável. Se você quiser executar o código .NET representado por uma árvore de expressão, você deve convertê-lo em instruções IL executáveis. A execução de uma árvore de expressões pode retornar um valor ou pode apenas executar uma ação, como chamar um método.

Somente árvores de expressão que representam expressões lambda podem ser executadas. As árvores de expressão que representam expressões lambda são do tipo LambdaExpression ou Expression<TDelegate>. Para executar essas árvores de expressão, chame o Compile método para criar um delegado executável e, em seguida, invoque o delegado.

Nota

Se o tipo do delegado não for conhecido, ou seja, a expressão lambda for do tipo LambdaExpression e não Expression<TDelegate>, chame o DynamicInvoke método no delegado em vez de invocá-lo diretamente.

Se uma árvore de expressão não representar uma expressão lambda, você poderá criar uma nova expressão lambda que tenha a árvore de expressão original como seu corpo, chamando o Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) método. Em seguida, você pode executar a expressão lambda conforme descrito anteriormente nesta seção.

Expressões do Lambda para funções

Você pode converter qualquer LambdaExpression ou qualquer tipo derivado de LambdaExpression em IL executável. Outros tipos de expressão não podem ser convertidos diretamente em código. Esta restrição tem pouco efeito na prática. As expressões do Lambda são os únicos tipos de expressões que você gostaria de executar convertendo em linguagem intermediária executável (IL). (Pense no que significaria executar diretamente um System.Linq.Expressions.ConstantExpressionarquivo . Significaria algo útil?) Qualquer árvore de expressão que seja um System.Linq.Expressions.LambdaExpression, ou um tipo derivado de LambdaExpression pode ser convertida em IL. O tipo System.Linq.Expressions.Expression<TDelegate> de expressão é o único exemplo concreto nas bibliotecas .NET Core. Ele é usado para representar uma expressão que mapeia para qualquer tipo de delegado. Como esse tipo mapeia para um tipo delegado, o .NET pode examinar a expressão e gerar IL para um delegado apropriado que corresponda à assinatura da expressão lambda. O tipo de delegado é baseado no tipo de expressão. Você deve saber o tipo de retorno e a lista de argumentos se quiser usar o objeto delegado de uma maneira fortemente tipada. O LambdaExpression.Compile() método retorna o Delegate tipo. Você precisa convertê-lo para o tipo de delegado correto para que qualquer ferramenta de tempo de compilação verifique a lista de argumentos ou o tipo de retorno.

Na maioria dos casos, existe um mapeamento simples entre uma expressão e seu representante correspondente. Por exemplo, uma árvore de expressão representada por Expression<Func<int>> seria convertida em um delegado do tipo Func<int>. Para uma expressão lambda com qualquer tipo de retorno e lista de argumentos, existe um tipo de delegado que é o tipo de destino para o código executável representado por essa expressão lambda.

O System.Linq.Expressions.LambdaExpression tipo contém LambdaExpression.Compile e LambdaExpression.CompileToMethod membros que você usaria para converter uma árvore de expressão em código executável. O Compile método cria um delegado. O CompileToMethod método atualiza um System.Reflection.Emit.MethodBuilder objeto com o IL que representa a saída compilada da árvore de expressão.

Importante

CompileToMethod só está disponível no .NET Framework, não no .NET Core ou .NET 5 e posterior.

Opcionalmente, você também pode fornecer um System.Runtime.CompilerServices.DebugInfoGenerator que recebe as informações de depuração de símbolo para o objeto delegado gerado. O DebugInfoGenerator fornece informações completas de depuração sobre o delegado gerado.

Você converteria uma expressão em um delegado usando o seguinte código:

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

O exemplo de código a seguir demonstra os tipos concretos usados quando você compila e executa uma árvore de expressão.

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.

O exemplo de código a seguir demonstra como executar uma árvore de expressão que representa elevar um número a uma potência criando uma expressão lambda e executando-a. O resultado, que representa o número elevado à potência, é exibido.

// 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

Execução e prazos de vida

Você executa o código invocando o delegado criado quando você chamou LambdaExpression.Compile(). O código anterior, add.Compile(), retorna um delegado. Você invoca esse delegado chamando func(), que executa o código.

Esse delegado representa o código na árvore de expressão. Você pode manter o identificador para esse delegado e invocá-lo mais tarde. Você não precisa compilar a árvore de expressões sempre que quiser executar o código que ela representa. (Lembre-se de que as árvores de expressão são imutáveis, e compilar a mesma árvore de expressão posteriormente cria um delegado que executa o mesmo código.)

Atenção

Não crie mecanismos de cache mais sofisticados para aumentar o desempenho, evitando chamadas de compilação desnecessárias. Comparar duas árvores de expressão arbitrárias para determinar se elas representam o mesmo algoritmo é uma operação demorada. O tempo de computação que você economiza LambdaExpression.Compile() evitando chamadas extras provavelmente é mais do que consumido pelo tempo de execução do código que determina se duas árvores de expressão diferentes resultam no mesmo código executável.

Limitações

Compilar uma expressão lambda para um delegado e invocá-lo é uma das operações mais simples que você pode executar com uma árvore de expressão. No entanto, mesmo com esta operação simples, existem ressalvas que você deve estar ciente.

As expressões do Lambda criam fechamentos sobre quaisquer variáveis locais referenciadas na expressão. Você deve garantir que todas as variáveis que fariam parte do delegado sejam utilizáveis no local onde você chama Compilee quando você executa o delegado resultante. O compilador garante que as variáveis estejam no escopo. No entanto, se sua expressão acessar uma variável que implementa IDisposable, é possível que seu código possa descartar o objeto enquanto ele ainda está mantido pela árvore de expressão.

Por exemplo, esse código funciona bem, porque int não 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;
}

O delegado capturou uma referência à variável constantlocal. Essa variável é acessada a qualquer momento posteriormente, quando a função retornada por CreateBoundFunc é executada.

No entanto, considere a seguinte classe (bastante inventada) 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;
    }
}

Se você usá-lo em uma expressão como mostrado no código a seguir, você obtém um System.ObjectDisposedException quando você executa o código referenciado Resource.Argument pela propriedade:

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;
    }
}

O delegado retornado desse método foi fechado sobre o constant objeto, que foi descartado. (Foi descartado, porque foi declarado em um using comunicado.)

Agora, quando você executa o delegado retornado desse método, você tem um ObjectDisposedException lançado no ponto de execução.

Parece estranho ter um erro de tempo de execução representando uma construção em tempo de compilação, mas esse é o mundo em que você entra quando trabalha com árvores de expressão.

Existem inúmeras permutações deste problema, por isso é difícil oferecer orientação geral para evitá-lo. Tenha cuidado ao acessar variáveis locais ao definir expressões e tenha cuidado ao acessar o estado no objeto atual (representado por this) ao criar uma árvore de expressões retornada por meio de uma API pública.

O código em sua expressão pode fazer referência a métodos ou propriedades em outros assemblies. Esse assembly deve estar acessível quando a expressão é definida, quando é compilada e quando o delegado resultante é invocado. Você se depara com um ReferencedAssemblyNotFoundException nos casos em que ele não está presente.

Resumo

As árvores de expressão que representam expressões lambda podem ser compiladas para criar um delegado que você pode executar. As árvores de expressão fornecem um mecanismo para executar o código representado por uma árvore de expressão.

A Árvore de Expressão representa o código que seria executado para qualquer construção criada por você. Desde que o ambiente onde você compila e executa o código corresponda ao ambiente onde você cria a expressão, tudo funciona conforme o esperado. Quando isso não acontece, os erros são previsíveis e são detetados em seus primeiros testes de qualquer código usando as árvores de expressão.