Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Знакомимся с DynamicObject

12 комментариев

Каждый раз, когда у вас появляется новая интересная фича в языке, всегда появляются люди которые начинают выжимать из фичи максимум. DynamicObject – это как раз такая фича, которая кажется простой и понятной, но в шаловливых ручках становится более опасной затеей.

Знакомство

Для начала, давайте посмотрим – что же это за класс System.Dynamic.DynamicObject. Класс этот кажется обычным – от него можно, например, отнаследоваться и перегрузить один или несколько из его методов. Только вот методы какие-то непростые… давайте посмотрим поближе.

Сначала сделаем тестовый объект DO и отнаследуем от DynamicObject:

class DO : DynamicObject {}

Теперь, используя ключевое слово dynamic мы можем без всяких угрызений совести вызвать какой-то метод на этом объекте:

dynamic dobj = new DO();
dobj.NonExistentMethod();

Угадайте что мы получим. Получим нечто под названием RuntimeBinderException и вот это сообщение.

'DynamicObjectAbuse.DO' does not contain a definition for 'NonExistentMethod'

что естественно, т.к. метода NonExistentMethod() у нашего класса попросту нет. А интересно то, что его может никогда и не быть. В этом вся соль DynamicObject – возможность вызова свойств и методов которых у класса нет. Либо нет на момент компиляции, либо нет совсем.

Сага о несуществующих методах

Как такое получилось? Очень просто – когда мы вызываем метод, мы на самом деле вызываем метод

bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)

В случае с вызовом метода NonExistentMethod(), данный метод вызывается без аргументов, а параметр binder как раз содержит информацию о вызове.

{Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder}
    [Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder]: {Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder}
    base {System.Dynamic.DynamicMetaObjectBinder}: {Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder}
    CallInfo: {System.Dynamic.CallInfo}
    IgnoreCase: false
    Name: "NonExistentMethod"
    ReturnType: {Name = "Object" FullName = "System.Object"}

В данном случае мы получаем название метода, которое можно как-то обработать. Как – это вам решать. Тут может быть любая бизнес-логика. Опять же, есть механизмы для получения аргументов (args) и возвращения результата (result). Метод возвращает true если все прошло успешно или false если все накрылось. Возврат false из этого метода как раз породит исключение которое мы видели выше.

Что есть кроме методов?

Набор перегружаемых операций для DynamicObject впечатляет. Это прежде всего возможность реагировать на доступ к свойствам которых нет, а также на конверсии, унарные и бинарные операторы, доступ через индекс и т.п. Часть операций вообще не предназначены для C#/VB – например перехват создания объекта, удаление членов объекта, удаление объекта по индексу, и т.д.

Существует один небольшой казус – через this вы будете получать статический объект DO вместо статически типизированного динамического DO. Решение этой проблемы предсказуемо:

private dynamic This { get { return this; } }

Это на тот случай если это действительно нужно. Конечно, не стоит делать глупостей вроде вызова методов на This из TryInvokeMember() т.к. вы банально получите StackOverflowException.

ExpandoObject

ExpandoObject – это вообще лебединая песня. Этот класс позволяет пользователям произвольно добавлять методы и свойства:

dynamic eo = new ExpandoObject();
eo.Name = "Dmitri";
eo.Age = 25;
eo.Print = new Action(() =>
  Console.WriteLine("{0} is {1} years old",
  eo.Name, eo.Age));
eo.Print();

Сериализация такого объекта – задача конечно непростая т.к. он реализует IDictionary – интерфейс, который не сериалиуется например в XML из-за каких-то весьма мутных причин связанных с разрозненостью сборок и интерфейсов. Не важно. Если действительно нужно, можно воспользоваться System.Runtime.Serialization.DataContractSerializer:

var s = new DataContractSerializer(typeof (IDictionary<string, object>));
var sb = new StringBuilder();
using (var xw = XmlWriter.Create(sb))
{
  s.WriteObject(xw, eo);
}
Console.WriteLine(sb.ToString());

Естественно, что методы такая штука сериализовывать не будет. Для этого можно организовать танцы с бубном вокруг DataContractResolver, но целью этой статьи подобные практики не являются.

Что с этим делать?

ОК, вообщем-то функционал понятный с точки зрения СОМ-разработки, в которой каждое более-менее серьезное взаимодействие похоже на расчищение авгиевых конюшен. Взаимодействие с динамическими языками тоже хороший плюс, и будь я хоть сколько-то заинтересован в этом, я бы обязательно в этой статье рассказал про те binder’ы и прочие инфрастурктурные прелести, к которым все это относится.

Вот класный пример, который цитируется везде где только можно (так что надеюсь это не плагиат). Суть в том, что работая с XML, доступ к элементам и аттрибутам XElement выглядит просто нечеловечно:

var xe = XElement.Parse(something);
var name = xe.Elements("People").Element("Dmitri").Attributes("Name").Value; // WTF?

Это просто нечеловечный синтаксис. Вот гораздо более “гламурное” решение: сначала делаем DynamicObject, который своими виртуальными свойствами резолвит содержимое XElement:

public class DynamicXMLNode : DynamicObject
{
  XElement node;
  public DynamicXMLNode(XElement node)
  {
    this.node = node;
  }
  public DynamicXMLNode()
  {
  }
  public DynamicXMLNode(String name)
  {
    node = new XElement(name);
  }
  public override bool TrySetMember(
      SetMemberBinder binder, object value)
  {
    XElement setNode = node.Element(binder.Name);
    if (setNode != null)
      setNode.SetValue(value);
    else
    {
      if (value.GetType() == typeof(DynamicXMLNode))
        node.Add(new XElement(binder.Name));
      else
        node.Add(new XElement(binder.Name, value));
    }
    return true;
  }
  public override bool TryGetMember(
      GetMemberBinder binder, out object result)
  {
    XElement getNode = node.Element(binder.Name);
    if (getNode != null)
    {
      result = new DynamicXMLNode(getNode);
      return true;
    }
    else
    {
      result = null;
      return false;
    }
  }
}

Теперь дело за малым – если нужно свойство, можно его просто брать через точку:

var xe = XElement.Parse(something);
var dxn = new DynamicXmlNode(xe);
var name = dxn.people.dmitri.name;

Монады и AOP

В очередной раз хочу заметить, что имея возможность вот так вот контролировать доступ к объектам, мы можем навешивать на них AOP в стиле Unity.Interceptor или другого IoC+AOP фреймворка который работает на динамических проксях. Например, в примере чуть выше, мы можем гарантировать что у нас никогда не будет выброшен NullReferenceException, даже если один из элементов в цепочке действительно null. Для этого правда придется сделать фейк-объекты, но это сродни создания промежуточных классов для fluent-интерфейсов.

DSL’ки

Конечно, возможность “писать все что угодно” в классах подводит нас к такой идее что в принципе можно на базе этого строить DSLи, которые никак статически не будут проверяться (в отличии от синтаксиса в стиле MPS), но могут быть использованы для того, чтобы описывать какие-то хитрые доменные языки.

“Стоп”, скажете вы, “но не проще ли использовать строки, генераторы и прочую метаинфраструктуру?” На самом деле все зависит от того, как это смотреть. Например, наш пример с DynamicXmlNode это и есть DSL для которой XML является доменом. Точно так же, я могу например написать следующее:

myobject.InvokeEachMethodThatBeginsWithTest()

Мораль в том, что в нашем DynamicObject мы будем тупо парсить строку InvokeEachMethod... и потом реагировать на нее соответственно. В данном случае – будем использовать reflection. Конечно это значит что любое использование этого функционала в качестве DSL является а)совсем недокументированно и стороннему человеку непонятно; и б)органичено правилами по именованию идентификаторов. Не получится например скомпилировать следующее:

DateTime dt = (DateTime)timeDO.friday.13.fullmoon;

Зато вот скомпилировать friday13 получится. Впрочем, уже сейчас существуют (и наверняка используются в продакшн) методы расширения вроде July() которые позволяют писать весьма криптичный код вроде 4.July(2010). Как по мне так это совсем не круто.

Ссылки на примеры

Вот несколько примеров того, как толковые люди используют механизм DynamicObject для своих инфернальных целей:

Если коротко, то вариантов использования очень много, хотя безусловно “каноническим” программированием это безобразие не назвать. Уверен, что отсутсвие статических проверок может при неграмотном использовании наплодить кучу недетектируемых багов, поэтому мой вам совет – будьте осторожны!

Реклама

Written by Dmitri

20 июня 2010 в 23:10

Опубликовано в .NET, C#

комментариев 12

Subscribe to comments with RSS.

  1. Хорошая статейка. Наводит на кое-какие размышления. Спасибо!

    Calabonga

    21 июня 2010 at 1:45

  2. В класс DynamicXMLNode можно еще добавить такое (вкратце):

    public string this[string attributeName]
    {
    get
    {
    return node.Attribute(attributeName).Value;
    }
    set
    {
    node.Attribute(attributeName).SetValue(value);
    }
    }

    Farsight

    22 июня 2010 at 10:05

    • Да, можно, но это возврат к синтаксису со строками.

      Dmitri

      22 июня 2010 at 11:57

      • Согласен. Только тогда непонятно, как вы через Ваш код вытаскиваете атрибут name?

        Farsight

        24 июня 2010 at 8:53

  3. У меня код

    var xe = XElement.Parse(something);
    var dxn = new DynamicXmlNode(xe);
    var name = dxn.people.dmitri.name;

    работать не захотел, уже на dxn.people не стал компилироваться

    Вадим

    1 июля 2010 at 19:03

    • Тут просто вместо var нужно использовать dynamic.

      Dmitri

      14 марта 2011 at 15:28

  4. Кстати, если все данные находятся в атрибутах Xml-элементов — данный подход теряет смысл.
    Или я не очень прав !?

    Вадим

    2 июля 2010 at 2:09

  5. Вместо этого кода:
    var dxn = new DynamicXmlNode(xe);
    Нужно использовать этот:
    dynamic dxn = new DynamicXmlNode(xe);
    Тогда скомпилится и даже заработает :)

    Kavayii

    6 декабря 2010 at 21:48


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: