Executar árvores de expressão

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

Somente árvores de expressão que representam expressões lambda podem ser executadas. Á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 método Compile para criar um delegado executável e, em seguida, invoque o delegado.

Observação

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

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

Expressões lambda para funções

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

Na maioria dos casos, existe um mapeamento simples entre uma expressão e o delegado 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 delegado que é o tipo de destino para o código executável representado por essa expressão lambda.

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

Importante

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

Como opção, você também pode fornecer um System.Runtime.CompilerServices.DebugInfoGenerator que receberá 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.

Uma expressão seria convertida 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 ao compilar e executar 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 a elevação de um número a uma potência, criando uma expressão lambda e executando-a. O resultado, representado pelo 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 tempos de vida

O código é executado ao invocar o delegado que foi 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 reter o identificador para esse delegado e invocá-lo mais tarde. Você não precisa compilar a árvore de expressão sempre que deseja executar o código que ela representa. (Lembre-se que as árvores de expressão são imutáveis e compilar a mesma árvore de expressão mais tarde, criará um delegado que executa o mesmo código.)

Cuidado

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 evitando chamadas extras a LambdaExpression.Compile() provavelmente é maior que o consumido pelo tempo de execução do código que determina se as duas árvores de expressão diferentes resultam no mesmo código executável.

Advertências

A compilação de uma expressão lambda para um delegado e invocar esse delegado é uma das operações mais simples que você pode realizar com uma árvore de expressão. No entanto, mesmo com essa operação simples, há limitações que você deve estar ciente.

As expressões lambda criam fechamentos sobre todas as variáveis locais que são referenciadas na expressão. Você deve assegurar que todas as variáveis que farão parte do delegado são utilizáveis no local em que você chamar Compile e no momento em que você executar o delegado resultante. O compilador garante que as variáveis estejam no escopo. No entanto, se a sua expressão acessa uma variável que implementa IDisposable, é possível que o código descarte o objeto enquanto ele ainda é 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 local constant. Essa variável é acessada a qualquer momento mais tarde, quando a função retornada por CreateBoundFunc for executada.

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

Se você a usar em uma expressão, conforme mostrado no seguinte código, obterá uma System.ObjectDisposedException ao executar o código referenciado pela propriedade 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;
    }
}

O delegado retornado desse método fechou sobre o objeto constant, que foi descartado. (Foi descartado, porque foi declarado em uma instrução using).

Agora, ao executar o delegado retornado por esse método, uma ObjectDisposedException será gerada no ponto de execução.

Parece estranho ter um erro de runtime representando um constructo de tempo de compilação, mas é isso que ocorre quando trabalha com árvores de expressão.

Há várias permutações desse problema, portanto é difícil oferecer diretrizes gerais para evitá-lo. Tenha cuidado ao acessar variáveis locais quando define expressões e ao acessar o estado no objeto atual (representado por this) quando cria uma árvore de expressão retornada por meio de uma API pública.

O código na sua expressão pode referenciar métodos ou propriedades em outros assemblies. Esse assembly deve estar acessível quando a expressão for definida, quando ela for compilada e quando o delegado resultante for invocado. Você é recebido com um ReferencedAssemblyNotFoundException quando ele não está presente.

Resumo

As árvores de expressão que representam expressões lambda podem ser compiladas para criar um delegado que pode ser executado. 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 constructo específico que você criar. Contanto que o ambiente em que você compilar e executar o código corresponda ao ambiente em que você criar a expressão, tudo funcionará conforme o esperado. Quando isso não acontece, os erros são previsíveis e capturados nos primeiros testes de qualquer código que usam as árvores de expressão.