Поделиться через


Интерпретация выражений

В следующем примере кода показано, как дерево выражений, представляющее лямбда-выражение num => num < 5 , может быть разложено на его части.

// 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

Теперь давайте напишем код, чтобы изучить структуру дерева выражений. Каждый узел в дереве выражений — это объект класса, производный от Expression.

Эта конструкция делает посещение всех узлов в дереве выражений относительно простой рекурсивной операцией. Общая стратегия — начать с корневого узла и определить тип узла.

Если у типа узла есть дочерние элементы, рекурсивно обойдите каждый дочерний элемент. На каждом дочернем узле повторите процесс, используемый на корневом узле: определите тип, а если тип имеет дочерних элементов, посетите каждый из дочерних элементов.

Проверка выражения без дочерних элементов

Начнем с посещения каждого узла в простом дереве выражений. Ниже приведен код, который создает константное выражение, а затем проверяет его свойства:

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}");

Предыдущий код выводит следующие выходные данные:

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

Теперь давайте напишем код, который будет изучать это выражение и записывать в него некоторые важные свойства.

Выражение сложения

Давайте начнем с примера добавления из введения к этому разделу.

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

Примечание.

Не используйтеvar для объявления этого дерева выражений, так как естественный тип делегата Func<int>, а не Expression<Func<int>>.

Корневой узел — это LambdaExpression. Чтобы получить интересный код в правой части => оператора, необходимо найти один из дочерних элементов LambdaExpression. Это можно сделать со всеми выражениями в этом разделе. Родительский узел действительно помогает найти тип возвращаемого значения LambdaExpression.

Чтобы проверить каждый узел в этом выражении, необходимо рекурсивно посетить множество узлов. Ниже приведена простая первая реализация:

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}");

Этот пример выводит следующие выходные данные:

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

Вы заметили много повторений в предыдущем примере кода. Давайте приведем в порядок и создадим посетителя узлов выражений общего назначения. Это потребует от нас написания рекурсивного алгоритма. Любой узел может быть типом с возможностью иметь дочерние элементы. Любой узел с дочерними элементами требует, чтобы мы посетили эти элементы и идентифицировали, каким является этот узел. Вот упрощённая версия, которая использует рекурсию для обработки операций сложения.

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}");
    }
}

Этот алгоритм служит основой для посещения любого произвольного LambdaExpression. Созданный код ищет только небольшой пример возможных наборов узлов дерева выражений, с которыми он может столкнуться. Тем не менее, вы по-прежнему можете узнать довольно много от того, что он производит. (По умолчанию в Visitor.CreateFromExpression методе выводится сообщение в консоль ошибок при обнаружении нового типа узла. Таким образом, вы знаете, чтобы добавить новый тип выражения.)

При запуске этого объекта посещения в ранее упомянутом выражении сложения вы получите следующий результат.

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

Теперь, когда вы создали более общую реализацию посетителей, вы можете посетить и обработать гораздо больше различных типов выражений.

Выражение сложения с большим количеством операндов

Давайте рассмотрим более сложный пример, но по-прежнему ограничиваем типы узлов только для добавления:

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

Перед тем как запустить эти примеры на алгоритме посетителя, попробуйте представить себе, каким может быть результат. Помните, что + оператор является двоичным оператором: он должен иметь два дочерних элемента, представляющих левые и правые операнды. Существует несколько возможных способов создания дерева, которое может быть правильным:

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;

Вы можете увидеть разделение на два возможных ответа, чтобы выделить наиболее перспективные. Первый представляет правильные ассоциативные выражения. Второй представляет левые ассоциативные выражения. Преимущество обоих этих двух форматов заключается в том, что формат масштабируется до любого произвольного числа выражений сложения.

Если вы выполняете это выражение через посетителя, вы увидите эти выходные данные, убедившись, что простое выражение сложения остается ассоциативным.

Чтобы запустить этот пример, и просмотреть дерево полного выражения, необходимо внести одно изменение в дерево исходного выражения. Если дерево выражений содержит все константы, результирующее дерево просто содержит постоянное значение 10. Компилятор выполняет все добавление и уменьшает выражение до самой простой формы. Простое добавление одной переменной в выражение достаточно, чтобы увидеть исходное дерево:

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

Создайте посетителя для этой суммы и запустите посетителя, чтобы увидеть следующий вывод:

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

Вы можете запустить любой из других примеров с помощью кода визитора и посмотреть, какое дерево оно представляет. Ниже приведен пример предыдущего sum3 выражения (с дополнительным параметром, чтобы предотвратить вычисление константы компилятором):

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

Ниже приведены выходные данные посетителя:

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

Обратите внимание, что скобки не являются частью выходных данных. В дереве выражений нет узлов, представляющих скобки в входном выражении. Структура дерева выражений содержит всю информацию, необходимую для передачи порядка выполнения операций.

Расширение этого примера

В этом примере рассматриваются только самые рудиментарные деревья выражений. Код, который вы видели в этом разделе, обрабатывает только целые числа констант и двоичный + оператор. В качестве последнего примера давайте изменим способ обработки посетителем более сложного выражения. Давайте применим его к следующему факториальному выражению:

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

Этот код представляет собой одну из возможных реализаций математической факториальной функции. Способ написания этого кода выделяет два ограничения для создания деревьев выражений путем назначения лямбда-выражений выражениям. Во-первых, лямбда-выражения инструкции не допускаются. Это означает, что вы не можете использовать циклы, блоки, операторы if / else и другие структуры управления, распространенные в C#. Вы ограничены использованием выражений. Во-вторых, вы не можете рекурсивно вызывать то же выражение. Можно было бы, если бы он уже был делегатом, но вы не можете вызвать его в виде дерева выражений. В разделе о создании деревьев выражений вы узнаете, как преодолеть эти ограничения.

В этом выражении встречаются узлы всех этих типов.

  1. Равно (двоичное выражение)
  2. Умножение (двоичное выражение)
  3. Условное выражение ? :
  4. Выражение вызова метода (вызовы Range() и Aggregate())

Одним из способов изменения алгоритма посетителя является сохранение его выполнения и запись типа узла при каждом достижении default предложения. После нескольких итераций вы увидите каждый из потенциальных узлов. Тогда у вас есть все, что вам нужно. Результат будет примерно таким:

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 и MethodCallVisitor обрабатывают эти два узла.

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");
        }
    }
}

И выходные данные для дерева выражений будут следующими:

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

Расширение библиотеки примеров

В примерах в этом разделе показаны основные методы посещения и проверки узлов в дереве выражений. Это упрощает типы узлов, с которыми вы столкнетесь, чтобы сосредоточиться на основных задачах посещения и доступа к узлам в дереве выражений.

Во-первых, посетители обрабатывают только константы, которые являются целыми числами. Константные значения могут быть любым другим числовым типом, а язык C# поддерживает преобразования и повышение между этими типами. Более надежная версия этого кода отражает все эти возможности.

Даже в последнем примере распознается подмножество возможных типов узлов. Вы все еще можете привести множество выражений, которые вызывают его сбой. Полная реализация включена в .NET Standard под именем ExpressionVisitor и может обрабатывать все возможные типы узлов.

Наконец, библиотека, используемая в этой статье, была создана для демонстрации и обучения. Он не оптимизирован. Это позволяет сделать структуры понятными и подчеркнуть методы, используемые для посещения узлов и анализа их содержимого.

Даже с этими ограничениями вы должны быть хорошо на пути к написанию алгоритмов, которые считывают и понимают деревья выражений.