Почему классы Reflection.Emit?
Можно, фактически, с уверенностью предположить, что при написании большей части кода, который вы создавали в .NET, вы следовали вот такому общему рабочему процессу:
- Написать код на любимом языке
- Скомпилировать его
- Запустить результат
Но что, если бы вы были в состоянии писать и компилировать код, во время того как код выполнялся? Давайте рассмотрим некоторые сценарии, в которых классы Reflection.Emit
могут пригодиться для решения конкретных задач программирования во время выполнения.
Поддержка DSL
Если вам когда-либо приходилось делать много работы по разбору (парсингу) и обработке текста (на .NET языках или нет), вы, вероятно, использовали нечто, что называется регулярным выражением. Регулярные выражения – это несколько загадочные строки, которые содержат большую мощь, чтобы получить конкретные структурные паттерны из моря символов. Например, на рисунке 5-1 показано регулярное выражение для поиска телефонных номеров.
Рисунок 5-1: Простое регулярное выражение для поиска телефонных номеров

Это может выглядеть не совсем презентабельно, но это выражение может найти в тексте телефонный номер из США, если в нем используется дефис для разделения цифр, например: 123-555-1212. Если вы найдете время, чтобы покопаться в регулярных выражениях, вы сможете делать удивительные вещи по извлечению информации из текстового файла.
И как это относится к получению кодов операций? В .NET есть класс Regex
, который обеспечивает поддержку регулярных выражений. Вот как нужно использовать предыдущее выражение в .NET, чтобы найти номер телефона:
var phone =
"Find this: 123-555-1212. Or this: 123-555-9999.";
var matches = Regex.Match(phone,
@"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}");
while(matches.Success)
{
Console.Out.WriteLine(matches.Value);
matches = matches.NextMatch();
}
Регулярное выражение является примером DSL. DSL, как вы узнали в главе 1, являются, как правило, более мелкими, легкими языками, приспособленными для решения конкретной проблемы. Они обычно встроены в другие языки и фреймворки. Регулярные выражения сами по себе не подходят для создания сложных приложений, но они могут легко найти ссылки в тексте из файла, которое приложение загрузило в память.
Вопрос по использованию регулярных выражений в .NET заключается в том, что Regex
должен разобрать выражение, чтобы перевести его в исполняемый код. Вот где в игру вступают классы Reflection.Emit
. Regex
использует Reflection.Emit
для создания быстрой реализации заданного регулярного выражения, чтобы выполнить операции, определенные в выражении. Если вы создаете собственный DSL и хотите использовать его в коде, вы можете использовать Reflection.Emit
для компиляции кода в IL, который может работать так же быстро, как и любой код, написанный на C# или VB.
Примечание
Регулярные выражения не являются специфическими для .NET. Они существовали задолго до того, как коды операций увидели свет, поэтому есть много информации по этим удивительным выражениям. Начинать хорошо с сайта www.regular-expressions.info. Вы также можете посмотреть DSL in Action, написанную Debasish Ghosh (Manning, 2010) http://manning.com/ghosh/.
Превращение кода рефлексии в IL
Есть моменты, когда вам нужно выполнить некоторую обработку, которая не может произойти во время выполнения. Пример ToString()
в главе 2 показывает, почему вы, возможно, могли бы захотеть использовать метапрограммирование, чтобы уменьшить объем кода, который вы пишете, и отложить эту обработку, пока она не будет необходима. Вы также видели это с классом Lazy<T>
в главе 2. Lazy<T>
предоставляет вам возможность отложить загрузку значения, пока пользователь не вызовет свойство Value
: в это время Lazy<T>
создаст свойство. Если вы никогда вызовите Value
, Lazy<T>
не будет ничего делать. Аналогично происходит и с сериализацией. Сериализация представляет собой процесс, при котором содержимое объекта сохраняется в каком-то постоянном хранилище, например, памяти или файле. Вы можете десериализовать объект позже, если это необходимо. Некоторые стратегии сериализации могут быть достаточно сложными, и лучше отложить выполнение, пока вы не узнаете, что вам это нужно.
Так обстоит дело с XML сериализацией через класс XmlSerializer
. Если вы никогда не видели, как работает XmlSerializer
, то это довольно просто. Скажем, у вас был простой объект с несколькими свойствами:
public sealed class DataBucket
{
public Guid Id { get; set; }
public string Value { get; set; }
}
Сериализация экземпляра DataBucket
занимает всего несколько строк кода:
var target = new DataBucket
{
Id = Guid.NewGuid(),
Value = Guid.NewGuid().ToString("N")
};
using(var stream = new StringWriter())
{
var serializer = new XmlSerializer(typeof(DataBucket));
serializer.Serialize(stream, target);
Console.Out.WriteLine(
stream.GetStringBuilder().ToString());
}
Это выведет следующую XML информацию на окно консоли:
<?xml version="1.0" encoding="utf-16"?>
<DataBucket xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Id>b3a14833-7fbc-4e09-86b0-0d878055c1e9</Id>
<Value>ee77b2ea4c664ec798e67ce74fa9eb9f</Value>
</DataBucket>
XmlSerializer
использует изрядное количество рефлексии, чтобы выяснить, какие данные находятся в заданном объекте, чтобы выполнить над ним необходимые операции по сериализации. Поскольку возможно написать код, который использует Serialize()
и Deserialize()
, но не выполняется при запуске приложения, XmlSerializer
откладывает создание логики сериализации, пока она не будет нужна.
Этот подход имеет два преимущества. Первое легко заметить: вы выполняете «ленивые» (lazy) вычисления для сериализации, что хорошо, если сериализация никогда не произойдет. Второе, возможно, не так легко увидеть (хотя поскольку вы читали главу 2, вы, наверное, знаете, в чем оно заключается!). Это возможность сохранения и повторного использования динамической логики для будущего использования. Нахождение всей информации об объекте через его метаданные занимает время: помните, рефлексия происходит медленнее, чем сопоставимый скомпилированный код. Кроме того, как только вы выясняете, что должно быть сериализовано в объекте, эта логика не изменится в течение жизненного цикла приложения, так как не изменится определение типа. XmlSerializer
использует Reflection.Emit
, чтобы сгенерировать сборку во время выполнения, которая содержит все логику сериализации, необходимую для данного объекта.
Это еще одно использование Reflection.Emit
. Если вы пишете код, который использует рефлексию, довольно часто вы будете сталкиваться с тем, что вы выполняете логику, основываясь на данном типе или сборке. При использовании некоторой магии Emitter API (как вы увидите в разделе 5.5), вы сможете компилировать вашу логику в исполняемом коде, который можно кэшировать. Как вы увидите в разделе 5.5.3, этот технический прием может дать существенный выигрыш в производительности.
Использование функционала .NET, не поддерживаемого в вашем языке
Большинство .NET разработчиков знакомо с обработчиками исключений. В следующем фрагменте код в блоке catch
сработает, если код в блоке try
выбросит NotImplementedException
:
try
{
// logic goes here...
}
catch(NotImplementedException)
{
// Exception handling logic goes here...
}
Вы также можете использовать блок finally
, который всегда будет работать независимо от того, что происходит в блоке try
:
try
{
// logic goes here...
}
finally
{
// Clean-up logic usually goes here...
}
Примечание
Это то, во что оператор using превращает ваш код.
Dispose()
вызывается для объекта в оператореusing
внутри блокаfinally
, который гарантирует, чтоDispose()
будет вызван.
Однако знаете ли вы, есть еще один вид блока обработчика в .NET, который C# и VB не используют? Он называется блок fault
, и если бы он был в C#, он мог бы выглядеть примерно так:
try
{
// logic goes here...
}
fault
{
// Exception handling logic goes here before it's rethrown...
}
Код в блоке fault
будет выполнять только тогда, когда в блоке try
будет выброшено исключение. Затем исключение выбрасывается повторно. Это было бы удобно с транзакциями, когда вы вызываете метод Rollback()
внутри блока fault
: но, увы, ключевое слово fault
не реализовано в C# или VB.
При помощи классов Reflection.Emit
вы могли бы легко написать код, который обернет код в блоке try…fault
, потому что Reflection.Emit
поддерживает все функциональные возможности, которые позволяет .NET, а не только то, что вы видите в вашем любимом .NET языке. Другой скрытый функционал возможен на уровне IL:
- Вызов перегруженных методов, которые отличаются только возвращаемыми типами
- Выброс исключений, которые не наследуются от класса
Exception
- Создание вызовов метода, известных как tail calls, которые очищают стек перед вызовом метода (очень удобно для предотвращения переполнения стека в сценариях рекурсивного вызова)
Конечно, необходимость использования некоторых из этих дополнительных функций редка (или ее вовсе нет), но приятно знать, что у вас есть все возможности .NET, доступные вам благодаря классам Reflection.Emit
. Если вы хотите знать обо всем, что доступно в .NET, вы можете найти множество информации.
Примечание
Существует еще одна возможность CLR для исключений, которую поддерживает VB, а C# нет. Она называется блоком
filter
, который похож на блокcatch
с дополнительным булевым выражением. Если это выражение равноtrue
, код в блоке выполняется, в противном случае исключение продолжает раскручивание стека. Эта простая возможность делает VB немного более выразительным в отношении обработки исключений, чем C#.
Теперь, когда вы увидели несколько сценариев, в которых желателен динамический код, давайте кратко пройдемся по тому, как код преобразуется в сборку и как эта трансформация выглядит за кулисами.