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


Деревья выражений сборки

Компилятор 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.

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