Работа с динамическим типом в C#
Введение термина “динамический” в .NET 4.0 приводит к сдвигу парадигмы для программирования на C #. Для программистов на C # динамическое поведение поверх сильной системы может оказаться неправильным. Это похоже на шаг назад, когда вы теряете безопасность типа во время компиляции.
Динамическое программирование может привести к тому, что вы столкнетесь с ошибками времени выполнения. Объявление динамической переменной, которая может мутировать во время выполнения, пугает. Качество кода страдает, когда разработчики делают неправильные предположения о данных.
Для программистов на C # логично избегать динамического поведения в коде. Есть преимущества для классического подхода наличия сильных типов. Хорошая обратная связь по типам данных с помощью проверки типов имеет первостепенное значение для рабочих программ. Система хорошего типа передает намерение и уменьшает двусмысленность в коде.
Что говорит Dynamic Language Runtime о C #? .NET предлагает систему богатого типа, полезную для написания программного обеспечения корпоративного уровня. Давайте подробнее рассмотрим значение слова “динамический” и изучим, что оно может сделать.
Иерархия типов
Каждый тип в Common Language Runtime (CLR) наследуется от System.Object. Теперь прочитайте это последнее предложение еще раз, пока вы его не усвоите. Это означает, что тип объекта является общим родителем для всей системы типов. Этот факт сам по себе помогает нам, когда мы достигаем более странного динамического поведения. Идея здесь заключается в разработке этой «кодовой зависимости», поэтому вы знаете, как перемещаться по динамическим типам на C #.
Чтобы продемонстрировать это, вы можете написать следующую программу:
Console.WriteLine("long inherits from ValueType: " + typeof(long).IsSubclassOf(typeof(ValueType))); |
Мы будем опускать использование операторов до конца этой статьи, чтобы сфокусироваться на образцах кода. Затем мы пройдемся по каждому именному пространству и тому, что оно делает. Это удержит нас от необходимости повторяться и дает возможность проанализировать все типы.
Приведенный выше код оценивается как True внутри консоли. Длительный тип в .NET - это тип значения, поэтому он больше похож на перечисление или структуру. ValueType переопределяет поведение по умолчанию, исходящее от класса объекта.Наследники ValueType идут в стек, который имеет короткий срок службы и более эффективен.
Чтобы проверить, что ValueType наследуется от System.Object, выполните следующие действия:
Console.WriteLine("ValueType inherits from System.Object: " +
typeof(ValueType).IsSubclassOf(typeof(Object)));
Это оценивается как True. Это цепочка наследования, возвращающаяся в System.Object. Для типов значений в цепочке есть как минимум два родителя.
Взгляните на другой тип C #, который спускается из System.Object, например:
Console.WriteLine("string inherits from System.Object: " +
typeof(string).IsSubclassOf(typeof(Object)));
Этот код выбрасывает True в консоль. Другим типом, который наследуется от объекта, являются ссылочные типы. Они распределяются по куче и проходят сбор мусора. CLR управляет ссылочными типами и освобождает их от кучи, когда это необходимо.
Посмотрите на следующий рисунок, чтобы вы могли визуализировать систему типов CLR:
И типы значений, и ссылочные типы являются основными строительными блоками CRL. Эта элегантная система типов предшествует как .NET 4.0, так и динамическим типам. Рекомендуем держать эту схему в голове, когда вы работаете с типами на C #. Итак, как DLR вписывается в эту картину?
Dynamic Language Runtime
Dynamic Language Runtime (DLR) - удобный способ работы с динамическими объектами. Например, скажем, что у вас есть данные как XML или JSON, где члены не известны заранее. DLR позволяет использовать естественный код для работы с объектами и доступа к членам.
В случае C # это позволяет работать с библиотеками, где типы не известны во время компиляции. Динамический тип исключает магические строки в коде для естественного API. Это разблокирует динамические языки, которые находятся поверх CRL, таких как IronPython.
Подумайте о DLR как о поддержке трех основных услуг:
- Деревья выражений, которые поступают из пространства имен System.Linq.Expressions. Компилятор генерирует деревья выражений во время выполнения, которые поддерживают динамическую языковую совместимость. Динамические языки выходят за рамки этой статьи, и мы не будем здесь их освещать.
- Кэширование точек вызова, которое кэширует результаты динамических операций. DLR кэширует операцию типа a + b и сохраняет характеристики a и b. Когда выполняется динамическая операция, DLR извлекает информацию, доступную из предыдущих операций.
- Динамическая совместимость объектов - это типы C #, которые вы можете использовать для доступа к DLR. Эти типы включают DynamicObject и ExpandoObject. Существует больше типов, но обратите внимание на эти два при работе с динамическим типом.
Чтобы увидеть, как DLR и CLR подходят друг другу, просмотрите эту схему
DLR находится поверх CRL. Мы говорили, что каждый тип происходит из System.Object. Мы применили его к CLR, но как насчет DLR? Протестируйте эту теорию с помощью этой программы:
Console.WriteLine("ExpandoObject inherits from System.Object: " +
typeof(ExpandoObject).IsSubclassOf(typeof(Object)));
Console.WriteLine("DynamicObject inherits from System.Object: " +
typeof(DynamicObject).IsSubclassOf(typeof(Object)));
И ExpandoObject, и DynamicObject оцениваются как True в командной строке. Подумайте об этих двух объектах как об основных строительных блоках для работы с динамическим типом. Это ясно показывает, как совпадают обе среды выполнения.
Сериализатор JSON
Одна из проблем, решаемая динамическим типом, заключается в том, что у вас есть HTTP-запрос JSON, где члены неизвестны. Скажем, есть такой произвольный JSON, который вы хотите использовать в C #. Чтобы решить эту проблему, сериализуйте этот JSON в динамический тип C #.
Мы будем использовать сериализатор Newtonsoft, вы можете добавить эту зависимость через NuGet, например:
dotnet add package Newtonsoft.Json –-version 11.0.2
Вы можете использовать этот сериализатор для работы с ExpandoObject и DynamicObject. Изучите, что каждый динамический тип приносит динамическому программированию.
Динамический тип ExpandoObject
ExpandoObject - это удобный тип, который позволяет устанавливать и извлекать динамические элементы. Он реализует IDynamicMetaObjectProvider, который позволяет обмениваться экземплярами между языками в DLR. Поскольку он реализует IDictionary и IEnumerable, он работает с типами из CLR. Это позволяет, например, экземпляру объекта ExpandoObject работать с IDictionary. Затем перечислите члены, как и любой другой тип IDictionary.
Чтобы использовать ExpandoObject с произвольным JSON, вы можете написать следующую программу:
dotnet add package Newtonsoft.Json –-version 11.0.2
Обратите внимание, что хотя это динамический JSON, он связывается с типами C # в среде CLR. Поскольку тип номера неизвестен, сериализатор по умолчанию выбирает самый большой тип, который длинный. Обратите внимание, что мы безопасно вывели результаты сериализации в динамический тип с нулевыми проверками. Причина в том, что сериализатор возвращает тип объекта из среды CLR. Поскольку ExpandoObject наследуется от System.Object, его можно распаковать в DLR-тип.
Чтобы быть модными, перечислите exObj с IDictionary:
foreach (var exObjProp in exObj as IDictionary<string, object>
?? new Dictionary<string, object>())
{
Console.WriteLine($"IDictionary = {exObjProp.Key}: {exObjProp.Value}");
}
Это напечатает IDictionary = a: 1 в консоли. Обязательно используйте строку и объект в качестве типов ключей и значений. В противном случае во время преобразования будет выведено исключение RuntimeBinderException.
Динамический тип DynamicObject
DynamicObject предлагает точный контроль над динамическим типом. Вы наследуете от этого типа и переопределяете динамическое поведение. Например, вы можете определить, как устанавливать и получать динамические элементы в типе. DynamicObject позволяет вам выбирать динамические операции для реализации через переопределения. Это обеспечивает более легкий доступ, чем у языкового исполнителя, который реализует IDynamicMetaObjectProvider. Это абстрактный класс, поэтому он наследует от этого, а не создает его. Этот класс имеет 14 виртуальных методов, которые определяют динамические операции над типом. Каждый виртуальный метод позволяет переопределениям задавать динамическое поведение.
Допустим, что вам нужен точный контроль над тем, что входит в динамический JSON. Хотя вы заранее не знаете свойства, с DynamicObject, вы получаете контроль над типом.
Давайте переопределим три метода: TryGetMember, TrySetMember и GetDynamicMemberNames:
public class TypedDynamicJson<T> : DynamicObject
{
private readonly IDictionary<string, T> _typedProperty;
public TypedDynamicJson()
{
_typedProperty = new Dictionary<string, T>();
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
T typedObj;
if (_typedProperty.TryGetValue(binder.Name, out typedObj))
{
result = typedObj;
return true;
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (value.GetType() != typeof(T))
{
return false;
}
_typedProperty[binder.Name] = (T)value;
return true;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _typedProperty.Keys;
}
}
C# генерирует сильный тип _typedProperty в общем виде, который управляет типами членов. Это означает, что тип свойства относится к типу T. Динамические члены JSON находятся внутри словаря и сохраняют только общий тип. Этот динамический тип допускает однородное множество элементов того же типа. Хотя он допускает динамический набор членов, вы можете твердо определить поведение.
Допустим, вы заботитесь только о длинных типах от произвольного JSON:
var dynObj = JsonConvert.DeserializeObject<TypedDynamicJson<long>>(
"{\"a\":1,\"b\":\"1\"}") as dynamic;
Console.WriteLine($"dynObj.a = {dynObj?.a}, type of {dynObj?.a.GetType()}");
var members = string.Join(",", dynObj?.GetDynamicMemberNames());
Console.WriteLine($"dynObj member names: {members}");
В результате вы увидите одно свойство со значением 1, потому что второе свойство является строковым типом. Если вы измените общий тип на строку, он вместо этого возьмет второе свойство.
Результаты
До сих пор было изучено немного основы; вот некоторые основные моменты:
- Все типы из CLR и DLR наследуются от System.Object
- DLR - это место, где происходят все динамические операции
- ExpandoObject реализует перечисляемые типы из CLR, такие как IDictionary
- DynamicObject имеет точный контроль над динамическим типом посредством виртуальных методов
Посмотрите результаты, полученные в консоли:
Будут единичные тесты
Для модульных тестов мы будем использовать тестовую среду xUnit. В .NET Core вы добавляете тестовый проект с новой командой xunit. Одна из проблем, которая становится очевидной, - имитация и проверка динамических параметров. Например, предположим, что вы хотите проверить, существует ли вызов метода с динамическими свойствами.
Чтобы использовать библиотеку Moq mock, вы можете добавить эту зависимость через NuGet, например:
dotnet add package Moq –-version 4.10.0
Скажем, у вас есть интерфейс, и идея состоит в том, чтобы проверить, что он вызван с правильным динамическим объектом:
public interface IMessageBus
{
void Send(dynamic message);
}
Игнорируйте, что реализует этот интерфейс. Эти детали реализации не нужны для написания модульных тестов. Это будет тестируемая система:
public class MessageService
{
private readonly IMessageBus _messageBus;
public MessageService(IMessageBus messageBus)
{
_messageBus = messageBus;
}
public void SendRawJson<T>(string json)
{
var message = JsonConvert.DeserializeObject<T>(json) as dynamic;
_messageBus.Send(message);
}
}
Вы можете использовать generics, поэтому вы можете передать динамический тип для сериализатора. Затем вызовите IMessageBus и отправьте динамическое сообщение. Тестируемый метод принимает строковый параметр и выполняет вызов с динамическим типом.
Для модульных тестов инкапсулируйте его в класс MessageServiceTests. Начните с инициализации mocks и тестируемой службы:
public class MessageServiceTests
{
private readonly Mock<IMessageBus> _messageBus;
private readonly MessageService _service;
public MessageServiceTests()
{
_messageBus = new Mock<IMessageBus>();
_service = new MessageService(_messageBus.Object);
}
}
IMessageBus получает имитацию, используя C # generic в библиотеке Moq. Затем создайте экземпляр mock с использованием свойства Object. Частные переменные экземпляра полезны во всех модульных тестах. Частные экземпляры с высокой степенью повторного использования добавляют целостность класса.
Чтобы проверить вызов с помощью Moq, необходимо предпринять интуитивный подход:
_messageBus.Verify(m => m.Send(It.Is<ExpandoObject>(
o => o != null && (o as dynamic).a == 1)));
Но, увы, сообщение об ошибке, которое вы увидите, будет следующим: «Дерево выражений не может содержать динамическую операцию». Это связано с тем, что выражения лямбда C # не имеют доступа к DLR. Он ожидает, что из CLR будет выбран тип, который затрудняет проверку этого динамического параметра. Помните свое обучение и используйте свою кодовую зависимость, чтобы решить эту проблему.
Чтобы перемещаться по тому, что кажется несоответствием между типами, используйте метод обратного вызова:
dynamic message = null;
_messageBus.Setup(m => m.Send(It.IsAny<ExpandoObject>()))
.Callback<object>(o => message = o);
Обратите внимание, что обратный вызов набирается в System.Object. Поскольку все типы наследуются от типа объекта, вы можете сделать присвоение динамическому типу. C # может распаковать объект внутри выражения лямбда в динамическое сообщение.
Время написать хороший модульный тест для типа ExpandoObject. Используйте xUnit в качестве рамки тестирования, поэтому вы увидите метод с атрибутом Fact.
[Fact]
public void SendsWithExpandoObject()
{
// arrange
const string json = "{\"a\":1}";
dynamic message = null;
_messageBus.Setup(m => m.Send(It.IsAny<ExpandoObject>()))
.Callback<object>(o => message = o);
// act
_service.SendRawJson<ExpandoObject>(json);
// assert
Assert.NotNull(message);
Assert.Equal(1, message.a);
}
Тестируйте с типом DynamicObject, повторно используя TypedDymaicJson, которое вы видели раньше:
[Fact]
public void SendsWithDynamicObject()
{
// arrange
const string json = "{\"a\":1,\"b\":\"1\"}";
dynamic message = null;
_messageBus.Setup(m => m.Send(It.IsAny<TypedDynamicJson<long>>()))
.Callback<object>(o => message = o);
// act
_service.SendRawJson<TypedDynamicJson<long>>(json);
// assert
Assert.NotNull(message);
Assert.Equal(1, message.a);
Assert.Equal("a", string.Join(",", message.GetDynamicMemberNames()));
}
Используя генераторы C #, вы можете менять динамические типы для сериализатора при повторном использовании кода. Метод обратного вызова в Moq позволяет вам сделать необходимый перерыв между системами типов. Наличие элегантной иерархии типов с общим родителем превращается в спасателя.
Использование выражений
Следующие примеры использования являются частью примеров кода:
- Система: базовые типы CRL, такие как Object и Console
- System.Collections.Generic: Перечислимые типы, такие как IDictionary
- System.Dynamic: динамические типы DLR, такие как ExpandoObject и DynamicObject
- Newtonsonft.Json: сериализатор JSON
- Moq: Mocking library
- Xunit: Тестирование
Заключение
Динамический тип C# сперва может показаться пугающим , но имеет преимущества поверх строго типизированной системы. DLR - это место, где происходят все динамические операции и взаимодействуют с CRL. Наследование типов упрощает работу с системами обоих типов одновременно. В C# нет никакой враждебности между динамическим и статическим программированием. Обе типовые системы работают вместе, чтобы решить динамические проблемы творчески.