Главная страница   /   5.2. Сборка изнутри (Метапрограммирование в .NET

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

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

Кевин Хазард

5.2. Сборка изнутри

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

Трансформация языков высокого уровня

При компиляции программы компилятор превращает ваш код в формат, который может понять CLR. Этот формат немного сложнее понять, чем то, что вы используете с языками высокого уровня, как C#, так что мы потратим некоторое время на изучение деталей.

Давайте начнем с простого примера, показанного в следующем листинге. Этот код создает случайное число, которое извлекается с помощью класса отложенной инициализации Lazy<T>. Это число затем показывается пользователю в окне консоли.

Листинг 5-1: Отложенная инициализация случайного числа
using System;
namespace LazyIntegers
{
	internal static class Program
	{
		private static void Main(string[] args)
		{
			var lazyInteger = new Lazy<int>(() =>
			{
				return new Random().Next();
			});
			Console.Out.WriteLine(lazyInteger.Value);
		}
	}
}

При компиляции этого кода компилятор преобразует его в сборку. Сборка представляет собой файл, содержащий весь контент, который вы даете компилятору, как классы и исходные файлы. Код в листинге 5-1 предназначен для консольного приложения, поэтому вы получите исполняемый файл (EXE-файл), как LazyIntegers.exe, когда закончится компиляция. Если бы вы создавали библиотеку классов, компилятор создал бы DLL. Оба эти файла находятся в формате PE файла, но для ваших целей вполне достаточно знать, что компилятор создает файл, который CLR может принять и выполнить.

Если бы вы увидели фрагмент кода из листинга 5-1 онлайн, вы бы довольно легко сообразили, что он делает. В следующем листинге показан код, который делает то же самое, что и метод Main() из листинга 5-1, но он находится в том формате, который хранится в сборке, как только компилятор сделал свое дело.

Листинг 5-2: C# код, трансформированный в формат .NET сборки
.method private hidebysig static void Main(string[] args) cil managed
{
	.entrypoint
	.maxstack 3
	.locals init ([0] class
	[mscorlib]System.Lazy`1<int32> lazyInteger)
	IL_0000: ldsfld class [mscorlib]System.Func`1<int32>
	LazyIntegers.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
	IL_0005: brtrue.s IL_0018
	IL_0007: ldnull
	IL_0008: ldftn int32 LazyIntegers.Program::'<Main>b__0'()
	IL_000e: newobj instance void class
	[mscorlib]System.Func`1<int32>::.ctor(object, native int)
	IL_0013: stsfld class [mscorlib]System.Func`1<int32>
	LazyIntegers.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
	IL_0018: ldsfld class [mscorlib]System.Func`1<int32>
	LazyIntegers.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
	IL_001d: newobj instance void class
	[mscorlib]System.Lazy`1<int32>::.ctor(class
	[mscorlib]System.Func`1<!0>)
	IL_0022: stloc.0
	IL_0023: call class [mscorlib]System.IO.TextWriter
	[mscorlib]System.Console::get_Out()
	IL_0028: ldloc.0
	IL_0029: callvirt instance !0 class
	[mscorlib]System.Lazy`1<int32>::get_Value()
	IL_002e: callvirt instance void
	[mscorlib]System.IO.TextWriter::WriteLine(int32)
	IL_0033: ret
}

Этот формат пришел из Intermediate Language Disassembler или ILDasm. Это инструмент .NET фреймворка, который позволяет увидеть все составляющие сборки. Вы можете запустить его из командной строки Visual Studio, набрав ildasm. Рисунок 5-2 показывает, на что похож ILDasm, когда загружено консольное приложение, которое содержит код из листинга 5-1.

Рисунок 5-2: Сборка, открытая в ILDasm. Вы можете увидеть содержимое сборки в дереве.

Примечание

В зависимости от того, как настроена ваша рабочая среда, у вас, возможно, будет неправильная информация о пути, так чтобы при наборе ildasm в командной строке все работало корректно. VS устанавливает инструмент командной строки Visual Studio, который вы можете найти в папке Visual Studio Tools. Запустите этот файл, и вы сможете использовать ILDasm.

Чтобы загрузить сборку в ILDasm, используйте опцию меню File > Open. Вы можете перейти к любому члену сборки и проверить его содержимое. Рисунок 5-3 является окном кода, которое будет показано, если вы двойным щелчком мыши кликните по методу Main().

Рисунок 5-3: Код в ILDasm. Это код за кулисами метода Main().

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

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

Разметка членов в сборках и ключевых словах

Давайте сосредоточимся на первой строке кода из листинга 5-2. В настоящее время мы не будем тратить время на те строки, которые начинаются с IL. Это реализация метода: то, что вы видите, является кодами операций. Мы вернемся к кодам операций в разделе 5-3; сейчас мы больше заинтересованы в определениях членов и разметке сборки.

Большую часть этой первой строки довольно легко интерпретировать. Первый кусочек – это .method, который определяет, что содержание внутри фигурных скобок представляет собой метод. Как вы можете себе представить, члены могут находиться в области действия других членов. Следующий листинг показывает, что этот метод является членом класса Program. Каждый раз, когда вы сталкиваетесь с чем-то, что имеет перед собой точку, вы встречаете директиву. Есть много других директив, и большинство из них связаны с членами, с которыми вы знакомы в .NET, например, .assembly, .field и .event.

Листинг 5-3: Область видимости метода Main() в классе Program
.class private abstract auto ansi sealed beforefieldinit
	LazyIntegers.Program
	extends [mscorlib]System.Object
{
	.method private hidebysig static void Main(string[] args) cil managed
	{
		// ...
	}
}

Вы также можете увидеть другие ключевые слова в определении метода. Например, private определяет область видимости метода, а static указывает, что метод определен для класса (это не экземпляр метода). Для некоторых других их связь с оригинальным C# кодом может быть не столь очевидной, например, для managed и hidebysig. Кроме того, некоторые комбинации могут даже казаться противоречащими друг другу. В случае с определением класса LazyIntegers.Program, ключевые слова abstract и sealed используются одновременно. Вы можете задаться вопросом, как может класс быть и таким, и таким одновременно.

Примечание

Причина того, что ILDasm показывает имя класса как LazyIntegers.Program, заключается в том, что это и есть имя класса. C# и VB позволяют разделить полное имя класса на пространство имен и имя класса. Вы также можете обращаться к пространствам имен через ключевые слова using (C#) или Imports (VB), но это все синтаксический «сахар» и организационная надстройка над тем, что представляет собой истинное имя класса.

Есть еще много директив и ключевых слов, которые можно использовать и которые мы не сможем охватить в этой книге. Как вы можете определить все ключевые слова, с которыми вы, возможно, столкнетесь, когда посмотрите на .NET код в этом формате? К счастью, ряд документов по спецификации, называемых Partition документами, содержат подробную информацию по .NET. Вы всегда можете обратиться к этой документации в случае, если вы столкнетесь с директивой или ключевым словом, которое вы никогда не видели прежде. Вы можете найти ее на http://mng.bz/qu5U. Мы ссылаемся на ECMA документацию по стандартам время от времени для уточнения некоторых концепций, поэтому мы настоятельно рекомендуем загрузить ее. Познавательно также просмотреть ее содержимое, чтобы увидеть, что может происходить в .NET сборках.

Определения ключевых слов

Если вам интересно, managed обозначает, что метод содержит только коды операций IL (что отличается от метода P/Invoke).

hidebysig определяет, как метод «прячет» другие методы от базового класса и самого себя.

Program определяется как sealed и abstract одновременно, потому что это статический класс C#.

На уровне CLR нет понятия о статическом классе, но вы можете сделать класс и sealed (вы не можете наследовать от него), и abstract (вы никогда не сможете создать его экземпляр).

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