Выполнение деревьев выражений

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

Можно выполнять только деревья выражений, представляющие лямбда-выражения. Деревья выражений, представляющие лямбда-выражения, имеют тип LambdaExpression или Expression<TDelegate>. Для выполнения таких деревьев выражений вызовите метод Compile, чтобы создать исполняемый делегат, а затем вызовите делегат.

Примечание.

Если тип делегата не известен, то есть лямбда-выражение имеет тип LambdaExpression , а не Expression<TDelegate>вызывает DynamicInvoke метод делегата, а не вызывает его напрямую.

Если дерево выражений не представляет лямбда-выражение, можно создать новое лямбда-выражение, которое имеет исходное дерево выражений в качестве его тела, вызвав Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) метод. Затем можно выполнить лямбда-выражение, как описано ранее в этом разделе.

Лямбда-выражения для функций

Любое лямбда-выражение или тип, производный от лямбда-выражения, можно преобразовать в исполняемый код IL. Другие типы выражений нельзя напрямую преобразовать в код. На практике это ограничение редко имеет значение. Лямбда-выражения — это единственный тип выражений, которые может потребоваться выполнить путем преобразования в исполняемый код IL. (Подумайте о том, что это будет означать, чтобы напрямую выполнить System.Linq.Expressions.ConstantExpression. Будет ли это означать что-нибудь полезное?) Любое дерево выражений, которое является типом, производным System.Linq.Expressions.LambdaExpressionот LambdaExpression этого, можно преобразовать в IL. Тип выражения System.Linq.Expressions.Expression<TDelegate> — единственный конкретный пример в библиотеках .NET Core. Он служит для представления выражения, которое соответствует любому типу делегата. Так как этот тип сопоставлен с типом делегата, .NET может проверить выражение и создать код IL для делегата, который соответствует сигнатуре лямбда-выражения. Тип делегата основан на типе выражения. Если требуется использовать объект делегата строго типизированным образом, необходимо знать тип возвращаемого значения и список аргументов. Метод LambdaExpression.Compile() возвращает тип Delegate. Необходимо привести его к правильному типу делегата, чтобы любые средства во время компиляции проверка список аргументов или тип возвращаемого значения.

В большинстве случаев существует простое сопоставление между выражением и соответствующим делегатом. Например, дерево выражений, представленное Expression<Func<int>> приложением, преобразуется в делегат типа Func<int>. Для лямбда-выражения с любым типом возвращаемого значения и списком аргументов существует тип делегата, который является целевым типом для исполняемого кода, представленного этим лямбда-выражением.

Тип System.Linq.Expressions.LambdaExpression содержит члены LambdaExpression.Compile и LambdaExpression.CompileToMethod, которые используются для преобразования дерева выражения в исполняемый код. Метод Compile создает делегат. Метод CompileToMethod обновляет объект System.Reflection.Emit.MethodBuilder с помощью кода IL, который представляет скомпилированные выходные данные дерева выражения.

Внимание

CompileToMethodдоступен только в платформа .NET Framework, а не в .NET Core или .NET 5 и более поздних версиях.

Кроме того, можно указать System.Runtime.CompilerServices.DebugInfoGenerator сведения об отладке символов для созданного объекта делегата. Предоставляет DebugInfoGenerator полные сведения об отладке созданного делегата.

Выражение преобразовывается в делегат с помощью следующего кода:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

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

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

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

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

Выполнение и время существования

Код выполняется путем вызова делегата, созданного при вызове LambdaExpression.Compile(). Предыдущий код add.Compile()возвращает делегат. Этот делегат вызывается путем вызова func(), который выполняет код.

Делегат представляет код в дереве выражения. Вы можете сохранить дескриптор делегата и вызвать его позднее. Вам не нужно компилировать дерево выражения каждый раз, когда вы хотите выполнить представляемый им код. (Помните, что деревья выражений неизменяемы, и компиляция того же дерева выражений позже создает делегат, который выполняет тот же код.)

Внимание

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

Предупреждения

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

Лямбда-выражения создают замыкания для всех локальных переменных, на которые в них имеются ссылки. Необходимо обеспечить возможность использования всех переменных, которые будут входить в делегат, в месте вызова Compile и при выполнении итогового делегата. Компилятор гарантирует, что переменные находятся в область. Однако если выражение обращается к переменной, реализующей IDisposable, возможно, что код может удалить объект, пока он все еще находится в дереве выражений.

Например, этот код работает хорошо, так как int не реализует IDisposable:

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

Делегат захватил ссылку на локальную переменную constant. Доступ к этой переменной осуществляется позднее, когда выполняется функция, возвращаемая CreateBoundFunc.

Однако рассмотрим следующий класс (скорее выдуманные) для реализации System.IDisposable:

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

Если вы используете его в выражении, как показано в следующем коде, при System.ObjectDisposedException выполнении кода, на который Resource.Argument ссылается свойство:

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

Делегат, возвращенный этим методом, замкнулся на объекте constant, который был ликвидирован. (Причиной ликвидации является то, что он был объявлен в операторе using.)

Теперь при выполнении делегата, возвращаемого из этого метода, возникает ObjectDisposedException исключение в точке выполнения.

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

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

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

Итоги

Деревья выражений, представляющие лямбда-выражения, можно компилировать с целью создания делегатов, которые можно выполнять. Деревья выражений предоставляют один механизм для выполнения кода, представленного деревом выражений.

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