Создание деревьев выражений

Компилятор 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 нельзя создать лямбды операторов. Ниже приведен код, необходимый для формирования тех же функциональных возможностей. Для создания while цикла не существует API. Вместо этого необходимо создать цикл, содержащий условный тест, и целевой объект метки, чтобы выйти из цикла.

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.

Дополнительные сведения см. в записи блога Generating Dynamic Methods with Expression Trees in Visual Studio 2010 (Создание динамических методов с использованием деревьев выражений в Visual Studio 2010), которая также применима к более поздним версиям Visual Studio.