Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Дерево выражений — это структура данных, которая определяет код. Деревья выражений основаны на той же структуре, что компилятор использует для анализа кода и создания скомпилированных выходных данных. При чтении этой статьи вы заметили довольно много сходства между деревьями выражений и типами, используемыми в API Roslyn для создания анализаторов и CodeFixes. (Анализаторы и CodeFixes — это пакеты NuGet, которые выполняют статический анализ кода и предлагают потенциальные исправления для разработчика.) Основные понятия аналогичны, и конечный результат — это структура данных, которая позволяет проверять исходный код в понятном виде. Однако деревья выражений основаны на другом наборе классов и API, отличных от API Roslyn. Ниже приведена строка кода:
var sum = 1 + 2;
При анализе предыдущего кода в виде дерева выражений дерево содержит несколько узлов. Самый внешний узел — это оператор объявления переменной с назначением (var sum = 1 + 2;
) Тот внешний узел содержит несколько дочерних узлов: объявление переменной, оператор назначения и выражение, представляющее правую сторону знака равенства. Это выражение также подразделяется на выражения, представляющие операцию добавления, а также левые и правые операнды добавления.
Давайте рассмотрим немного больше выражений, составляющих правую сторону знака равенства. Выражение — 1 + 2
двоичное выражение. В частности, это двоичное выражение сложения. Двоичное выражение сложения содержит два дочерних элемента, представляющих левые и правые узлы выражения сложения. Здесь оба узла являются константными выражениями: левый операнд является значением 1
, а правый операнд — значение 2
.
Визуально все утверждение является деревом: вы можете начать с корневого узла и перейти к каждому узлу в этом дереве, чтобы увидеть код, составляющий утверждение.
- Оператор объявления переменной с присвоением (
var sum = 1 + 2;
)- Объявление типа неявной переменной (
var sum
)- Неявное ключевое слово var (
var
) - Объявление имени переменной (
sum
)
- Неявное ключевое слово var (
- Оператор назначения (
=
) - Двоичное выражение сложения (
1 + 2
)- Левый операнд (
1
) - Оператор сложения (
+
) - Правый операнд (
2
)
- Левый операнд (
- Объявление типа неявной переменной (
Предыдущее дерево может выглядеть сложно, но это очень мощно. Следуя тому же процессу, вы разложите намного более сложные выражения. Рассмотрим это выражение:
var finalAnswer = this.SecretSauceFunction(
currentState.createInterimResult(), currentState.createSecondValue(1, 2),
decisionServer.considerFinalOptions("hello")) +
MoreSecretSauce('A', DateTime.Now, true);
Предыдущее выражение также является объявлением переменной с назначением. В этом случае правая часть присваивания является гораздо более сложным деревом. Вы не собираетесь разложить это выражение, но рассмотрите, какие узлы могут быть разными. Существуют вызовы методов, где текущий объект используется в качестве приемника: один имеет явный this
приемник, а другой - нет. Существуют вызовы методов с помощью других объектов-получателей, существуют постоянные аргументы различных типов. И, наконец, есть двоичный оператор сложения. В зависимости от типа возвращаемого значения SecretSauceFunction()
или MoreSecretSauce()
, этот оператор двоичного сложения может быть вызовом метода переопределенного оператора сложения, который затем разрешается в статический вызов оператора двоичного сложения, определенного для класса.
Несмотря на эту воспринимаемую сложность, предыдущее выражение создает структуру дерева, перемещаемую так же легко, как в первом примере. Вы продолжаете проходить по дочерним узлам, чтобы находить листовые узлы в выражении. Узлы-родители имеют ссылки на дочерние узлы, и каждый узел имеет свойство, описывающее тип узла.
Структура дерева выражений очень согласована. После того как вы узнали основы, вы понимаете даже самый сложный код, когда он представлен как дерево выражений. Элегантность в структуре данных объясняет, как компилятор C# анализирует самые сложные программы C# и создает правильные выходные данные из этого сложного исходного кода.
Когда вы ознакомитесь со структурой деревьев выражений, вы обнаружите, что полученные знания быстро позволяют работать с множеством более сложных сценариев. Деревья выражений обладают невероятной мощью.
Помимо преобразования алгоритмов для выполнения в других средах, деревья выражений упрощают написание алгоритмов, которые проверяют код перед выполнением кода. Вы пишете метод, аргументы которого являются выражениями, и перед выполнением кода изучаете эти выражения. Дерево выражений представляет собой полное представление кода: вы видите значения любого подэкспрессии. Вы видите имена методов и свойств. Отображается значение любых константных выражений. Вы преобразуете дерево выражений в исполняемый делегат и выполняете код.
API-интерфейсы для деревьев выражений позволяют создавать деревья, которые представляют практически любую допустимую конструкцию кода. Тем не менее, чтобы сохранить вещи как можно проще, некоторые идиомы C# не могут быть созданы в дереве выражений. Одним из примеров являются асинхронные выражения (с помощью ключевых слов async
и await
). Если вашим потребностям требуются асинхронные алгоритмы, необходимо напрямую управлять Task
объектами, а не полагаться на поддержку компилятора. Другим является создание циклов. Как правило, вы создаёте эти циклы, используя циклы for
, foreach
, while
или do
. Как показано далее в этой серии, API-интерфейсы для деревьев выражений поддерживают одно циклическое выражение и break
continue
выражения, которые управляют повторяющимся циклом.
Единственное, что нельзя сделать, — изменить дерево выражений. Деревья выражений — это неизменяемые структуры данных. Если вы хотите изменить (изменить) дерево выражений, необходимо создать новое дерево, которое является копией исходного, но с нужными изменениями.