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


Построение деревьев выражений

Компилятор C# создал все деревья выражений, которые вы видели до сих пор. Вы создали лямбда-выражение, присвоенное переменной, тип которой является Expression<Func<T>> или аналогичным типом. Во многих сценариях вы создаете выражение в памяти во время выполнения.

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

Создание узлов

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

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

Чтобы построить это дерево выражений, сначала создайте узлы-листья. Конечные узлы являются константами. Constant Используйте метод для создания узлов:

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

Затем создайте выражение сложения:

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

Создав выражение добавления, создайте лямбда-выражение:

var lambda = Expression.Lambda(addition);

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

Для таких выражений можно объединить все вызовы в одну инструкцию:

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

Создание дерева

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

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

Начните с создания выражений параметров для x и y:

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

Создание выражений умножения и добавления следует шаблону, который вы уже видели:

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

Затем необходимо создать выражение вызова метода для обращения к Math.Sqrt.

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

Вызов GetMethod может вернуть null, если метод не найден. Скорее всего, это связано с ошибкой имени метода. В противном случае это может означать, что требуемая сборка не загружается. Наконец, вы помещаете вызов метода в лямбда-выражение и определите аргументы для лямбда-выражения:

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

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

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

Во-вторых, необходимо использовать подмножество API отражения для создания System.Reflection.MethodInfo объекта, чтобы создать дерево выражений для доступа к такому методу. Необходимо ограничиться подмножеством API отражения, которые доступны на платформе .NET Core. Опять же, эти методы расширяются на другие деревья выражений.

Создавайте код углубленно

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

Давайте создадим дерево выражений, эквивалентное этому коду:

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

Предыдущий код не построил дерево выражений, а просто создал делегат. Expression Используя класс, нельзя создавать лямбда-инструкции. Ниже приведен код, необходимый для создания той же функциональности. Не существует API для создания while цикла, необходимо создать цикл, содержащий условный тест и метку для выхода из цикла.

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

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

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

Сопоставление конструкций кода с выражениями

В следующем примере кода показано дерево выражений, представляющее лямбда-выражение num => num < 5 с помощью 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 });

API деревьев выражений также поддерживает операции присваивания и выражения управления потоком, такие как циклы, условные блоки и try-catch блоки. С помощью API можно создавать деревья выражений, которые являются более сложными, чем те, которые можно создать из лямбда-выражений компилятором C#. В следующем примере показано, как создать дерево выражений, вычисляющее факториал числа.

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

Дополнительные сведения см. в статье "Создание динамических методов с помощью деревьев выражений" в Visual Studio 2010, которое также относится к более поздним версиям Visual Studio.