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


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

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

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

Замечание

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

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

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

Вы можете преобразовать любое LambdaExpression или любой тип, производный от LambdaExpression, в выполнимый 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, если он отсутствует в системе.

Сводка

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

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