Partilhar via


Interpretar expressões

O exemplo de código a seguir demonstra como a árvore de expressão que representa a expressão num => num < 5 lambda pode ser decomposta em suas partes.

// Add the following using directive to your code file:
// using System.Linq.Expressions;

// Create an expression tree.
Expression<Func<int, bool>> exprTree = num => num < 5;

// Decompose the expression tree.
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;

Console.WriteLine($"Decomposed expression: {param.Name} => {left.Name} {operation.NodeType} {right.Value}");

// This code produces the following output:

// Decomposed expression: num => num LessThan 5

Agora, vamos escrever algum código para examinar a estrutura de uma árvore de expressão. Cada nó em uma árvore de expressão é um objeto de uma classe derivada de Expression.

Esse design torna a visita a todos os nós em uma árvore de expressão uma operação recursiva relativamente simples. A estratégia geral é começar no nó raiz e determinar que tipo de nó é.

Se o tipo de nó tiver filhos, visite recursivamente as crianças. Em cada nó filho, repita o processo utilizado no nó raiz: determine o tipo e, se o tipo tiver filhos, visite-os a cada um.

Examinar uma expressão sem filhos

Vamos começar visitando cada nó em uma árvore de expressão simples. Aqui está o código que cria uma expressão constante e, em seguida, examina suas propriedades:

var constant = Expression.Constant(24, typeof(int));

Console.WriteLine($"This is a/an {constant.NodeType} expression type");
Console.WriteLine($"The type of the constant value is {constant.Type}");
Console.WriteLine($"The value of the constant value is {constant.Value}");

O código anterior imprime a seguinte saída:

This is a/an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24

Agora, vamos escrever o código que examinaria essa expressão e escreveria algumas propriedades importantes sobre ela.

Expressão de adição

Vamos começar com o exemplo de adição da introdução a esta seção.

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

Observação

Não usevar para declarar esta árvore de expressão, porque o tipo natural do delegado é Func<int>, não Expression<Func<int>>.

O nó raiz é um LambdaExpression. A fim de obter o código interessante do lado direito do operador =>, precisas encontrar um dos filhos do LambdaExpression. Faz isso com todas as expressões nesta secção. O nó pai nos ajuda a encontrar o tipo de retorno do LambdaExpression.

Para examinar cada nó nesta expressão, você precisa visitar recursivamente muitos nós. Aqui está uma primeira implementação simples:

Expression<Func<int, int, int>> addition = (a, b) => a + b;

Console.WriteLine($"This expression is a {addition.NodeType} expression type");
Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}");
Console.WriteLine($"The return type is {addition.ReturnType.ToString()}");
Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:");
foreach (var argumentExpression in addition.Parameters)
{
    Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name: {argumentExpression.Name}");
}

var additionBody = (BinaryExpression)addition.Body;
Console.WriteLine($"The body is a {additionBody.NodeType} expression");
Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression");
var left = (ParameterExpression)additionBody.Left;
Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}");
Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression");
var right = (ParameterExpression)additionBody.Right;
Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");

Este exemplo imprime a seguinte saída:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
        Parameter Type: System.Int32, Name: a
        Parameter Type: System.Int32, Name: b
The body is a/an Add expression
The left side is a Parameter expression
        Parameter Type: System.Int32, Name: a
The right side is a Parameter expression
        Parameter Type: System.Int32, Name: b

Você percebe muita repetição no exemplo de código anterior. Vamos limpar isso e criar um visitante de nó de expressão de propósito mais geral. Isso vai exigir que escrevamos um algoritmo recursivo. Qualquer nó pode ser de um tipo que possa ter descendentes. Qualquer nó que possui filhos requer que visitemos esses filhos e determinemos o que esse nó representa. Aqui está a versão aprimorada que utiliza a recursão para percorrer as operações de adição:

using System.Linq.Expressions;

namespace Visitors;
// Base Visitor class:
public abstract class Visitor
{
    private readonly Expression node;

    protected Visitor(Expression node) => this.node = node;

    public abstract void Visit(string prefix);

    public ExpressionType NodeType => node.NodeType;
    public static Visitor CreateFromExpression(Expression node) =>
        node.NodeType switch
        {
            ExpressionType.Constant => new ConstantVisitor((ConstantExpression)node),
            ExpressionType.Lambda => new LambdaVisitor((LambdaExpression)node),
            ExpressionType.Parameter => new ParameterVisitor((ParameterExpression)node),
            ExpressionType.Add => new BinaryVisitor((BinaryExpression)node),
            _ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
        };
}

// Lambda Visitor
public class LambdaVisitor : Visitor
{
    private readonly LambdaExpression node;
    public LambdaVisitor(LambdaExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression type");
        Console.WriteLine($"{prefix}The name of the lambda is {((node.Name == null) ? "<null>" : node.Name)}");
        Console.WriteLine($"{prefix}The return type is {node.ReturnType}");
        Console.WriteLine($"{prefix}The expression has {node.Parameters.Count} argument(s). They are:");
        // Visit each parameter:
        foreach (var argumentExpression in node.Parameters)
        {
            var argumentVisitor = CreateFromExpression(argumentExpression);
            argumentVisitor.Visit(prefix + "\t");
        }
        Console.WriteLine($"{prefix}The expression body is:");
        // Visit the body:
        var bodyVisitor = CreateFromExpression(node.Body);
        bodyVisitor.Visit(prefix + "\t");
    }
}

// Binary Expression Visitor:
public class BinaryVisitor : Visitor
{
    private readonly BinaryExpression node;
    public BinaryVisitor(BinaryExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This binary expression is a {NodeType} expression");
        var left = CreateFromExpression(node.Left);
        Console.WriteLine($"{prefix}The Left argument is:");
        left.Visit(prefix + "\t");
        var right = CreateFromExpression(node.Right);
        Console.WriteLine($"{prefix}The Right argument is:");
        right.Visit(prefix + "\t");
    }
}

// Parameter visitor:
public class ParameterVisitor : Visitor
{
    private readonly ParameterExpression node;
    public ParameterVisitor(ParameterExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This is an {NodeType} expression type");
        Console.WriteLine($"{prefix}Type: {node.Type}, Name: {node.Name}, ByRef: {node.IsByRef}");
    }
}

// Constant visitor:
public class ConstantVisitor : Visitor
{
    private readonly ConstantExpression node;
    public ConstantVisitor(ConstantExpression node) : base(node) => this.node = node;

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This is an {NodeType} expression type");
        Console.WriteLine($"{prefix}The type of the constant value is {node.Type}");
        Console.WriteLine($"{prefix}The value of the constant value is {node.Value}");
    }
}

Este algoritmo é a base de um algoritmo que visita qualquer elemento arbitrário LambdaExpression. O código que você criou procura apenas uma pequena amostra dos possíveis conjuntos de nós de árvore de expressão que ele pode encontrar. Ainda assim, pode aprender bastante com o que ele produz. (O caso padrão no método Visitor.CreateFromExpression imprime uma mensagem para o console de erro quando um novo tipo de nó é encontrado. Assim, você sabe adicionar um novo tipo de expressão.)

Ao executar esse visitante na expressão de adição anterior, você obtém a seguinte saída:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
        This is an Parameter expression type
        Type: System.Int32, Name: b, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This is an Parameter expression type
                Type: System.Int32, Name: a, ByRef: False
        The Right argument is:
                This is an Parameter expression type
                Type: System.Int32, Name: b, ByRef: False

Agora que você criou uma implementação de visitante mais geral, você pode visitar e processar muitos outros tipos diferentes de expressões.

Expressão de adição com mais operandos

Vamos tentar um exemplo mais complicado, mas ainda assim limitar os tipos de nó apenas à adição:

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

Antes de executar esses exemplos no algoritmo do visitante, tente um exercício de pensamento para descobrir qual pode ser a saída. Lembre-se que o + operador é um operador binário: ele deve ter dois filhos, representando os operandos esquerdo e direito. Existem várias maneiras possíveis de construir uma árvore que podem ser corretas:

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));
Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4;

Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4);
Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4);
Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;

Você pode ver a separação em duas respostas possíveis para destacar as mais promissoras. A primeira representa expressões associativas corretas . Os segundos representam expressões associativas de esquerda . A vantagem de ambos os formatos é que o formato é dimensionado para qualquer número arbitrário de expressões de adição.

Se você executar esta expressão ao ser processada pelo visitante, vê o seguinte resultado, verificando que a expressão de adição simples é associativa à esquerda.

Para executar este exemplo e ver a árvore de expressão completa, faça uma alteração na árvore de expressão de origem. Quando a árvore de expressão contém todas as constantes, a árvore resultante simplesmente contém o valor constante de 10. O compilador executa toda a adição e reduz a expressão à sua forma mais simples. Basta adicionar uma variável na expressão para ver a árvore original:

Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;

Crie um visitante para esta soma e execute-o para ver o seguinte resultado:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This binary expression is a Add expression
                        The Left argument is:
                                This is an Constant expression type
                                The type of the constant value is System.Int32
                                The value of the constant value is 1
                        The Right argument is:
                                This is an Parameter expression type
                                Type: System.Int32, Name: a, ByRef: False
                The Right argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 3
        The Right argument is:
                This is an Constant expression type
                The type of the constant value is System.Int32
                The value of the constant value is 4

Você pode executar qualquer um dos outros exemplos através do código de visitante e ver qual árvore ele representa. Aqui está um exemplo da expressão anterior sum3 (com um parâmetro adicional para impedir que o compilador calcule a constante):

Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);

Aqui está a resposta do visitante:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: a, ByRef: False
        This is an Parameter expression type
        Type: System.Int32, Name: b, ByRef: False
The expression body is:
        This binary expression is a Add expression
        The Left argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 1
                The Right argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: a, ByRef: False
        The Right argument is:
                This binary expression is a Add expression
                The Left argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 3
                The Right argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: b, ByRef: False

Observe que os parênteses não fazem parte da saída. Não há nós na árvore de expressões que representem os parênteses na expressão de entrada. A estrutura da árvore de expressão contém todas as informações necessárias para comunicar a precedência.

Estendendo este exemplo

A amostra trata apenas das árvores de expressão mais rudimentares. O código que você viu nesta seção lida apenas com inteiros constantes e o operador binário + . Como exemplo final, vamos atualizar o visitante para lidar com uma expressão mais complicada. Vamos fazê-lo funcionar para a seguinte expressão fatorial:

Expression<Func<int, int>> factorial = (n) =>
    n == 0 ?
    1 :
    Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);

Este código representa uma implementação possível para a função fatorial matemática. A maneira como você escreveu este código destaca duas limitações da criação de árvores de expressão atribuindo expressões lambda a Expressões. Primeiro, lambdas de declaração não são permitidas. Isso significa que você não pode usar loops, blocos, instruções if / else e outras estruturas de controle comuns em C#. Você está limitado a usar expressões. Em segundo lugar, você não pode chamar recursivamente a mesma expressão. Você poderia se ele já fosse um delegado, mas você não pode chamá-lo em sua forma de árvore de expressão. Na seção sobre a construção de árvores de expressão, você aprende técnicas para superar essas limitações.

Nesta expressão, você encontra nós de todos esses tipos:

  1. Igual (expressão binária)
  2. Multiplicar (expressão binária)
  3. Condicional (? : expressão)
  4. Expressão de chamada de método (chamada Range() e Aggregate())

Uma maneira de modificar o algoritmo de visitação é mantê-lo em execução e registrar o tipo de nó toda vez que você chegar à sua default condição. Depois de algumas iterações, você verá cada um dos nós potenciais. Então, você tem tudo o que precisa. O resultado seria algo assim:

public static Visitor CreateFromExpression(Expression node) =>
    node.NodeType switch
    {
        ExpressionType.Constant    => new ConstantVisitor((ConstantExpression)node),
        ExpressionType.Lambda      => new LambdaVisitor((LambdaExpression)node),
        ExpressionType.Parameter   => new ParameterVisitor((ParameterExpression)node),
        ExpressionType.Add         => new BinaryVisitor((BinaryExpression)node),
        ExpressionType.Equal       => new BinaryVisitor((BinaryExpression)node),
        ExpressionType.Multiply    => new BinaryVisitor((BinaryExpression) node),
        ExpressionType.Conditional => new ConditionalVisitor((ConditionalExpression) node),
        ExpressionType.Call        => new MethodCallVisitor((MethodCallExpression) node),
        _ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
    };

ConditionalVisitor e MethodCallVisitor processam esses dois nós:

public class ConditionalVisitor : Visitor
{
    private readonly ConditionalExpression node;
    public ConditionalVisitor(ConditionalExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
        var testVisitor = Visitor.CreateFromExpression(node.Test);
        Console.WriteLine($"{prefix}The Test for this expression is:");
        testVisitor.Visit(prefix + "\t");
        var trueVisitor = Visitor.CreateFromExpression(node.IfTrue);
        Console.WriteLine($"{prefix}The True clause for this expression is:");
        trueVisitor.Visit(prefix + "\t");
        var falseVisitor = Visitor.CreateFromExpression(node.IfFalse);
        Console.WriteLine($"{prefix}The False clause for this expression is:");
        falseVisitor.Visit(prefix + "\t");
    }
}

public class MethodCallVisitor : Visitor
{
    private readonly MethodCallExpression node;
    public MethodCallVisitor(MethodCallExpression node) : base(node)
    {
        this.node = node;
    }

    public override void Visit(string prefix)
    {
        Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
        if (node.Object == null)
            Console.WriteLine($"{prefix}This is a static method call");
        else
        {
            Console.WriteLine($"{prefix}The receiver (this) is:");
            var receiverVisitor = Visitor.CreateFromExpression(node.Object);
            receiverVisitor.Visit(prefix + "\t");
        }

        var methodInfo = node.Method;
        Console.WriteLine($"{prefix}The method name is {methodInfo.DeclaringType}.{methodInfo.Name}");
        // There is more here, like generic arguments, and so on.
        Console.WriteLine($"{prefix}The Arguments are:");
        foreach (var arg in node.Arguments)
        {
            var argVisitor = Visitor.CreateFromExpression(arg);
            argVisitor.Visit(prefix + "\t");
        }
    }
}

E a saída para a árvore de expressão seria:

This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
        This is an Parameter expression type
        Type: System.Int32, Name: n, ByRef: False
The expression body is:
        This expression is a Conditional expression
        The Test for this expression is:
                This binary expression is a Equal expression
                The Left argument is:
                        This is an Parameter expression type
                        Type: System.Int32, Name: n, ByRef: False
                The Right argument is:
                        This is an Constant expression type
                        The type of the constant value is System.Int32
                        The value of the constant value is 0
        The True clause for this expression is:
                This is an Constant expression type
                The type of the constant value is System.Int32
                The value of the constant value is 1
        The False clause for this expression is:
                This expression is a Call expression
                This is a static method call
                The method name is System.Linq.Enumerable.Aggregate
                The Arguments are:
                        This expression is a Call expression
                        This is a static method call
                        The method name is System.Linq.Enumerable.Range
                        The Arguments are:
                                This is an Constant expression type
                                The type of the constant value is System.Int32
                                The value of the constant value is 1
                                This is an Parameter expression type
                                Type: System.Int32, Name: n, ByRef: False
                        This expression is a Lambda expression type
                        The name of the lambda is <null>
                        The return type is System.Int32
                        The expression has 2 arguments. They are:
                                This is an Parameter expression type
                                Type: System.Int32, Name: product, ByRef: False
                                This is an Parameter expression type
                                Type: System.Int32, Name: factor, ByRef: False
                        The expression body is:
                                This binary expression is a Multiply expression
                                The Left argument is:
                                        This is an Parameter expression type
                                        Type: System.Int32, Name: product, ByRef: False
                                The Right argument is:
                                        This is an Parameter expression type
                                        Type: System.Int32, Name: factor, ByRef: False

Estender a biblioteca de exemplos

Os exemplos nesta seção mostram as principais técnicas para visitar e examinar nós em uma árvore de expressão. Ele simplificou os tipos de nós que você encontrará para se concentrar nas tarefas principais de visitar e acessar nós em uma árvore de expressão.

Primeiro, os visitantes só lidam com constantes que são inteiros. Os valores constantes podem ser qualquer outro tipo numérico, e a linguagem C# suporta conversões e promoções entre esses tipos. Uma versão mais robusta desse código refletiria todos esses recursos.

Mesmo o último exemplo reconhece um subconjunto dos tipos de nó possíveis. Você ainda pode alimentá-lo com muitas expressões que fazem com que ele falhe. Uma implementação completa está incluída no .NET Standard sob o nome ExpressionVisitor e pode lidar com todos os tipos de nó possíveis.

Finalmente, a biblioteca utilizada neste artigo foi construída para demonstração e aprendizagem. Não está otimizado. Isso torna as estruturas claras, bem como destacar as técnicas usadas para visitar os nós e analisar o que está presente.

Mesmo com essas limitações, você deve estar bem encaminhado para escrever algoritmos que leiam e entendam árvores de expressão.