Expando-объекты в C# 4.0Источник: Журнал MSDN Дино Эспозито
Загрузка примера кода Большая часть кода, написанного для Microsoft .NET Framework, основана на статической типизации, хотя .NET поддерживает динамическую типизацию через механизм отражения (reflection). Более того, в JScript система динамических типов поверх .NET появилась десять лет назад; то же самое относится и к Visual Basic. Статическая типизация подразумевает, что каждое выражение имеет известный тип. Типы и присваивания проверяются при компиляции, и большинство возможных ошибок типизации заблаговременно отлавливается. Хорошо известное исключение - попытка приведения типов в период выполнения, что иногда вызывает динамическую ошибку, если исходный тип несовместим с целевым. Статическая типизация обеспечивает высокую производительность и четкость кода, но основана на предположении, что вы заранее знаете почти все о своем коде и данных. Сегодня существует сильная потребность в некотором смягчении этого ограничения. Выход за рамки статической типизации, как правило, подразумевает выбор из трех разных вариантов: динамической типизации, динамических объектов и косвенного (или основанного на отражении) программирования. В .NET-программировании отражение доступно со времен .NET Framework 1.0 и широко применялось для поддержки специальных инфраструктур вроде контейнеров Inversion of Control (IoC). Эти инфраструктуры разрешают зависимости типов в период выполнения, тем самым позволяя вашему коду работать с интерфейсом, не зная конкретный тип, стоящий за объектом, и его реальное поведение. С помощью .NET-отражения вы можете реализовать разновидности косвенного программирования (indirect programming), где ваш код "общается" с промежуточным объектом, который в свою очередь диспетчеризует вызовы фиксированному интерфейсу. Вы передаете в виде строки имя члена для вызова и получаете гибкость в его чтении из какого-нибудь внешнего источника. Интерфейс целевого объекта фиксирован и неизменяем - за любыми вызовами, инициируемыми через отражение, всегда стоит некий общеизвестный интерфейс. Динамическая типизация означает, что ваш скомпилированный код игнорирует статическую структуру типов, которые можно распознать при компиляции. По сути, при динамической типизации любые проверки типов откладываются до периода выполнения. Интерфейс, для которого вы кодируете, по-прежнему остается фиксированным и неизменяемым, но в зависимости от используемого вами значения вы можете получить разные интерфейсы в разные периоды выполнения. В .NET Framework 4 введен ряд новых средств, позволяющих выходить за рамки статических типов. Я уже рассказывал о новом ключевом слове dynamic в майском номере за 2010 г.. В этой статье будет исследована поддержка динамически определяемых типов, таких как expando- и динамических объектов. С помощью динамических объектов можно программно определить интерфейс типа вместо того, чтобы считывать его из определения, статически хранящегося в сборках. Динамические объекты сочетают формальную четкость статически типизированных объектов с гибкостью динамических типов. Случаи применения динамических объектовДинамические объекты вовсе не предназначены для замены сильных сторон статических типов. Статические типы есть сейчас и останутся на обозримую перспективу, и именно они образуют фундамент разработки ПО. При статической типизации вы надежно обнаруживаете ошибки типов при компиляции и благодаря этому получаете код свободный от проверок типов в период выполнения, а значит, и быстрее работающий. Кроме того, требование прохождения этапа компиляции заставляет разработчиков и архитекторов тщательнее проектировать ПО и определения открытых интерфейсов для взаимодействующих уровней. Но бывают ситуации, в которых приходится программно оперировать относительно хорошо структурированными блоками данных. В идеале, вы бы предпочли, чтобы эти данные предоставлялись через объекты. Но вместо этого вы получаете их как простой поток данных - по сетевому соединению или чтением из файла на диске. В этом случае у вас два варианта: использовать подход с косвенным программированием и применять специально подбираемый тип (ad hoc type). В первом случае вы пользуетесь обобщенным API, который выступает в роли прокси и упорядочивает за вас запросы и операции обновления. Во втором случае вы обзаводитесь специфическим типом, корректно моделирующим данные, с которыми вы имеете дело. Вопрос в том, кто будет создавать такой тип? В некоторых сегментах .NET Framework уже есть хорошие примеры внутренних модулей, где создаются специальные типы для конкретных блоков данных. Один из самых очевидных примеров - ASP.NET Web Forms. Когда вы выдаете запрос к ASPX-ресурсу, веб-сервер извлекает содержимое ASPX-файла на серверной стороне. Потом это содержимое загружается в строку, которая обрабатывается и помещается в HTML-ответ. Поэтому вы получаете относительно хорошо структурированный блок текста, с которым вы затем работаете. Чтобы сделать что-то с этими данными, вам нужно понимать, на какие серверные элементы управления вы должны ссылаться, корректно создавать их экземпляры и связывать эти экземпляры воедино в свою страницу. Это, конечно, можно делать с помощью XML-анализатора для каждого запроса. Однако в таком случае вы получаете дополнительные издержки разбора ответа после каждого запроса, которые суммарно могут оказаться неприемлемыми. Из-за этих дополнительных издержек группа ASP.NET решила ввести разовую стадию разбора разметки в класс, который можно компилировать динамически. Итог состоит в том, что такой простой блок разметки используется через специальный класс, производный от класса в отделенном коде для страницы Web Forms: <html> <head runat="server"> <title></title> </head> <body> <form runat="server"> <asp:TextBox runat="server" /> <asp:Button runat="server" Text="Click" /> <hr /> <asp:Label runat="server"></asp:Label> </form> </body> </html> На рис. 1 показана структура класса периода выполнения, создаваемого на основе разметки. Имена методов, обозначенные серым цветом, относятся к внутренним процедурам, применяемым при разборе элементов в экземпляры серверных элементов управления. Expando-объекты в C# 4.0 Рис. 1. Структура динамически создаваемого класса в Web Forms Вы можете применить этот подход практически в любой ситуации, где ваше приложение многократно принимает внешние данные для обработки, например поток XML-данных. Для манипуляций над XML-данными существует несколько API - от XML DOM до LINQ-to-XML. В любом случае вы должны либо действовать неявно, запрашивая XML DOM или LINQ-to-XML API, либо использовать те же API для самостоятельного разбора исходных данных в специальные объекты. В .NET Framework 4 динамические объекты предлагают альтернативный, более простой API для динамического создания типов на основе некоторых исходных данных. Как пример навскидку рассмотрим следующую XML-строку: <Persons> <Person> <FirstName> Dino </FirstName> <LastName> Esposito </LastName> </Person> <Person> <FirstName> John </FirstName> <LastName> Smith </LastName> </Person> </Persons> Чтобы преобразовать ее в программируемый тип, в .NET Framework 3.5 вы, вероятно, написали бы примерно такой код, как на рис. 2. Рис. 2. Применение LINQ-to-XML для загрузки данных в объект Person var persons = GetPersonsFromXml(file); foreach(var p in persons) Console.WriteLine(p.GetFullName()); // Load XML data and copy into a list object var doc = XDocument.Load(@"..\..\sample.xml"); public static IList<Person> GetPersonsFromXml(String file) { var persons = new List<Person>(); var doc = XDocument.Load(file); var nodes = from node in doc.Root.Descendants("Person") select node; foreach (var n in nodes) { var person = new Person(); foreach (var child in n.Descendants()) { if (child.Name == "FirstName") person.FirstName = child.Value.Trim(); else if (child.Name == "LastName") person.LastName = child.Value.Trim(); } persons.Add(person); } return persons; } Этот код использует LINQ-to-XML для загрузки исходного контента в экземпляр класса Person: public class Person { public String FirstName { get; set; } public String LastName { get; set; } public String GetFullName() { return String.Format("{0}, {1}", LastName, FirstName); } } .NET Framework 4 предлагает другой API для тех же целей. Этот API, в основе которого лежит новый класс ExpandoObject, более прямолинеен в использовании и не требует от вас планировать, писать, отлаживать, тестировать и поддерживать класс Person. Давайте подробнее рассмотрим ExpandoObject. Expando-объекты в C# 4.0 Использование класса ExpandoObjectExpando-объекты были изобретены еще за несколько лет до появления .NET. Впервые я услышал этот термин, когда описывали JScript-объекты в середине 1990-х. Expando - это своего рода "надувной" объект, чья структура всецело определяется в период выполнения. В .NET Framework 4 его используют так, будто это классический управляемый объект с тем исключением, что его структура не считывается ни из какой сборки, а формируется полностью динамически. Expando-объект идеален для моделирования динамически изменяемой информации, например содержимого конфигурационного файла. Посмотрим, как использовать класс ExpandoObject для хранения содержимого из ранее упомянутого XML-документа. Полный исходный код показан нарис. 3. Рис. 3. Применение LINQ-to-XML для загрузки данных в expando-объект public static IList<dynamic> GetExpandoFromXml(String file) { var persons = new List<dynamic>(); var doc = XDocument.Load(file); var nodes = from node in doc.Root.Descendants("Person") select node; foreach (var n in nodes) { dynamic person = new ExpandoObject(); foreach (var child in n.Descendants()) { var p = person as IDictionary<String, object>); p[child.Name] = child.Value.Trim(); } persons.Add(person); } return persons; } Функция возвращает список динамически определенных объектов. С помощью LINQ-to-XML вы разбираете узлы в разметке и создаете экземпляр ExpandoObject для каждого из них. Имя каждого узла ниже <Person> становится новым свойством expando-объекта, а значением этого свойства - текст внутри узла. Исходя из данного XML-контента, вы в итоге получаете expando-объект со свойством FirstName, значение которого равно "Dino". Однако на рис. 3 вы видите, что для заполнения expando-объекта применяется синтаксис индексатора. Это требует дополнительных пояснений. Внутри класса ExpandoObjectКласс ExpandoObject принадлежит пространству имен System.Dynamic и определен в сборке System.Core. ExpandoObject представляет объект, члены которого можно добавлять и удалять динамически в период выполнения. Класс "запечатан" (sealed) и реализует ряд интерфейсов: public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, object>, ICollection<KeyValuePair<string, object>>, IEnumerable<KeyValuePair<string, object>>, IEnumerable, INotifyPropertyChanged; Как видите, класс предоставляет свое содержимое, используя различные перечисляемые интерфейсы, в том числе IDictionary<String, Object> и IEnumerable. Кроме того, он реализует IDynamicMetaObjectProvider. Это стандартный интерфейс, обеспечивающий совместное использование объекта в Dynamic Language Runtime (DLR) программами, написанными в соответствии с DLR-моделью взаимодействия. Иначе говоря, только объекты, реализующие интерфейс IDynamicMetaObjectProvider, могут совместно использоваться динамическими .NET-языками. Expando-объект можно передать, скажем, компоненту, написанному на IronRuby. Это не так-то легко сделать в случае обычного управляемого .NET-объекта. Вернее, сделать-то можно, но вы просто не получите динамического поведения. Класс ExpandoObject также реализует интерфейс INotifyPropertyChanged. Это позволяет классу генерировать событие PropertyChanged, когда добавляется или модифицируется какой-либо член. Поддержка интерфейса INotifyPropertyChanged является ключевой в использовании expando-объектов в клиентских интерфейсах приложений Silverlight и Windows Presentation Foundation. Вы создаете экземпляр ExpandoObject так же, как и любой другой .NET-объект, но переменную для хранения экземпляра объявляете с ключевым словом dynamic: dynamic expando = new ExpandoObject(); На этом этапе, чтобы добавить свойство к expando, вы просто присваиваете ему новое значение: expando.FirstName = "Dino"; Не имеет ни малейшего значения, что в этот момент нет никакой информации о члене FirstName, его типе или области видимости. Это динамический код; именно поэтому наблюдается столь колоссальная разница, если вы используете ключевое слово var при присваивании экземпляра ExpandoObject какой-либо переменной: var expando = new ExpandoObject(); Этот код компилируется и работает нормально. Однако при таком определении присвоить какое-либо значение свойству FirstName нельзя. Класс ExpandoObject - в том виде, как он определен в System.Core, - не имеет такого члена. Точнее, у класса ExpandoObject нет открытых членов. И это ключевой момент. Когда статический тип expando является динамическим, операции связываются как динамические, в том числе операция просмотра членов. Когда статическим типом является ExpandoObject, операции связываются как обычные с просмотром членов при компиляции. Таким образом, компилятору известно, что dynamic - специальный тип, но он не знает, что ExpandoObject - специальный тип. На рис. 4 показаны варианты, предлагаемые IntelliSense в Visual Studio 2010, когда expando-объект объявляется как динамический тип и когда он интерпретируется как простой .NET-объект. В последнем случае IntelliSense показывает исходные члены System.Object плюс список методов расширения для классов-наборов. Expando-объекты в C# 4.0 Рисунок Рис. 4. Visual Studio 2010 IntelliSense и expando-объекты Также стоит отметить, что некоторые коммерческие инструменты в определенных обстоятельствах выходят за рамки этого базового поведения. На рис. 5 показан ReSharper 5.0, который захватывает список членов, определенных в объекте на данный момент. Этого не происходит, если члены добавляются программно через индексатор. Рис. 5. ReSharper 5.0 IntelliSense в работе с expando-объектами Чтобы добавить метод в expando-объект, вы просто определяете его как свойство, но используете делегат Action<T> или Func<T> для выражения его поведения. Вот пример: person.GetFullName = (Func<String>)(() => { return String.Format("{0}, {1}", person.LastName, person.FirstName); }); Метод GetFullName возвращает String, полученный комбинацией значений свойств LastName и FirstName, которые, как предполагается, доступны в expando-объекте. Если вы попытаетесь обратиться к несуществующему члену в expando-объекте, то получите исключение RuntimeBinderException. Программы, управляемые XMLЧтобы связать воедино концепции, о которых я вам рассказал, позвольте мне продемонстрировать вам один пример, где структура данных и структура UI определяются в XML-файле. Содержимое файла разбирается в набор expando-объектов и обрабатывается приложением. Однако приложение работает только с динамически представляемой информацией и не привязано к какому-либо статическому типу. В коде на рис. 3 определяется список динамически определяемых expando-объектов person. Как и следовало ожидать, если добавить новый узел в XML-схему, в expando-объекте будет создано новое свойство. Если вам нужно считать имя члена из внешнего источника, вы должны применять API индексатора для его добавления в expando. Класс ExpandoObject реализует интерфейс IDictionary<String, Object> явным образом. Это значит, что вам нужно отделить интерфейс ExpandoObject от типа словаря, чтобы использовать API индексатора или метод Add: (person as IDictionary<String, Object>)[child.Name] = child.Value; Ввиду такого поведения просто редактируйте XML-файл, чтобы делать доступными разные наборы данных. Но как использовать это динамическое изменение данных? Ваш UI должен быть достаточно гибким, чтобы принимать изменяемый набор данных. Рассмотрим простой пример, где вы лишь показываете данные через консоль. Допустим, XML-файл содержит раздел, описывающий ожидаемый UI - что бы это ни означало в вашем контексте. Для данного примера я использую следующее: <Settings> <Output Format="{0}, {1}" Params="LastName,FirstName" /> </Settings> Эта информация будет загружаться в другой expando-объект с помощью такого кода: dynamic settings = new ExpandoObject(); settings.Format = node.Attribute("Format").Value; settings.Parameters = node.Attribute("Params").Value; У основной процедуры будет следующая структура: public static void Run(String file) { dynamic settings = GetExpandoSettings(file); dynamic persons = GetExpandoFromXml(file); foreach (var p in persons) { var memberNames = (settings.Parameters as String). Split(','); var realValues = GetValuesFromExpandoObject(p, memberNames); Console.WriteLine(settings.Format, realValues); } } Expando-объект содержит формат вывода, а также имена членов, значения которых будут отображаться. Применительно к динамическому объекту person вам нужно загрузить значения для указанных членов, используя примерно такой код: public static Object[] GetValuesFromExpandoObject( IDictionary<String, Object> person, String[] memberNames) { var realValues = new List<Object>(); foreach (var m in memberNames) realValues.Add(person[m]); return realValues.ToArray(); } Поскольку expando-объект реализует IDictionary<String, Object>, вы можете использовать API индексатора для считывания и задания значений. Наконец, список значений, извлеченных из expando-объекта, передается в консоль для отображения.На рис. 6 представлены два снимка консольного приложения, вся разница между которыми определяется структурой нижележащего XML-файла. Expando-объекты в C# 4.0 Рис. 6. Два примера консольного приложения, управляемого XML-файлом Согласен, пример тривиальный, но механика, приводящая его в действие, аналогична той, которая потребуется для более интересных примеров. Попробуйте сами и поделитесь своими впечатлениями со мной |