Partilhar via


Construir á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 em tempo de execução.

As árvores de expressão são imutáveis. Ser imutável significa que você deve construir a árvore das folhas até a raiz. As APIs que você usa para criar árvores de expressão refletem este fato: os métodos que você usa para criar um nó tomam todos os seus filhos como argumentos. Vamos percorrer alguns exemplos para mostrar as técnicas.

Criar nós

Você começa com a expressão de adição com a qual tem trabalhado ao longo destas seções:

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

Para construir essa árvore de expressão, primeiro você constrói os nós de folha. Os nós foliares são constantes. Use o Constant método 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, você cria a expressão lambda:

var lambda = Expression.Lambda(addition);

Esta 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 esta, você pode combinar todas as chamadas em uma única instrução:

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

Construa uma árvore

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

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

Você começa 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 das 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 GetMethod chamada pode retornar null se o método não for encontrado. Muito provavelmente é porque você escreveu incorretamente o nome do método. Caso contrário, isso pode significar que o assembly necessário não está carregado. Finalmente, você coloca a chamada de método em uma expressão lambda e certifique-se de definir os argumentos para a expressão lambda:

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

Neste exemplo mais complicado, você vê mais algumas técnicas que muitas vezes precisa para criar árvores de expressão.

Primeiro, você precisa criar os objetos que representam parâmetros ou variáveis locais antes de usá-los. Depois de criar esses objetos, você pode usá-los em sua árvore de expressão sempre que precisar.

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

Crie código em profundidade

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

Vamos construir 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 construiu a árvore de expressão, mas simplesmente o delegado. Usando a Expression classe, você não pode criar lambdas de instrução. Aqui está o código necessário para criar a mesma funcionalidade. Não há uma API para construir um while loop, 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 construir a árvore de expressão para a função fatorial é um pouco mais longo, mais complicado e está repleto de rótulos e instruções de quebra e outros elementos que você gostaria de evitar em nossas tarefas diárias de codificação.

Para esta seção, você escreveu código para visitar cada nó nesta árvore de expressão e escrever informações sobre os nós que são criados neste exemplo. Você pode visualizar ou baixar o código de exemplo no repositório GitHub dotnet/docs. Experimente por si mesmo, construindo e executando as amostras.

Mapear construções de código para expressões

O exemplo de código a seguir demonstra uma árvore de expressão que representa a expressão num => num < 5 lambda 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 oferece suporte a atribuições e expressões de fluxo de controle, como loops, blocos condicionais e try-catch blocos. Usando a API, você pode criar árvores de expressão que são mais complexas do que aquelas que podem ser criadas a partir de expressões lambda pelo compilador 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 obter mais informações, consulte Gerando métodos dinâmicos com árvores de expressão no Visual Studio 2010, que também se aplica a versões posteriores do Visual Studio.