Compartilhar via


Criar árvores de expressão

O compilador C# criou todas as árvores de expressão que você viu até agora. Você criou uma expressão lambda atribuída a uma variável digitada como um Expression<Func<T>> ou algum tipo semelhante. Para muitos cenários, você cria uma expressão na memória no tempo de execução.

As árvores de expressão são imutáveis. Ser imutável significa que você precisa criar a árvore de folhas até a raiz. As APIs que você usa para criar as árvores de expressão refletem esse fato: os métodos que você usa para criar um nó usam todos os filhos como argumentos. Vejamos alguns exemplos para mostrar as técnicas a você.

Criar nós

Comece com a expressão de adição com a qual você tem trabalhado nestas seções:

Expression<Func<int>> sum = () => 1 + 2;

Para construir essa árvore de expressão, você precisará criar os nós folha. Os nós folha são constantes. Use o método Constant para criar os nós:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

Em seguida, crie a expressão de adição:

var addition = Expression.Add(one, two);

Depois de criar a expressão de adição, crie a expressão lambda:

var lambda = Expression.Lambda(addition);

Essa expressão lambda não contém argumentos. Mais adiante nesta seção, você verá como mapear argumentos para parâmetros e criar expressões mais complicadas.

Para expressões como essa, você pode combinar todas as chamadas em uma só instrução:

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

Criar uma árvore

A seção anterior mostrou os conceitos básicos da criação de uma árvore de expressão na memória. Árvores mais complexas geralmente significam mais tipos de nó e mais nós na árvore. Vamos percorrer mais um exemplo e mostrar dois outros tipos de nó que você normalmente criará quando criar árvores de expressão: os nós de argumento e os nós de chamada de método. Vamos criar uma árvore de expressão para criar esta expressão:

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

Você começará criando expressões de parâmetro para x e y:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

A criação de expressões de multiplicação e adição segue o padrão que você já viu:

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

Em seguida, você precisa criar uma expressão de chamada de método para a chamada para Math.Sqrt.

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

A chamada GetMethod poderá retornar null se o método não for encontrado. Provavelmente, isso ocorre porque você digitou incorretamente o nome do método. Caso contrário, isso pode significar que o assembly necessário não está carregado. Por fim, você colocará a chamada de método em uma expressão lambda e definirá os argumentos para a expressão lambda:

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

Neste exemplo mais complicado, é possível ver mais algumas técnicas que frequentemente serão necessárias para criar árvores de expressão.

Primeiro, você precisa criar os objetos que representam parâmetros ou variáveis locais antes de usá-los. Após ter criado esses objetos, você pode usá-los em sua árvore de expressão quando for necessário.

Depois, você precisa usar um subconjunto das APIs de reflexão para criar um objeto System.Reflection.MethodInfo para que possa criar uma árvore de expressão para acessar esse método. Você deve se limitar ao subconjunto das APIs de reflexão que estão disponíveis na plataforma do .NET Core. Mais uma vez, essas técnicas se estenderão a outras árvores de expressão.

Criar código em profundidade

Você não fica limitado ao que pode criar usando essas APIs. No entanto, quanto mais complicada for a árvore de expressão que você quer criar, mais difícil ser;a gerenciar e ler o código.

Vamos criar uma árvore de expressão que é o equivalente a este código:

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

O código anterior não compilou a árvore de expressão, mas simplesmente o delegado. Usando a classe Expression, não é possível criar lambdas de instrução. Este é o código que é necessário para criar a mesma funcionalidade. Não há uma API para criar um loop while, em vez disso, você precisa criar um loop que contenha um teste condicional e um destino de rótulo para sair do loop.

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

O código para criar a árvore de expressão para a função fatorial é bem mais longo, mais complicado e está cheio de rótulos e instruções de interrupção, bem como outros elementos que você gostaria de evitar em nossas tarefas cotidianas de codificação.

Para esta seção, você gravou código para visitar todos os nós nesta árvore de expressão e gravar informações sobre os nós criados neste exemplo. Você pode exibir ou baixar o código de exemplo no repositório dotnet/docs do GitHub. Experimente por conta própria criando e executando os exemplos.

Mapear constructos de código para expressões

O exemplo de código a seguir demonstra uma árvore de expressão que representa a expressão lambda num => num < 5 usando a API.

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

A API de árvores de expressão também dá suporte a atribuições e expressões de fluxo de controle, como loops, blocos condicionais e blocos try-catch. Usando a API, você pode criar árvores de expressão mais complexas do que aquelas que podem ser criadas por meio de expressões lambda pelos compiladores do C#. O exemplo a seguir demonstra como criar uma árvore de expressão que calcula o fatorial de um número.

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Para saber mais, confira Gerar métodos dinâmicos com árvores de expressão no Visual Studio 2010, que também se aplica a versões mais recentes do Visual Studio.