Главная страница   /   10.1. Открытие компилятора (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

10.1. Открытие компилятора

Метапрограммирование в .NET обычно требует того, чтобы вы понимали низкоуровневые API, как System.Reflection.Emit, или находили фреймворки, как Cecil или NRefactory. Не существует последовательного, единого подхода для поддержки технологий метапрограммирования, как динамическое выполнение кода и парсинг. Это дробление происходит потому, что внутренняя работа компилятора не видна для вас. В этом разделе вы увидите, как компиляторы традиционно работали в .NET, почему это значительно усложняет метапрограммирование и как Roslyn решает эту проблему, открывая компилятор.

Текущее состояние дел: черный ящик

Начиная с первой версии .NET, компилятор существует как простой исполняемый файл. Вы вызываете его для превращения кода, содержащегося в текстовых файлах, в сборку. На рисунке 10-1 представлен этот упрощенный процесс, который происходит при компиляции C# кода.

Рисунок 10-1: Компиляция кода при помощи csc.exe. Это довольно простой процесс: вы указываете, с какими файлами работает компилятор, и он отдает вам сборку.

На первый взгляд компилятор может показаться несколько тривиальным, но внутренние работы невероятно сложны. Правила, которым должен следовать автор компилятора, чтобы изменить C# код в метаданные и IL, могут быть сложными, если не сказать больше. Представьте себе последние строки C# кода, который вы писали на недавнем большом проекте, и подумайте обо всех вещах, в которых компилятор должен отслеживать правильность: дженерик определения, лямбда выражения, определения переменных и так далее. Написание компилятора может быть одной из самых трудных вещей. Наверное, нет никакого другого наиболее часто запускаемого исполняемого файла для вас, как для C# разработчика, чем csc.exe, и если он не работает, как ожидалось, вы будете сразу же это видеть.

Примечание

Информацию о фазах, которые делает C# компилятор, можно получить на http://mng.bz/8dWX.

Хотя много работы происходит в фоновом режиме, у вас нет множества опций для управления тем, что делает компилятор. На самом деле, у вас меньше 50 опций, и некоторые из них не имеют ничего общего с самим процессом компиляции; они говорят компилятору создать сторонние файлы (например, /doc). Когда дело доходит до метапрограммирования, это создает проблемы, которые вам придется преодолеть.

Примечание

Полный список опций командной строки для компилятора C# доступен на http://mng.bz/xML9.

Ограничения для метапрограммирования

Архитектура компилятора является сложной, но на протяжении многих лет отсутствие доступа к этому процессу стало проявлять ограничения в различных областях разработки, и не только с метапрограммированием. Подумайте о механизме анализа кода. Сейчас механизм должен ждать, пока компилятор закончит свою работу, прежде чем он сможет проанализировать сборку и сказать вам о том, какие у вас могут быть проблемы с реализацией. Например:

[OperationContract(IsOneWay = true)]
public string MyOperation() { }

C# компилятор скомпилирует это без проблем, потому что он не знает о WCF правиле, что односторонние операции (методы, помеченные OperationContractAttribute) не могут возвращать значение. Конечно, вы можете написать модульный тест для проверки вашего кода и поймать эту проблему, прежде чем она уйдет за пределы вашей машины, но ведь было бы лучше, если бы вы знали об этом, когда произошла компиляция, а не тогда, когда механизм анализа сломается? Или еще лучше – вот бы знать о таких ошибках в то время, когда вы набираете код?

Многие сервисы и возможности, которые вы используете как .NET разработчик, должны работать при том факте, что они не могут «ввязаться» в процесс компиляции. Сервисы форматирования кода, механизмы анализа кода – им нужны способы определять, что не так с вашим кодом, без явного, полного знания вашего кода. Без стандартизированного способа получить эту информацию из кода, разработчики прибегают к созданию собственных библиотек и инструментов, чтобы заполнить пробелы. Это привело к дублированию усилий во многих областях, связанных с разбором (парсингом) кода и созданием сборок (например, Cecil и CCI).

Это также ключевой вопрос тогда, когда речь идет метапрограммировании. Для .NET разработчика нет никакого способа получить доступ к рабочим процессам компилятора, чтобы влиять и манипулировать тем, что делает компилятор. Задумайтесь об этом на минуту, ведь это относится ко всем техническим приемам и идеям, о которых вы читали в этой книге. Например, когда вы рассматривали добавление кода через переписывание сборки в главе 6, вы должны были использовать библиотеку под названием NRefactory для парсинга C# кода. Если бы .NET предоставлял эту связанную с парсингом информацию, вам не нужно было бы создавать ее самостоятельно (или использовать то, что сделал кто-то другой). Если у вас была возможность добавить реализацию ToString() или «вплести» код, который проверял бы аргументы на значение null, когда код компилировался, вам не нужно было бы вставить еще один этап после компиляции. Вы могли бы сделать это во время компиляции!

К счастью, Microsoft работает над новым, управляемым компилятором со множеством API, с которым вы можете поиграть. Он называется Project Roslyn.

Что предлагает Roslyn: белый ящик

Project Roslyn, наконец, открывает компилятор, что позволяет вам узнать все шаги и пути, по которым идет компилятор, когда он принимает ваш код и создает сборку. Рисунок 10-2 иллюстрирует то, что сейчас доступно для вас с Project Roslyn.

Рисунок 10-2: Внутренний мир компилятора. Доступ к такому уровню детализации открывает целый новый набор динамических возможностей для .NET разработчика.

Теперь у вас есть доступ к парсинговой информации, символам и так далее. Хотя у следующей версии csc.exe может и не быть данного типа расширений, его внутренние свойства уже не внутренние свойства вовсе – они уже находятся вовне благодаря Roslyn API. Мы раскроем детали того, как работает Roslyn, в следующих разделах. Но это касается не только того, что делает компилятор. Имея такую обширную информацию о коде, вы можете подумать о том, чтобы дать разработчикам немедленный фидбэк о том, как исправить возможные проблемы, или предоставить средства для генерации кода, либо генерировать код во время выполнения – возможности огромны. Roslyn дает много тем, кто интересуется методиками метапрограммирования, и он также обеспечивает единое представление для любителей анализа кода.

В этой главе вы увидите примеры того, как вы можете использовать Roslyn для выполнения и изучения кода для поддержки метапрограммирования. Прежде чем погрузиться в Roslyn API, давайте потратим немного времени и посмотрим, чего же нет в Roslyn. Это поможет установить ваш уровень ожиданий от того, что он может делать.

Что есть и чего нет в CTP

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

Во-первых, Roslyn работает только с C# и VB. Если вы любите F#, то вам не повезло, если дело доходит до анализа кода при помощи Roslyn API. То же самое касается любого другого языка, предназначенного для мира .NET. Это не означает, что Roslyn не изменится в будущих версиях, чтобы была возможность создавать провайдеры и расширения для Roslyn для других языков, но Microsoft нужно справиться с первой версией.

Во-вторых, Roslyn не касается расширения языков C# и VB. Когда вы начинаете играть с Roslyn API, вы можете начать с идеи, как эта:

public disposed class DisposableItem
{
	public void DoSomething()
	{
		// Important code goes here...
	}
	public string Data { get; set; }
}

Обратите внимание на добавление нового ключевого слова, disposed, в этом фрагменте кода. Наличие компилятора для генерации всего кода, который вам нужен, чтобы реализовать IDisposable, включая правила вокруг ObjectDisposedException, основываясь на существовании пользовательских маркеров – это было бы классно! Или как насчет того, чтобы сделать что-то вроде этого?

var x = 23.2;
var y = 4.2;
var z = x ^^ y;

Вместо того чтобы набирать Math.pow, вы могли бы использовать что-то вроде двойной каретки (^^), и чтобы ваша пользовательская реализация парсинга переводила этот маркер в вызов Math.pow. Вы думаете, ух, у меня есть доступ к движку компилятора, почему бы мне не добавить свои собственные ключевые слова и операции и забрать их, когда код разобран?

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

Это не значит, что вы не можете использовать метаданные, чтобы расширить C# следующим образом:

[Disposable]
public sealed class DisposableItem
	: IDisposable
{
	public void Dispose()
	{
		// Dispose code goes here...
	}
	public void DoSomething()
	{
		// Important code goes here...
	}
	public string Data { get; set; }
}

Затем вы можете использовать существование этих метаданных для генерации кода при помощи Roslyn. Здесь нет ничего плохого. Но Roslyn не разрешит вам расширить C# или VB.

Примечание

Для всей информации по ограничениям Roslyn зайдите на http://mng.bz/PXRO.

Рассмотрев ограничения, давайте посмотрим, что Roslyn может делать. Мы начнем с создания частей исполняемого кода, компилируя код в виде текста, и будем двигаться дальше по нескольким направлениям. Наш первый пример отобразит то, что вы уже видели ранее в этой книге, но он не будет включать в себя понимание IL или деревьев выражений. Все, что вам нужно, это ваше знание C#, которое, как вы увидите, позволит сосредоточиться на навыках, которые у вас уже есть.