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

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


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

    Подробнее
    Реклама

    Комментарии 35

      +2
      Вы вроде вызываете метод SomeMethod():
      dynamic dobj = new DO();
      dobj.SomeMethod();

      а исключение у вас вылетает для другого метода:
      'DynamicObjectAbuse.DO' does not contain a definition for 'NonExistentMethod'

      Так и должно быть? =) Студии 2010 к сожалению нет под рукой чтобы самому проверить(
        +2
        Так не должно быть :-) Это опечатка (равно как и в статье на gotdotnet) ;-)
          0
          Нет, автор видимо немного ошибся.
          Я только что проверил текст исключения указывает, что именно вызываемый метод не существует:
          Shell.ShellForm.Dyn does not contain a definition for 'SomeMethod'

          Да, еще автор забыл упомянуть, что нужно добавить референс на Microsoft.CSharp. Иначе компилятор ругается, что ему не хватает типа RuntimeBinder.Binder:
          Error 1 Predefined type 'Microsoft.CSharp.RuntimeBinder.Binder' is not defined or imported
          Error 2 One or more types required to compile a dynamic expression cannot be found. Are you missing references to 'Microsoft.CSharp.dll 
          and System.Core.dll'
          ?

            +2
            И, само собой, надо было указать, что фреймворк нужен не ниже 4 версии.
          +1
          В своё время я считал перечисленные возможности «фишкой» Objective-C. А теперь вижу что современные технологии стремятся в реализации таких фишек у себя, это приятно.
            +3
            Очень, очень важная мысль:
            Если коротко, то вариантов использования очень много, хотя безусловно “каноническим” программированием это безобразие не назвать. Уверен, что отсутсвие статических проверок может при неграмотном использовании наплодить кучу недетектируемых багов, поэтому мой вам совет – будьте осторожны!


            Тут все же надо понимать что добавление такого функционала (а особенно — использование его не по делу) — это как комбинирование губной гармошки с опасной бритвой — инструмент получается странный и требует особой сноровки в обращении =).
            +15
            ExpandoObject – это вообще лебединая песня.
            Фразеологизм лебединая песня (песнь) обязан своим происхождением народному поверью, по которому лебедь поет в своей жизни один раз – перед смертью. Отсюда и установившееся его значение: «Последнее, обычно наиболее значительное, произведение кого-либо; последнее проявление таланта, способностей и т. п.» («Фразеологический словарь русского языка»).

            символично, ёпт
              +2
              >Естественно, что методы такая штука сериализовывать не будет.
              Как можно сериализовать метод?
                0
                О, это rocket science. Наверное если это не Expression<T> то вообще никак, если это выражение то можно сериализовать граф. Сам я конечно не пробовал :)
                0
                Уточню, что динамическое поведение характерно не только для унаследованных от Dynamic object классов.
                «At runtime, if a dynamic object implements IDynamicMetaObjectProvider, that interface
                is used to perform the binding. If not, binding occurs in almost the same way
                as it would have had the compiler known the dynamic object’s runtime type. These
                two alternatives are called custom binding and language binding.» — C# 4.0 in a nutshell страница 162

                И еще: все вызовы перегруженных методов разрешаются во время выполнения. Среда определяет фактический тип dynamic object и делает максимально подходящий вызов,
                т.е. если у вас есть SomeMethod(object a,int b) и SomeMethod(int a, int b),
                то
                Dynamic a=3;

                SomeMethod(a,4); вызовет вторую перегрузку во время выполнения.
                  +1
                  Честно говоря, все эти штуки всё дальше переводят C# из разряда языков «ну, если скомпилилось, то есть хоть какая-то минимальная надежда, что заработает» в разряд «то, что скомпилилось вообще нифига не значит, поскольку весь реальный код будет генериться потом и на лету». Ладно, если я по ходу отладки столкнусь с сообщением в духе «нет такого метода в этом классе» или «свойство такое-то не принадлежит классу». А если пользователь потом, уже после релиза? Все-же в классическом С++ (и поначалу в .NET) такого не было, что вызывало как-то больше уверенности.
                    0
                    Никто не заставляет этим пользоваться. Другое дело что пользоваться этим так и так будут, точно так же как например пользуются PostSharp (что тоже небезопасно).
                      0
                      Есть предположение, что даже удачно скомпилированная программа не гарантирует работоспособности.
                      Работоспособность гарантируют юнит-тесты.

                      Отсюда вытекает первый постулат динамических языков программирования — строгая типизация не нужна, нужны юнит-тесты.
                      –4
                      лого на Google Wave смахивает
                        0
                        Понятно теперь откуда у Гугля ноги ростут :)
                        +4
                        Ну этим просто надо аккуратно пользоваться. Описаны паттерны, которые позволяют с использованием dynamic реализовать их намного красивее. Получившийся функционал также удобнее для дальнейшего использования.
                        Также сценарии, где используется рефлексия теперь будут удобнее в написании.
                        Кстати dynamic быстрее рефлексии.
                        Вот еще интересное, чтобы расставить все на свои места (из той же книги):
                        «Its
                        primary role is to provide runtime services to unify dynamic programming—in both
                        statically and dynamically typed languages. This means that languages such as C#,
                        VB, IronPython, and IronRuby all use a common protocol for calling functions dynamically,
                        allowing them to share libraries and call code written in other languages.
                        The DLR also makes it relatively easy to write new dynamic languages in .NET.
                        Instead of having to emit IL, dynamic language authors work at the level of expression
                        trees (the same expression trees in System.Linq.Expressions that we talked about in
                        Chapter 8).
                        The DLR further ensures that all consumers get the benefit of call-site caching, an
                        optimization whereby the DLR avoids unnecessarily repeating the potentially expensive
                        member resolution decisions made during dynamic binding.»
                          0
                          все что тут описано уже давно есть, например, в Руби. Зачем MS делает очередного монстра из красивого и стройного C# – мне не совсем понятно. Лучше бы улучшали виртуальную машину для увеличения скорости работы динамических языков и все было бы замечательно: нужна скорость – пишешь на C#, нужен динамизм – на IronRuby.
                            +3
                            Dynamic и вводили, вроде бы, для более качественной поддержки вещей типа IronRuby\IronPython.

                            Просто т.к. ручки торчат наружу — всегда найдется кто-либо кто дернет за них
                              0
                              А представляете что будет когда метапрограммирование в C# появится?
                                +1
                                0
                                «Дисциплина — главный козырь коммунизма, движущая сила армии.»

                                По рукам себя просто надо чаще бить =).
                                BTW Пример с DynamicXMLNode для меня иллюстрировал обратное — как раз когда видишь вот такие вещи

                                xe.Elements(«People»).Element(«Dmitri»).Attributes(«Name»).Value; // WTF?


                                Сразу возникает 2 желания — во первых проверить все промежуточные результаты, во вторых — переписать по-другому, чтобы была только одна проверка, благо XPath никто не отменял, типа такого

                                xe.XPathSelectElement("./People//Dmitri[@Name]");


                                А второй синтаксис, xe.people.dmitri.name, имхо ужасен, хотя и создает иллюзию статической типизации
                                  0
                                  xe.people.dmitri.name


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

                                  Или я отстал и хардкодинг уже хорошо?
                                    0
                                    во-во. Все велосипеды от незния матчасти. А если null где-то в это цепочек. А XPath для чего?
                                      0
                                      Вы о чем?
                                      Если вы посмотрите код DynamicXMLNode, то вроде как там специально делается такой сценарий (xe.people.dmitri.name), путем некоторых уловок с DynamicObject.
                                      Мне же не нравится такой сценарий именно из-за того, что создается иллюзия статической типизации ( выглядит понтово но не более), в то время как даже неаккуратный
                                      xe.Elements(«People»).Element(«Dmitri»).Attributes(«Name»).Value;
                                      вызывает желание сделать проверки ( именно из-за того что использует в том числе хардкодинг имен).

                                      Или я неправильно понял суть ваших комментариев.
                                        0
                                        Иллюзия статической типизации тут везде, это как раз считается бонусом т.к. дает писать более простой код.
                                          0
                                          Кем считается? для кого более простой? человек впервые увидевший конструкцию xe.people.dmitri.name имхо просто припухнет, пытаясь понять что есть что и кто…
                                0
                                В том-то и дело, что людям иногда нужен динамизм без дополнительных языков
                                –5
                                как я вижу, C# уверенно движется в сторону javascript :)
                                  –1
                                  пример фиговый. вместо xpath предлагается какой-то убогий дсл…
                                    +1
                                    Что-то мне получаемый код кажется катастрофически непрозрачным. А где непрозрачность, там тяжелые отладка, развитие и сопровождение. Так ли нужны эти прибамбасы? Сила в простоте.
                                      0
                                      Нужны, но редко. Я работал с рефлексией серьезно когда писал динамические запросы в LinQ (а он как мы знаем статически типизированный). В других случаях тоже что-то было, но по чуть чуть и не помню уже.
                                      Я к тому что например dynamic упростит работу с рефлексией.
                                        0
                                        А можно ли как-то dynamic использовать в visual c++?
                                          0
                                          Нет, конечно. Точноее может в C++/CLI/CX что-нибудь такое и сделали, но в обычном С++ это невозможно.
                                            0
                                            Да, я имел ввиду C++/CLI, например, такая проблема: в c# можно создать поле с динамическим объектом, например
                                            public dynamic DynObject
                                            а потом обращаться
                                            DynObject.item
                                            вместо
                                            DynObject.Collection[«item»]
                                            А как этот же функционал использовать в C++/CLI?

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое