Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Uma árvore de expressão é uma estrutura de dados que representa algum código. Não é um código compilado e executável. Se você quiser executar o código .NET representado por uma árvore de expressão, deverá convertê-lo em instruções IL executáveis. A execução de uma árvore de expressão 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.
Observação
Se o tipo do delegado não for conhecido, ou seja, a expressão lambda é 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 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. Essa restrição tem pouco efeito na prática. Expressões Lambda são os únicos tipos de expressões que você deseja executar convertendo em IL (linguagem intermediária executável). (Pense no que significaria executar diretamente um System.Linq.Expressions.ConstantExpression. Isso 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 do .NET Core. Ele é usado para representar uma expressão que corresponde a qualquer tipo de delegado. Como esse tipo corresponde a um tipo delegado, o .NET pode examinar a expressão e gerar IL para um delegado adequado que atenda à assinatura da expressão lambda. O tipo de 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 LambdaExpression.Compile() método retorna o Delegate tipo. 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 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 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 os 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 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.
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 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 elevar 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 tempo de vida
Execute 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 reter o identificador para esse delegado e invocá-lo mais tarde. Você não precisa compilar a árvore de expressão 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 mais tarde cria 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ária para determinar se elas representam o mesmo algoritmo é uma operação demorada. O tempo de computação que você economiza evitando chamadas LambdaExpression.Compile() 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.
Advertências
Compilar uma expressão lambda para um delegado e invocar esse delegado é uma das operações mais simples que você pode executar com uma árvore de expressão. No entanto, mesmo com essa operação simples, há ressalvas que você deve estar ciente.
As Expressões Lambda criam fechamentos em 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 sua expressão acessar uma variável que implementa IDisposable, é possível que seu código possa descartar 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 constantlocal. Essa variável é acessada a qualquer momento no futuro, 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ê usar isto em uma expressão, como mostrado no código a seguir, você obterá um System.ObjectDisposedException quando 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 tempo de execução representando uma estrutura de tempo de compilação, mas esse é o mundo que você adentra quando trabalha com árvores de expressão.
Há inúmeras permutações desse 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ão 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 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
Á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 que você criar. Desde que o ambiente em que você compila e execute o código corresponda ao ambiente em que você cria a expressão, tudo funciona conforme o esperado. Quando isso não acontece, os erros são previsíveis e são capturados em seus primeiros testes de qualquer código usando as árvores de expressão.