Главная страница   /   4.2. Классы поставщиков кода (Метапрограммирование в .NET

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

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

Кевин Хазард

4.2. Классы поставщиков кода

JScript не поддерживает 17 языковых характеристик, которые есть у C# и VB, включая дженерики, вложенные типы, а также различные возможности, касающиеся метаданных.

C++ говорит, что он не поддерживает некоторые особенности:

  • ArraysOfArrays
  • ChainedConstructorArguments
  • Resources
  • PartialTypes
  • GenericTypeDeclarations

Большинство разработчиков пишут кода на .NET платформе, используя C# и Visual Basic. Это не удивительно, что только два поставщика кода CodeDOM, которые определены внутри сборки System.dll, предназначены для этих популярных языков. Провайдеры кода для C++, JScript и Visual J# также находятся в Global Assembly Cache (GAC), но они реализуются в отдельных сборках. Некоторые сторонние поставщики кода доступны на CodePlex.com и в других хранилищах исходного кода, но только те только что упомянутые пять языков поддерживаются Microsoft.

CodeDOM провайдер для Boo

Если вы хотите понять, как поставщик CodeDOM реализуется внутри, проверьте, как это делается для языка Boo (“Boo.Lang.CodeDom.booproj,” posted 2010, http://mng.bz/Gqq9). Изучение CodeDOM провайдера является отличным способом, чтобы узнать суть CodeDOM.

Создание экземпляра провайдера кода

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

Листинг 4-1: Конфигурация CodeDOM через XML
<configuration>
	<system.codedom>
		<compilers>
			<compiler
				language="languageName[;...;...]"
				extension="fileExtension[;...;...]"
				type="typeName, assemblyName"
				warningLevel="number"
				compilerOptions="option1 option2 [...]">
		</compilers>
	</system.codedom>
</configuration>

Вы можете настроить провайдеры кода, определив строки <compiler/> в файле конфигурации с правильным указанием типа, расширения и атрибутов языка. Остальные атрибуты являются необязательными. Например, элемент <compiler/> для мифического языка им K# может появиться в файле конфигурации как

<compiler
	language="k#;ks;ksharp"
	extension=".ks;ks"
	type="KSharp.KSharpCodeProvider, KSharpCodeProvider">

Это позволит создать экземпляр языкового провайдера класса, определенного в сборке KSharp.KSharpCodeProvider.dll с помощью любой языковой строки, связанной с ним, например, вот так:

var ksharpProv = CodeDomProvider.CreateProvider("k#");

Внутренне, когда CodeDOM загружает конфигурационные данные для языка, он хранится в экземпляре класса CompilerInfo во внутреннем словаре, поддерживаемом CodeDOM. Вы можете запросить этот словарь, чтобы выяснить, какие языки поддерживаются, вызвав статический метод GetAllCompilerInfo класса CodeDomProvider для получения всех этих объектов CompilerInfo. Вот пример:

foreach (System.CodeDom.Compiler.CompilerInfo ci in
	System.CodeDom.Compiler.CodeDomProvider.GetAllCompilerInfo())
{
	foreach (string language in ci.GetLanguages())
		System.Console.Write("{0} ", language);
	System.Console.WriteLine();
}

Этот код создаст список, который выглядит примерно так:

c# cs csharp
vb vbs visualbasic vbscript
js jscript javascript
vj# vjs vjsharp
c++ mc cpp

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

Есть несколько способов создать экземпляр поставщика кода CodeDOM. Вы можете использовать статический метод CreateProvider для класса CodeDomProvider, как было показано ранее, в качестве своего рода фабрики классов, чтобы найти предварительный провайдер по одному из его синонимов. Вы также можете создать экземпляр поставщика кода напрямую, если у вас есть ссылка на сборку и желаемый тип, наследуемый от CodeDomProvider. Следующие две строки кода, демонстрирующие эти два подхода, примерно эквивалентны:

var csProv1 = System.CodeDom.Compiler
	.CodeDomProvider.CreateProvider("c#");
var csProv2 = new Microsoft.CSharp
	.CSharpCodeProvider();

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

Оба метода для создания поставщиков кода CodeDOM позволяют, чтобы были указаны различные варианты. Например, C# и Visual Basic провайдеры поддерживают дополнительный параметр с именем CompilerVersion, который, как вы можете догадаться, можно использовать, чтобы выбрать версию компилятора. Внутренне, это работает путем поиска установленных компиляторов в локальной системе. В следующем листинге показана небольшая программа, которая будет компилировать другую программу из исходного кода в строке. Программа выбирает версию компилятора "v4.0", передавая словарь, содержащий этот вариант, как CompilerVersion в конструктор провайдера.

Листинг 4-2: InstantiatingCodeProviders.cs
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Generic;
class IntantiatingCodeProviders
{
	static void Main()
	{
		var providerOptions = new Dictionary<string, string>();
		providerOptions.Add("CompilerVersion", "v4.0");
		var csProv = new CSharpCodeProvider(providerOptions);
		var compilerParameters =
		new CompilerParameters(new string[] { });
		CompilerResults results =
		csProv.CompileAssemblyFromSource(compilerParameters,
		@"namespace V3Features
{
	class Program {
		static void Main() {
			var name = ""Kevin"";
			System.Console.WriteLine(name);
		}
	}
}");
	}
}

Если вы выполните программу, созданную из файла InstantiatingCodeProviders.cs, в системе, которая имеет установленный компилятор C# 4.0, она будет работать до конца без ошибок, компилируя маленькую программу с пространством имен V3Features, переданному в виде строки методу CompileAssemblyFromSource. Но если вы измените значение CompilerVersion в словаре providerOptions на "v2.0", вы можете получить исключение, которое читается так, как показано в следующем листинге.

Листинг 4-3: Возможное исключение из InstantiatingCodeProviders.cs
“Compiler executable file csc.exe cannot be found.”
Stack Trace:
	at System.CodeDom.Compiler.RedistVersionInfo
		.GetCompilerPath(...)
	<... some stack frames omitted ...>
	at System.CodeDom.Compiler.CodeDomProvider
		.CompileAssemblyFromSource(...)
	at IntantiatingCodeProviders
		.Main() in C:\...\InstantiatingCodeProviders.cs:line 17

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

Вы можете увидеть в сообщение об исключении в листинге 4-3, что CodeDOM ищет исполняемый компилятором файл csc.exe. Это имя исполняемого файла для автономного компилятора C#. Трассировка стека показывает дальше, что CodeDOM пытается получить путь к конкретной версии компилятора, в данном случае "v2.0". Поскольку все компиляторы C# были названы csc.exe со времен .NET 1.0, должна быть найдена папка, содержащая версию 2.0.

Если у вас есть версия C# компилятора 2.0, установленная на компьютере, и CodeDOM может ее найти, вы не получите CodeDOM исключения, когда запустите программу. Вместо этого, вы получите исключение от самого компилятора C# 2.0. Вы можете предположить, какой будет ошибка? (Подсказка: программа, которая передается в виде строки для компиляции, включает пространство имен V3Features).

Больше об опции CompilerVersion

Интересно, что поставщиком кода для версии Visual Basic, которая поставляется в .NET Framework 4.0, не является "v10.0", как вы могли бы ожидать, учитывая, что в этом релизе мы имеет 10ую версию языка. На данный момент, версия поставщика кода для C# и VB соответствует версии фреймворка, так что вместо этого вам нужно указать "v4.0". А еще лучше, если вы хотите последнюю версию компилятор, можно опустить опцию CompilerVersion вообще. Поставщики кода Visual J#, C++ и JScript не поддерживали опцию CompilerVersion на момент написания книги.

Поддерживаемые опции генератора кода

Каждый языковой генератор может поддерживать множество возможностей. Некоторые языки поддерживают блоки try/catch, а некоторые нет. На момент написания книги CodeDOM позволял языковым генераторам регистрировать 26 поддерживаемых возможностей. Регистрозависимость исходного кода также может быть зарегистрирована, но по-другому, как вы можете видеть в следующем листинге. Пример, показанный в этом листинге, перечисляет каждый из установленных CodeDOM поставщиков кода и записи того, какие языковые и генераторные возможности поддерживаются.

Листинг 4-4: ShowCompilerFeatures.cs
using System;
using System.Text;
using System.CodeDom.Compiler;

internal class ShowCompilerFeatures
{
	private static void Main()
	{
		foreach (CompilerInfo ci in
			CodeDomProvider.GetAllCompilerInfo())
		{
			StringBuilder output = new StringBuilder();
			string language = ci.GetLanguages()[0];
			output.AppendFormat("{0} features:\r\n", language);
			CodeDomProvider provider = CodeDomProvider
				.CreateProvider(language);
			output.AppendFormat("CaseInsensitive = {0}\r\n",
			                    provider.LanguageOptions.HasFlag(
				                    LanguageOptions.CaseInsensitive));
			foreach (GeneratorSupport supportableFeature
				in Enum.GetValues(typeof (GeneratorSupport)))
			{
				output.AppendFormat("{0} = {1}\r\n",
				                    supportableFeature,
				                    provider.Supports(supportableFeature));
			}
			Console.WriteLine(output.ToString());
		}
	}
}

Запуск программы из листинга 4-4 показывает, что языки C# и Visual Basic сообщают True для всех 26 поддерживаемых возможностей генератора кода. Мы достигли хорошего соотношения между языками C# и VB в последние годы, так что это имеет смысл. Свойство провайдера LanguageOption для значения перечисляемого типа LanguageOption.CaseInsensitive сообщает False для C# и True для Visual Basic. Данный отчет является правильным, потому что VB является регистронезависимым, а C# - регистрозависимым.

Вот некоторые из наиболее интересных выводов об особенностях поддерживаемых языков в выходных данных этой программы из листинга 4-4:

  • JScript не поддерживает 17 языковых характеристик, которые есть у C# и VB, включая дженерики, вложенные типы, а также различные возможности, касающиеся метаданных.
  • C++ говорит, что он не поддерживает некоторые особенности:
  • ArraysOfArrays
  • ChainedConstructorArguments
  • Resources
  • PartialTypes
  • GenericTypeDeclarations

Когда вы строите граф кода CodeDOM для генерации кода, избегайте добавления объектов в граф кода для особенностей языка, которые не поддерживаются целевым языком. Использование C# и Visual Basic для вывода данных, как правило, является хорошим решением, потому что они поддерживают все возможности языка.

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

История лямбда-выражений

Как разработчик программного обеспечения вы можете подумать, что лямбда-выражения были задуманы как конструкция информатики. Но они приходят из математики и предшествуют современной компьютерной науке на пару десятилетий. Хотя лямбды (для краткости) больше имеют математические начала, компьютеры, какими мы их знаем, не появились бы на свет без некоторых составляющих, которые известны как лямбда-исчисления (lambda calculus). Лямбды были изначально созданы как обозначение для доказательства определенной части лямбда-исчисления, и в итоге вошли в функциональные языки программирования, такие как Scheme, Haskell и F# для создания функций. В настоящее время, мы можем наслаждаться выразительностью, которую обеспечивают лямбды при написании LINQ запросов в объектно-ориентированных языках, таких как C# и Visual Basic. .NET реализация деревьев выражений также находится под сильным влиянием лямбд, что делает также возможной богатую межъязыковую поддержку благодаря Dynamic Language Runtime (DLR).

Сервисы провайдеров кода

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

  • GetCompilerInfo
  • GetLanguageFromExtension
  • IsDefinedExtension
  • IsDefinedLanguage

Это все вспомогательные методы для поиска в словаре настроенных поставщиков кода и нахождения информации о них.

Вот некоторые из наиболее интересных методов экземпляра, которые можно вызвать, как только у вас под рукой окажется класс, унаследованный от CodeDomProvider:

  • CompileAssemblyFromDom
  • CompileAssemblyFromFile
  • CompileAssemblyFromSource
  • CreateCompiler
  • GenerateCodeFromCompileUnit
  • GenerateCodeFromNamespace
  • GenerateCodeFromType
  • Parse

Как видите, все эти методы затрагивают компиляцию, генерацию кода или парсинг. Каждый может быть собран в одну из трех групп, представляющие три формы, в которых может быть представлен код приложения. В первой форме существует исходный код, написанный на популярном языке программирования, таком как C# или C++. Этот исходный код может быть скомпилирован во вторую форму, известную как .NET сборка. Она также может быть разбита в третью форму, называемую графом кода. Вы также можете строить графы кода вручную, используя объекты операторов и выражений. Вторая и третья форма могут быть обратно сгенерированы в первую форму, когда вы только захотите.

Реализации CodeDomProvider

Не все провайдеры кода реализуют все показанные ранее методы компиляции, генерации и парсинга. Некоторые провайдеры кода будут реализовывать генерацию кода из одного источника, но не из другого. На момент написания книги, ни один из провайдеров кода Microsoft не реализует метод Parse, который предназначен для преобразования исходного кода непосредственно в граф кода. Будущие версии языков и инструментов Microsoft, возможно, смогут поддерживать нечто более лучшее, чем парсинг кода в граф кода CodeDOM. Обратимся к главе 10, чтобы узнать больше.

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