DynamicObject и ExpandoObject
Интересные возможности при разработке в C# и .NET с использованием DLR предоставляет пространство имен System.Dynamic и в частности класс ExpandoObject. Он позволяет создавать динамические объекты, наподобие тех, что используются в javascript:
// определяем объект, который будет хранять ряд значений
dynamic person = new System.Dynamic.ExpandoObject();
person.Name = "Tom";
person.Age = 46;
person.Languages = new List<string> { "english", "german", "french" };
Console.WriteLine($"{person.Name} - {person.Age}");
foreach (var lang in person.Languages)
Console.WriteLine(lang);
// объявляем метод
person.IncrementAge = (Action<int>)(x => person.Age += x);
person.IncrementAge(6); // увеличиваем возраст на 6 лет
Console.WriteLine($"{person.Name} - {person.Age}");
</int></string>
Консольный вывод:
Tom - 46
english
german
french
Tom - 52
У динамического объекта ExpandoObject можно объявить любые свойства, например, Name, Age, Languages, которые могут представлять самые различные объекты. Кроме того, можно задать методы с помощью делегатов.
DynamicObject
На ExpandoObject по своему действию похож другой класс - DynamicObject. Он также позволяет задавать динамические объекты, но применяется в более изощренных и сложных ситуациях и когда необходим больший контроль над динамическими объектами. Тогда как ExpandoObject больше подходит для простых ситуаций, где не требуется определять какие-то специфические операции или статические компоненты.
Для использования DynamicObject надо создать свой класс, унаследовав его от DynamicObject и реализовав его методы:
TryBinaryOperation(): выполняет бинарную операцию между двумя объектами. Эквивалентно стандартным бинарным операциям, например, сложению x + y)
TryConvert(): выполняет преобразование к определенному типу. Эквивалентно базовому преобразованию в C#, например, (SomeType) obj
TryCreateInstance(): создает экземпляр объекта
TryDeleteIndex(): удаляет индексатор
TryDeleteMember(): удаляет свойство или метод
TryGetIndex(): получает элемент по индексу через индексатор. В C# может быть эквивалентно следующему выражению int x = collection[i]
TryGetMember(): получаем значение свойства. Эквивалентно обращению к свойству, например, string n = person.Name
TryInvoke(): вызов объекта в качестве делегата
TryInvokeMember(): вызов метода
TrySetIndex(): устанавливает элемент по индексу через индексатор. В C# может быть эквивалентно следующему выражению collection[i] = x;
TrySetMember(): устанавливает свойство. Эквивалентно присвоению свойству значения, например: person.Name = "Tom"
TryUnaryOperation(): выполняет унарную операцию подобно унарным операциям в C#: x++
Каждый из этих методов имеет одну и ту же модель определения: все они возвращают логическое значение, показывающее, удачно ли прошла операция. В качестве первого параметра все они принимают объект связывателя или binder. Если метод представляет вызов индексатора или метода объекта, которые могут принимать параметры, то в качестве второго параметра используется массив object[] - он хранит переданные в метод или индексатор аргументы.
Почти все операции, кроме установки и удаления свойств и индексаторов, возвращают определенное значение (например, если мы получаем значение свойства). В этом случае применяется третий параметр out object vaue, который предназначен для хранения возвращаемого объекта.
Например, определение метода TryInvokeMember():
public virtual bool TryInvokeMember (InvokeMemberBinder binder, object?[]? args, out object? result)
Параметр InvokeMemberBinder binder является связывателем - получает свойства и методы объекта, object?[]? args хранит передаваемые аргументы, out object? result предназначен для хранения выходного результата.
Рассмотрим на примере. Создадим класс динамического объекта:
using System.Dynamic;
class PersonObject : DynamicObject
{
// словарь для хранения всех свойств
Dictionary<string, object> members = new Dictionary<string, object>();
// установка свойства
public override bool TrySetMember(SetMemberBinder binder, object? value)
{
if(value is not null)
{
members[binder.Name] = value;
return true;
}
return false;
}
// получение свойства
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
result = null;
if (members.ContainsKey(binder.Name))
{
result = members[binder.Name];
return true;
}
return false;
}
// вызов метода
public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
{
result = null;
if(args?[0] is int number)
{
// получаем метод по имен
dynamic method = members[binder.Name];
// вызываем метод, передавая его параметру значение args?[0]
result = method(number);
}
// если result не равен null, то вызов метода прошел успешно
return result != null;
}
}
Класс наследуется от DynamicObject, так как непосредственно создавать объекты DynamicObject мы не можем. И также здесь переопределяется три унаследованных метода.
Для хранения всех членов класса, как свойств, так и методов, определен словарь Dictionary<string, object> members. Ключами здесь являются названия свойств и методов, а значениями - значения этих свойств.
В методе TrySetMember() производится установка свойства:
bool TrySetMember(SetMemberBinder binder, object? value)
Параметр binder хранит название устанавливаемого свойства (binder.Name), а value - значение, которое ему надо установить.
Для получения значения свойства переопределен метод TryGetMember:
bool TryGetMember(GetMemberBinder binder, out object? result)
Опять же binder содержит название свойства, а параметр result будет содержать значение получаемого свойства.
Для вызова методов определен метод TryInvokeMember:
public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
{
result = null;
if(args?[0] is int number)
{
// получаем метод по имен
dynamic method = members[binder.Name];
// вызываем метод, передавая его параметру значение args?[0]
result = method(number);
}
// если result не равен null, то вызов метода прошел успешно
return result != null;
}
Сначала с помощью bindera получаем метод и затем передаем ему аргумент args[0], предварительно приведя его к типу int, и результат метода устанавливаем в параметре result. То есть в данном случае подразумевается, что метод будет принимать один параметр типа int и возвращать какой-то результат. Если метод возвращает true, то будем считать, что вызов метод прошел успешно.
Теперь применим класс в программе:
using System.Dynamic;
// создаем объект
dynamic person = new PersonObject();
// устанавливаем ряд свойств
person.Name = "Tom";
person.Age = 23;
// определяем метод для изменения свойства Age
Func<int, int> increment = (int n) => { person.Age += n; return person.Age; };
person.IncrementAge = increment;
Console.WriteLine($"{person.Name} - {person.Age}"); // Tom - 23
person.IncrementAge(4); // применяем метод
Console.WriteLine($"{person.Name} - {person.Age}"); // Tom - 27
Выражение person.Name = "Tom" будет вызывать метод TrySetMember, в который в качестве второго параметра будет передаваться строка "Tom".
Выражение return person.Age; вызывает метод TryGetMember.
Также у объекта person определен метод IncrementAge, который представляет действия лямбда-выражения (int n) => { person.Age += n; return person.Age; };. Это выражение принимает число n, увеличивает на это число свойство Age и возвращает новое значение person.Age. И при вызове этого метода будет происходить обращение к методу TryInvokeMember. И, таким образом, произойдет приращение значения свойства person.Age.