Я вырос как разработчик на C/C++ и особенно до появления Microsoft .NET Framework часто попрекал своих коллег, которые программировали на Visual Basic, за использование такого слабо типизированного языка.
То были времена, когда статическая типизация и программирование со строгой типизацией были очевидным способом успешной разработки. Но все меняется, и сегодня сообщество разработчиков на C# (на который, кажется, перешли почти все бывшие разработчики на C/C++) часто ощущает потребность в гораздо более динамичной модели программирования. В прошлой статье я познакомил вас с некоторыми средствами динамического программирования, которые Microsoft сделала доступными через C# 4.0 и Visual Studio 2010. На этот раз я более глубоко рассмотрю некоторые релевантные ситуации и начну с одной из наиболее веских причин для использования C# 4.0 — простоты программирования с применением COM-объектов в рамках .NET Framework.
Легкий доступ к COM-объектам
Объект считается динамическим, когда его структура и поведение не полностью описывается статически определенным типом, детально известным компилятору. Следует признать, что слово «динамический» звучит слишком общо в этом контексте, поэтому давайте посмотрим на простой пример. Следующий код в любом языке сценариев вроде VBScript выполняется успешно:
Set word = CreateObject("Word.Application")
Функция CreateObject предполагает, что передаваемая ей в качестве аргумента строка содержит progID зарегистрированного COM-объекта. Она создает экземпляр компонента и возвращает его интерфейс IDispatch. Детали интерфейса IDispatch никогда не видны на уровне языка сценариев. Главное в том, что вы можете писать такой код:
Set word = CreateObject("Word.Application")
word.Visible = True
Set doc = word.Documents.Add()
Set selection = word.Selection
selection.TypeText "Hello, world"
selection.TypeParagraph()
doc.SaveAs(fileName)
В этом коде вы сначала создаете ссылку на компонент, который автоматизирует поведение нижележащего приложения Microsoft Office Word. Затем вы делаете видимым основное окно Word, добавляете новый документ, пишете в нем какой-либо текст, а затем сохраняете документ в нужное место. Этот код предельно ясен, легко воспринимается и, что важнее, прекрасно работает.
Однако причина, по которой он работает, кроется в одной особенности VBScript — позднем связывании (late binding). Позднее связывание означает, что тип данного объекта не известен до тех пор, пока поток выполнения не наткнется на этот объект. Как только это произойдет, исполняющая среда первым делом убедится, что вызванный член действительно существует в этом объекте, а затем вызовет его. То есть до запуска кода никаких предварительных проверок не осуществляется.
Вероятно, вы знаете, что у языка сценариев наподобие VBScript нет компилятора. Однако Visual Basic (в том числе его CLR-версия) уже многие годы поддерживает такую функциональность. Признаюсь, я часто завидовал своим коллегам, работающим на Visual Basic, за то, что им было легче использовать COM-объекты, — ведь нередко нужно взаимодействовать с блоками таких приложений, как Office. Фактически в некоторых случаях моей группе в конечном счете приходилось писать отдельные части нашего кода на Visual Basic, хотя приложение в целом создавалось на C#. Стоит ли этому удивляться? Умение программировать на множестве языков становится просто необходимостью.
В Visual Basic функция CreateObject существует ради максимальной совместимости. Суть в том, что языки на основе .NET Framework проектировались в расчете на раннее связывание (early binding). Взаимодействие с COM в .NET Framework предусмотрено, но никогда не поддерживалось напрямую в таких языках с помощью ключевых слов или конструкций — пока не появился C# 4.0.
В C# 4.0 (и Visual Basic) есть средства динамического просмотра, присутствие которых указывает на то, что позднее связывание теперь одобрено для разработчиков под .NET Framework. Благодаря динамическому просмотру (dynamic lookup) вы можете кодировать обращения к методам, свойствам, свойствам-индексаторам и полям, обходя проверку статических типов и откладывая разрешение до периода выполнения.
C# 4.0 также поддерживает необязательные параметры, распознавая значение по умолчанию в объявлении члена. То есть при вызове такого члена необязательные параметры можно пропускать. Более того, аргументы можно передавать не только по имени, но и по позиции. Короче говоря, улучшенное связывание с COM в C# 4.0 просто означает, что теперь этот статический и строго типизированный язык поддерживает некоторые средства языков сценариев. Прежде чем изучать, как использовать новое ключевое слово dynamic для бесшовной работы с COM-объектами, давайте поглубже заглянем во внутреннюю механику динамического просмотра типов.
Исполняющая среда динамических языков
Когда вы объявляете какую-либо переменную динамической в Visual Studio 2010, вы напрочь лишаетесь поддержки IntelliSense в конфигурации по умолчанию. Любопытно, что если вы установите дополнительный инструмент вроде ReSharper 5.0 (jetbrains.com/resharper), то сможете получать частичную информацию о динамическом объекте через IntelliSense. На рис. 1 показан редактор кода с ReSharper и без него. Этот инструмент просто перечисляет члены, определенные в динамическом типе. Как минимум, динамический объект является экземпляром System.Object.
Рис. 1. IntelliSense для динамического объекта в Visual Studio 2010 с ReSharper и без него
Посмотрим, что будет, когда компилятор встретит следующий код (я умышленно взял тривиальный пример, чтобы упростить понимание деталей реализации):
class Program
{
static void Main(string[] args)
{
dynamic x = 1;
Console.WriteLine(x);
}
}
Во второй строке компилятор не пытается разрешить символ WriteLine, и никакого предупреждения или сообщения об ошибке не появляется, как это было бы при классической проверке статических типов. Ключевое слово dynamic заставляет здесь C# вести себя как интерпретируемый язык. Соответственно компилятор генерирует кое-какой специфический код, который интерпретирует выражение, где встречается динамическая переменная или аргумент. Этот интерпретатор основан на исполняющей среде динамических языков (Dynamic Language Runtime, DLR) — совершенно новом компоненте инфраструктуры .NET Framework. Если использовать более специфическую терминологию, то компилятору приходится генерировать дерево выражения, используя абстрактный синтаксис, поддерживаемый DLR, и передавать его библиотекам DLR для обработки. В DLR дерево, переданное компилятором, инкапсулируется в динамически обновляемый объект-приемник (site object). Этот объект отвечает за связывание методов с объектами «на лету». На рис. 2 показана сильно урезанная версия реального кода, генерируемого для ранее показанной тривиальной программы.
Код на рис. 2 отредактирован и упрощен для читаемости, но и в таком виде он демонстрирует суть происходящего. Динамическая переменная преобразуется в экземпляр System.Object, а затем в DLR создается объект-приемник для этой программы. Приемник управляет привязкой между методом WriteLine с его параметрами и и целевым объектом. Привязка хранится в контексте типа Program. Чтобы вызвать метод Console.WriteLine динамической переменной, вы обращаетесь к объекту-приемнику и передаете целевой объект (в данном случае — тип Console) и его параметры (здесь — динамическую переменную). На внутреннем уровне объект-приемник проверяет, действительно ли у целевого объекта есть член WriteLine, способный принять параметр вроде объекта, который в данный момент хранится в переменной x. Если что-то идет не так, исполняющая среда C# просто генерирует исключение RuntimeBinderException.
Рис. 2. Реализация динамической переменной
internal class Program
{
private static void Main(string[] args)
{
object x = 1;
if (MainSiteContainer.site1 == null)
{
MainSiteContainer.site1 = CallSite<
Action<CallSite, Type, object>>
.Create(Binder.InvokeMember(
"WriteLine",
null,
typeof(Program),
new CSharpArgumentInfo[] {
CSharpArgumentInfo.Create(...)
}));
}
MainSiteContainer.site1.Target.Invoke(
site1, typeof(Console), x);
}
private static class MainSiteContainer
{
public static CallSite<Action<CallSite, Type, object>> site1;
}
}
Работа с COM-объектами
Новые средства C# 4.0 значительно упрощают работу с COM-объектами из приложений на основе .NET Framework. Давайте посмотрим, как создать документ Word на C#, и сравним код, необходимый для этого в .NET 3.5 и .NET 4. Приложение-пример создает новый документ Word по заданному шаблону, заполняет его и сохраняет в заранее определенное место. Шаблон содержит несколько закладок для наиболее часто используемых блоков информации. На какую бы версию инфраструктуры вы ни ориентировались — .NET Framework 3.5 или 4, самый первый шаг на пути программного создания документа Word — добавление ссылки на библиотеку объектов Word (Microsoft Word Object Library) (рис. 3).
Рис. 3 Ссылка на Microsoft Word Object Library
До Visual Studio 2010 и .NET Framework 4 эта задача потребовала бы написания кода, приведенного на рис. 4.
Рис. 4. Создание нового документа Word в C# 3.0
public static class WordDocument
{
public const String TemplateName = @"Sample.dotx";
public const String CurrentDateBookmark = "CurrentDate";
public const String SignatureBookmark = "Signature";
public static void Create(String file, DateTime now, String author)
{
// Must be an Object because it is passed as a ref
Object missingValue = Missing.Value;
// Run Word and make it visible for demo purposes
var wordApp = new Application { Visible = true };
// Create a new document
Object template = TemplateName;
var doc = wordApp.Documents.Add(ref template,
ref missingValue, ref missingValue, ref missingValue);
doc.Activate();
// Fill up placeholders in the document
Object bookmark_CurrentDate = CurrentDateBookmark;
Object bookmark_Signature = SignatureBookmark;
doc.Bookmarks.get_Item(ref bookmark_CurrentDate).Range.Select();
wordApp.Selection.TypeText(current.ToString());
doc.Bookmarks.get_Item(ref bookmark_Signature).Range.Select();
wordApp.Selection.TypeText(author);
// Save the document
Object documentName = file;
doc.SaveAs(ref documentName,
ref missingValue, ref missingValue, ref missingValue,
ref missingValue, ref missingValue, ref missingValue,
ref missingValue, ref missingValue, ref missingValue,
ref missingValue, ref missingValue, ref missingValue,
ref missingValue, ref missingValue, ref missingValue);
doc.Close(ref missingValue,
ref missingValue, ref missingValue);
wordApp.Quit(ref missingValue,
ref missingValue, ref missingValue);
}
}
Для взаимодействия с COM-интерфейсом автоматизации зачастую требуются типы Variant. Когда вы обращаетесь к COM-объекту автоматизации из приложения на основе .NET Framework, вы представляете типы Variant как простые объекты. В итоге вы не можете использовать строку, чтобы указать, скажем, имя файла шаблона, на базе которого вам нужно создать документ Word, так как параметр типа Variant должен быть передан по ссылке. И вам приходится прибегать к Object, как показано ниже:
Object template = TemplateName;
var doc = wordApp.Documents.Add(ref template,
ref missingValue, ref missingValue, ref missingValue);
Второй аспект, который следует принять во внимание, заключается в том, что Visual Basic и языки сценариев относятся к программисту гораздо мягче, чем C# 3.0. Например, она не заставляют указывать все параметры, которые объявлены для метода COM-объекта. В частности, метод Add набора Documents требует передачи четырех аргументов, и вы не сможете игнорировать их, если только ваш язык не поддерживает необязательные параметры.
Как уже упоминалось, C# 4.0 действительно поддерживает необязательные параметры. И, хотя сработает простая перекомпиляция кода на рис. 4 в C# 4.0, вы могли бы даже переписать этот код и отбросить все ненужные ссылочные параметры:
Object template = TemplateName;
var doc = wordApp.Documents.Add(template);
С новой поддержкой «пропуска ссылочных параметров» в C# 4.0 код с рис. 4 упрощается и, что важнее, становится легче в восприятии и синтаксически ближе к коду на языках сценариев. На рис. 5 показана отредактированная версия, которая нормально компилируется в C# 4.0 и дает тот же эффект, что и код на рис. 4.
Рис. 5. Создание нового документа Word в C# 4.0
public static class WordDocument
{
public const String TemplateName = @"Sample.dotx";
public const String CurrentDateBookmark = "CurrentDate";
public const String SignatureBookmark = "Signature";
public static void Create(string file, DateTime now, String author)
{
// Run Word and make it visible for demo purposes
dynamic wordApp = new Application { Visible = true };
// Create a new document
var doc = wordApp.Documents.Add(TemplateName);
templatedDocument.Activate();
// Fill the bookmarks in the document
doc.Bookmarks[CurrentDateBookmark].Range.Select();
wordApp.Selection.TypeText(current.ToString());
doc.Bookmarks[SignatureBookmark].Range.Select();
wordApp.Selection.TypeText(author);
// Save the document
doc.SaveAs(fileName);
// Clean up
templatedDocument.Close();
wordApp.Quit();
}
}
Код на рис. 5 позволяет использовать чистые .NET-типы для вызова COM-объекта. А необязательные параметры еще больше упрощают эту задачу.
Ключевое слово dynamic и другие средства COM Interop, введенные в C# 4.0, не всегда ускоряют выполнение кода, но позволяют писать код на C# так, будто это сценарий. В случае COM-объектов это достижение, пожалуй, ничуть не менее важно, чем увеличение производительности.
Развертывание PIA больше не требуется
С момента появления .NET Framework вы могли обернуть COM-объект в управляемый класс и использовать его в .NET-приложении. Но для этого вы должны были применять PIA-сборку (primary interop assembly), предоставляемую поставщиком COM-объекта. PIA-сборки были обязательны, и их нужно было развертывать вместе с клиентскими приложениями. Однако такие сборки частенько были слишком велики по размеру и обертывали весь COM API, так что их упаковка в программу установки могла оказаться малоприятной процедурой.
Visual Studio 2010 предлагает вариант без PIA (no-PIA). Он увязан на способность компилятора встраивать в текущую сборки лишь необходимые определения, которые вы иначе бы брали из PIA. В итоге в конечную сборку попадают только действительно требуемые определения, и нет никакой нужды упаковывать в программу установки PIA-сборки соответствующего поставщика. На рис. 6 показан параметр в окне Properties, включающий поддержку no-PIA в Visual Studio 2010.
Рис. 6. Включение варианта no-PIA в Visual Studio 2010
Вариант без PIA основан на такой функциональности C# 4.0, как эквивалентность типов (type equivalence). Вкратце, эквивалентность типов означает, что два разных типа можно считать эквивалентными в период выполнения и использовать как взаимозаменяемые. Типичный пример эквивалентности типов — два интерфейса с одинаковыми именами, определенными в разных сборках. Это разные типы, но их можно использовать как взаимозаменяемые при условии, что в них существуют одни и те же методы.
В целом, работа с COM-объектами может по-прежнему приводить к заметным издержкам, но поддержка COM Interop в C# 4.0 по крайней мере значительно упрощает ваш код. Операции над COM-объектами из .NET-приложений позволяют подключаться к устаревшим приложениям и использовать их ресурсы, что крайне важно во многих бизнес-сценариях, где в ином случае у вас было бы не слишком много возможностей. COM — необходимое зло в .NET Framework, но ключевое слово dynamic делает его менее страшным.