Это заключительная часть цикла про Dynamic Language Runtime. Предыдущие статьи:

  1. Подробно о dynamic: подковерные игры компилятора, утечка памяти, нюансы производительности. В этой статье подробно рассматривается кэш DLR и важные для разработчика моменты, с ним связанные.
  2. Грокаем DLR. Общий обзор технологии, препарирование DynamicMetaObject и небольшая инструкция о том, как создать собственный динамический класс.

В этой небольшой заметке мы наконец разберем основные случаи использования dynamic в реальной жизни: когда без него не обойтись и когда он может существенно облегчить существование.



Когда без dynamic не обойтись


Таких случаев нет. Всегда можно написать аналогичный по функционалу код в статическом стиле, разница лишь в удобстве чтения и объеме кода. Например, при работе с COM-объектами вместо dynamic можно использовать рефлексию.

Когда dynamic полезен


Работа с COM-объектами


В первую очередь это, конечно же, работа с COM-объектами, ради которой всё это и затевалось. Сравните код, получившийся при помощи dynamic и при помощи рефлексии:

dynamic instance = Activator.CreateInstance(type);
instance.Run("Notepad.exe");

var instance = Activator.CreateInstance(type);
type.InvokeMember("Run", BindingFlags.InvokeMethod, null, instance, new[] { "Notepad.exe" });

Как правило, для работы с COM-объектами через рефлексию приходится создавать развесистые классы с обертками под каждый метод/свойство. Есть и менее очевидные плюшки типа возможности не заполнять ненужные вам параметры (обязательные с точки зрения COM-объекта) при вызове метода через dynamic.

Работа с конфигами


Ещё один хрестоматийный пример — работа с конфигами, например с XML. Без dynamic:

XElement person = XElement.Parse(xml);
	
Console.WriteLine(
	$"{person.Descendants("FirstName").FirstOrDefault().Value} {person.Descendants("LastName").FirstOrDefault().Value}"
	);

С dynamic:

dynamic person = DynamicXml.Parse(xml);

Console.WriteLine(
	$"{person.FirstName} {person.LastName}"
);

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

var person = StaticXml.Parse(xml);

Console.WriteLine(
	$"{person.GetElement("FirstName")} {person.GetElement("LastName")}"
);

Но, согласитесь, это выглядит гораздо менее изящно, чем через dynamic.

Работа с внешними ресурсами


Предыдущий пункт можно обобщить на любые действия с внешними ресурсами. У нас всегда есть две альтернативы: использование dynamic для получения кода в нативном C# стиле либо статическая типизация с «магическими строками». Давайте рассмотрим пример с REST API запросом. С dynamic можно написать так:

dynamic dynamicRestApiClient = new DynamicRestApiClient("http://localhost:18457/api");
dynamic catsList = dynamicRestApiClient.CatsList;

Где наш динамический класс по запросу свойства отправит запрос вида

[GET] http://localhost:18457/api/catslist

Затем десериализует его и вернем нам уже готовый к целевому использованию массив кошек. Без dynamic это будет выглядеть примерно так:

var restApiClient = new RestApiClient("http://localhost:18457/api");
var catsListJson = restApiClient.Get("catsList");
var deserializedCatsList = JsonConvert.DeserializeObject<Cat[]>(catsListJson);

Замена рефлексии


В предыдущем примере у вас мог возникнуть вопрос: почему в одном случае мы десериализуем возвращаемое значение к конкретному типу, а в другом — нет? Дело в том, что в статической типизации нам нужно явно привести объекты к типу Cat для работы с ними. В случае же dynamic, достаточно десериализовать JSON в массив объектов внутри нашего динамического класса и вернуть из него object[], поскольку dynamic берёт на себя работу с рефлексией. Приведу два примера того, как это работает:

dynamic deserialized = JsonConvert.DeserializeObject<object>(serialized);
var name = deserialized.Name;
var lastName = deserialized.LastName;

Attribute[] attributes = type.GetCustomAttributes(false).OfType<Attribute>();
dynamic attribute = attributes.Single(x => x.GetType().Name == "DescriptionAttribute");
var description = attribute.Description;

Тот же самый принцип, что и при работе с COM-объектами.

Visitor


С помощью dynamic можно очень элегантно реализовать этот паттерн. Вместо тысячи слов:

public static void DoSomeWork(Item item)
{
    InternalDoSomeWork((dynamic) item);
}

private static void InternalDoSomeWork(Item item)
{
    throw new Exception("Couldn't find handler for " + item.GetType());
}

private static void InternalDoSomeWork(Sword item)
{
   //do some work with sword
}

private static void InternalDoSomeWork(Shield item)
{
    //do some work with shield
}

public class Item { }
public class Sword : Item {}
public class Shield : Item {}

Теперь при передаче объекта типа Sword в метод DoSomeWork будет вызван метод InternalDoSomeWork(Sword item).

Выводы


Плюсы использования dynamic:

  • Можно использовать для быстрого прототипирования: в большинстве случаев уменьшается количество бойлерплейт кода
  • Как правило улучшает читаемость и эстетичность (за счет перехода от «магических строк» к нативному стилю языка) кода
  • Несмотря на распространенное мнение, благодаря механизмам кэширования существенного оверхеда по производительности в общем случае не возникает

Минусы использования dynamic:

  • Есть неочевидные нюансы, связанные с памятью и производительностью
  • При поддержке и чтении таких динамических классов нужно хорошо понимать, что вообще происходит
  • Программист лишается проверки типов и всех гарантий работоспособности, предоставляемых компилятором

Заключение


На мой взгляд, наибольший профит от использования dynamic разработчик получит в следующих ситуациях:

  • При прототипировании
  • В небольших/домашних проектах, где цена ошибки невысока
  • В небольших по объему кода утилитах, не подразумевающих длительное время работы. Если ваша утилита исполняется в худшем случае несколько секунд, задумываться об утечках памяти и снижении производительности обычно нет нужды

Как минимум спорным выглядит использование dynamic в сложных проектах с объемной базой кода, — здесь лучше потратить время на написание статических оберток, сведя таким образом количество неочевидных моментов к минимуму.

Если вы работаете с COM-объектами или доменами в сервисах/продуктах, подразумевающих длительное непрерывное время работы — лучше dynamic не использовать, несмотря на то, что именно для таких случаев он и создавался. Даже если вы досконально знаете что и как делать и никогда не допускаете ошибок, рано или поздно может прийти новый разработчик, который этого не знает. Итогом, скорее всего, будет трудновычислимая утечка памяти.

Комментарии (10)


  1. pankraty
    11.10.2019 12:34
    +1

    Вариант с Visitor-ом любопытный, но в данном случае использование dynamic-ов добавляет не так уж много преимуществ. У меня как-то был похожий случай, где как раз применение dynamic-ов сильно помогло.


    Во внешней библиотеки определены 2 класса, не имеющие общего предка, но имеющие несколько идентичных полей, с которыми должен работать наш метод.
    Будь это наши классы, это можно было бы решить добавлением интерфейса с необходимыми полями, но т.к. это внешняя библиотека — не вариант.


    Решение:


    public class ExternalClassA
    {
      // lots of properties
      public string PropertyA {get; }
      public int PropertyB { get; }
      // lots of other properties
    }
    
    public class ExternalClassB
    {
      // lots of properties
      public string PropertyA {get; }
      public int PropertyB { get; }
      // lots of other properties
    }
    
    public class OurClass
    {
      public void DoSomeWork(ExternalClassA subject)
      {
         DoSomeWorkInternal((dynamic) subject);
      }
    
      public void DoSomeWork(ExternalClassB subject)
      {
         DoSomeWorkInternal((dynamic) subject);
      }
    
      private void DoSomeWorkInternal(dynamic subject)
      {
         // here we can safely operate with subject.PropertyA, subject.PropertyB
      }
    }
    

    Т.е. под каждую реализацию, с которой мы планируем работать, создаем публичный метод, приводящий аргумент к dynamic и пробрасывающий вызовы в закрытый метод. Внутри него мы точно знаем, что извне не придет чего-то неподдерживаемого, а от изменений сигнатуры внешних классов страхуемся юнит-тестами.


  1. Nikolai46
    11.10.2019 18:31
    +2

    Было бы неплохо перечислить недостатки dynamic-ов отдельной секцией.

    Для работы с внешними ресурсами например существует возможность автогенерировать код сервиса (если есть wsdl, soap или подобное) и для клиента код получается таким же простым

    var restApiClient = new RestApiClient();
    List<Cat> catsList = await restApiClient.GetCatsList();

    Но в случае с dynamic не будет выявлено проблем на этапе компиляции.


    1. Lelushak Автор
      11.10.2019 21:20

      Было бы неплохо перечислить недостатки dynamic-ов отдельной секцией.


      Я выделил секцию «минусы» в разделе выводов. Недостатков, достойных упоминания, довольно мало. Добавлю туда ещё отсутствие проверки типов

      Для работы с внешними ресурсами например существует возможность автогенерировать код сервиса


      В общем случае такой возможности все-таки нет или генератор придется писать самостоятельно. К этому зачастую и сводится вопрос об использовании dynamic — выбор между кодом с проверкой типов и кодом с небольшим объемом


      1. Nikolai46
        11.10.2019 21:54
        +1

        Я выделил секцию «минусы» в разделе выводов. Недостатков, достойных упоминания, довольно мало. Добавлю туда ещё отсутствие проверки типов
        — Основной недостаток не приведён — это проверка на этапе компиляции vs runtime. То что это «no-no» с точки зрения правильных практик. Акцентировать на этом и объяснить почему. Чтобы на подкорке закрепилось у тех кто уголочки любит срезать.

        — Потом что через reflection с ним работать не очень.

        — И то что можно сделать такую магию через них, которая будет переусложнена
        В общем случае такой возможности все-таки нет или генератор придется писать самостоятельно.
        Какой нибудь swagger поможет в 80% случаев.


        1. Nikolai46
          11.10.2019 22:40
          +2

          Да, ещё один весомый недостаток (вытекающий из reflection/runtime ограничений) — нету правильной поддержки со стороны IDE.


  1. MorganNNJ
    11.10.2019 21:22

    Раз в тегах стоит dlr то без использования dynamic во ViewModel статья выглядит как черновик.


    1. Lelushak Автор
      11.10.2019 21:28

      Можете привести пример? Не встречал такого использования


      1. Nikolai46
        11.10.2019 21:58

        Очень часто используют dynamic как ViewBag и т.п. в cshtml шаблонах которые строятся через Razor.

        Я например использовал массив dynamic объектов чтобы в UI строить таблицу когда я незнаю точное количество полей и их тип. Через dynamic очень компактненько получалось.


        1. RajaKajiev
          12.10.2019 21:48

          Если сможете развернуть этот коментарий в статью хотя б экрана на три — цены ей не будет! ;)


      1. MorganNNJ
        11.10.2019 22:09

        ViewBag в MVC как пример модели. Для WPF реализация DynamicObject с интересами INotifyPropertyChanged и INotifyDataErrorInfo ну и соотв использование для разных несложных форм.