Pull to refresh

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

Reading time7 min
Views14K
Каждый раз, когда у вас появляется новая интересная фича в языке, всегда появляются люди которые начинают выжимать из фичи максимум. 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<stringobject>));
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 для своих инфернальных целей:


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

Articles

Change theme settings