Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Это заключительная часть цикла про Dynamic Language Runtime. Предыдущие статьи:
В этой небольшой заметке мы наконец разберем основные случаи использования dynamic в реальной жизни: когда без него не обойтись и когда он может существенно облегчить существование.
Таких случаев нет. Всегда можно написать аналогичный по функционалу код в статическом стиле, разница лишь в удобстве чтения и объеме кода. Например, при работе с COM-объектами вместо dynamic можно использовать рефлексию.
В первую очередь это, конечно же, работа с COM-объектами, ради которой всё это и затевалось. Сравните код, получившийся при помощи dynamic и при помощи рефлексии:
Как правило, для работы с COM-объектами через рефлексию приходится создавать развесистые классы с обертками под каждый метод/свойство. Есть и менее очевидные плюшки типа возможности не заполнять ненужные вам параметры (обязательные с точки зрения COM-объекта) при вызове метода через dynamic.
Ещё один хрестоматийный пример — работа с конфигами, например с XML. Без dynamic:
С dynamic:
Разумеется, для этого нужно реализовать свой динамический класс. В качестве альтернативы первому листингу можно написать класс, который будет работать примерно так:
Но, согласитесь, это выглядит гораздо менее изящно, чем через dynamic.
Предыдущий пункт можно обобщить на любые действия с внешними ресурсами. У нас всегда есть две альтернативы: использование dynamic для получения кода в нативном C# стиле либо статическая типизация с «магическими строками». Давайте рассмотрим пример с REST API запросом. С dynamic можно написать так:
Где наш динамический класс по запросу свойства отправит запрос вида
Затем десериализует его и вернем нам уже готовый к целевому использованию массив кошек. Без dynamic это будет выглядеть примерно так:
В предыдущем примере у вас мог возникнуть вопрос: почему в одном случае мы десериализуем возвращаемое значение к конкретному типу, а в другом — нет? Дело в том, что в статической типизации нам нужно явно привести объекты к типу Cat для работы с ними. В случае же dynamic, достаточно десериализовать JSON в массив объектов внутри нашего динамического класса и вернуть из него object[], поскольку dynamic берёт на себя работу с рефлексией. Приведу два примера того, как это работает:
Тот же самый принцип, что и при работе с COM-объектами.
С помощью dynamic можно очень элегантно реализовать этот паттерн. Вместо тысячи слов:
Теперь при передаче объекта типа Sword в метод DoSomeWork будет вызван метод InternalDoSomeWork(Sword item).
Плюсы использования dynamic:
Минусы использования dynamic:
На мой взгляд, наибольший профит от использования dynamic разработчик получит в следующих ситуациях:
Как минимум спорным выглядит использование dynamic в сложных проектах с объемной базой кода, — здесь лучше потратить время на написание статических оберток, сведя таким образом количество неочевидных моментов к минимуму.
Если вы работаете с COM-объектами или доменами в сервисах/продуктах, подразумевающих длительное непрерывное время работы — лучше dynamic не использовать, несмотря на то, что именно для таких случаев он и создавался. Даже если вы досконально знаете что и как делать и никогда не допускаете ошибок, рано или поздно может прийти новый разработчик, который этого не знает. Итогом, скорее всего, будет трудновычислимая утечка памяти.
- Подробно о dynamic: подковерные игры компилятора, утечка памяти, нюансы производительности. В этой статье подробно рассматривается кэш DLR и важные для разработчика моменты, с ним связанные.
- Грокаем 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 не использовать, несмотря на то, что именно для таких случаев он и создавался. Даже если вы досконально знаете что и как делать и никогда не допускаете ошибок, рано или поздно может прийти новый разработчик, который этого не знает. Итогом, скорее всего, будет трудновычислимая утечка памяти.