суббота, 30 апреля 2011 г.

Создание своего Dynamic Object

 

Давайте допустим, что нам понадобилось создать свой динамический объект. Согласен, такое бывает редко, но если вдруг понадобится, сейчас в .net 4.0 есть решение как это сделать, даже два. Первое - это отнаследоваться от класса DynamicObject и переопределить необходимые методы. Второе – это использовать класс ExpendObject. Сначала рассмотрим первый вариант.

 

DynamicObject

DynamicObject - это базовый класс для указания динамического поведения во время выполнения. Этот класс должен наследоваться; непосредственно создавать его экземпляры нельзя.

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

Этот класс может быть полезным, если требуется создать более удобный протокол для библиотеки. Например, если пользователи библиотеки должны использовать синтаксис, подобный Scriptobj.SetProperty("Count", 1), можно предоставить возможность использовать гораздо более простой синтаксис, такой как scriptobj.Count = 1. Давайте рассмотрим реальный пример.

Предположим нам надо разработать класс описывающий машину. Соответственно, должны быть определены, например, такие свойства как марка, модель машины, год выпуска, цвет, и конечно же VIN номер. И допустим, что нужно иметь возможность строить такой объект во время выполнения, предварительно не зная какие свойства и методы должны быть определены.

Для реализации своего собственного динамического объекта можно переопределить методы TryGetMember, TrySetMember и TryInvokeMember класса DynamicObject. Что это за методы? Вот что написано в MSDN:

Имя

Описание

TryGetMember

Предоставляет реализацию для операций, получающих значения членов. Классы, производные от класса DynamicObject, могут переопределять этот метод, чтобы задать динамическое поведение для таких операций, как получение значения свойства.

TryInvokeMember

Предоставляет реализацию для операций, вызывающих член. Классы, производные от класса DynamicObject, могут переопределять этот метод, чтобы задать динамическое поведение для таких операций, как вызов метода.

TrySetMember

Предоставляет реализацию для операций, задающих значения членов. Классы, производные от класса DynamicObject, могут переопределять этот метод, чтобы задать динамическое поведение для таких операций, как задание значения свойства.

Ниже представлен код нашего динамического объекта:

class MyDynamicObject : DynamicObject

{

Dictionary<string, object> _data = new Dictionary<string, object>();

public int Count

{

get

{

return _data.Count;

}

}

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

string name = binder.Name.ToLower();

return _data.TryGetValue(name, out result);

}

public override bool TrySetMember(SetMemberBinder binder, object value)

{

_data[binder.Name] = value;

return true;

}

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)

{

dynamic method = _data[binder.Name];

result = method((string)args[0]);

return result != null;

}

}

В нашем примере мы будем хранить все методы, свойства или поля объкта в переменной _data типа Dictionary<string, object>. Соответственно метод TrySetMember добавляет новое поле или метод к объекту в словарь _data. Имя поля или метода хранится в передаваемой переменной binder типа SetMemberBinder в свойстве Name. Метод TryGetMember достаёт нужный метод из _data по имени.

Далее давайте используем наш, только что созданный класс.

class Program

{

static void Main(string[] args)

{

dynamic dynamicObject;

dynamicObject = "My first dynamic object.";

Console.WriteLine("Type: " + dynamicObject.GetType());

Console.WriteLine("Value: " + dynamicObject);

dynamicObject = new Car() { Model = "Volvo", Year = "1999", VIN = "IR8934GFddf34" };

Console.WriteLine("**********************************************************");

Console.WriteLine("Type: " + dynamicObject.GetType());

Console.WriteLine("Value: {0}, {1}, VIN={2}", dynamicObject.Model, dynamicObject.Year, dynamicObject.VIN);

dynamicObject = new MyDynamicObject();

dynamicObject.Model = "Volvo";

dynamicObject.Year = "1999";

dynamicObject.VIN = "IR8934GFddf34";

Console.WriteLine("**********************************************************");

Console.WriteLine("Type: " + dynamicObject.GetType());

Console.WriteLine("Value: {0}, {1}, VIN={2}", dynamicObject.Model, dynamicObject.Year, dynamicObject.VIN);

Func<string, string> ConvertToUpper = s => s.ToUpper();

dynamicObject.ConvertToUpper = ConvertToUpper;

Console.WriteLine("**********************************************************");

Console.WriteLine("Upper VIN is {0}", dynamicObject.ConvertToUpper(dynamicObject.VIN));

Console.Read();

}

Как вы видите, для добавления новый полей или методов динамическому объекту не нужно явно вызывать только что переопределённые методы, Net Framework обработает это сам, нам нужно всего лишь обратиться к полю, например dynamicObject.Model. Для добавления нового метода нужно использовать делегат Func<T, TResult>. Вот что про делегаты написано в MSDN:

public delegate TResult Func<in T, out TResult>(

T arg

)

Имя

Описание

in T

Тип параметра метода, инкапсулируемого данным делегатом.

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

out TResult

Тип возвращаемого значения метода, инкапсулируемого данным делегатом.

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

arg

Тип: T
Параметр метода, инкапсулируемого данным делегатом.

Тип: TResult

Возвращаемое значение метода, инкапсулируемого данным делегатом.

Мы в нашем примере делегату Func<T, TResult> присваиваем лямбда-выражение, которое просто все символы в строке делает большими буквами. Базовый тип лямбда-выражения должен быть одним из универсальных методов-делегатов Func. Это позволяет передавать лямбда-выражение в качестве параметра без явного присвоения его делегату. В частности, поскольку у многих методов типов в пространстве имен System.Linq есть параметры Func<T, TResult>, лямбда-выражение этим методам можно передавать без явного создания делегата Func<T, TResult>.

Вот что мы увидим в консоли, выполнив нашу программу.

Type: System.String

Value: My first dynamic object.

**********************************************************

Type: MyDynamic.Car

Value: Volvo, 1999, VIN=IR8934GFddf34

**********************************************************

Type: MyDynamic.MyDynamicObject

Value: Volvo, 1999, VIN=IR8934GFddf34

**********************************************************

Upper VIN is IR8934GFDDF34

Выполнение этого кода показывает, что объект dynamicObject меняет свой тип и отлично выполняет добавление несуществующих раннее методов и полей. Далее рассмотрим второй способ создания динамического объекта.

ExpandoObject

Класс ExpandoObject позволяет добавлять и удалять члены экземпляров во время выполнения, а также устанавливать и получать значения этих членов. Этот класс поддерживает динамическую привязку, позволяющую использовать стандартный синтаксис, например sampleObject.sampleMember.

Класс ExpandoObject реализует стандартный интерфейс среды DLR IDynamicMetaObjectProvider, который позволяет использовать экземпляры класса ExpandoObject совместно между языками, поддерживающими модель взаимодействия DLR. Например, можно создать экземпляр класса ExpandoObject в C# и передать его функции IronPython.

При использовании ExpandoObject нам не придётся переопределять какие-либо методы, можно сразу инициализировать несуществующие раннее поля или методы, а затем вызывать их.

static void MyExpando()

{

dynamic expandoObject = new ExpandoObject();

expandoObject.Model = "Volvo";

expandoObject.Year = "1999";

expandoObject.VIN = "IR8934GFddf34";

Console.WriteLine("Type: " + expandoObject.GetType());

Console.WriteLine("Value: {0}, {1}, VIN={2}", expandoObject.Model, expandoObject.Year, expandoObject.VIN);

Func<string, string> ConvertToUpper = s => s.ToUpper();

expandoObject.ConvertToUpper = ConvertToUpper;

Console.WriteLine("Upper VIN is {0}", expandoObject.ConvertToUpper(expandoObject.VIN));

}

Вот что мы увидим в консоли, выполнив нашу программу.

Type: System.Dynamic.ExpandoObject

Value: Volvo, 1999, VIN=IR8934GFddf34

Upper VIN is IR8934GFDDF34

В чём же разница между DynamicObject и ExpandoObject? Разница, конечно же, заключается в уровне контроля над объектом. При наследовании от DynamicObject можно управлять добавлением и обращением к свойствам динамического объекта, определять любое поведение взаимодействия объекта с исполняющей средой. При использовании ExpandoObject.

таких возможностей нет, зато его использование проще.

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

Пример использования

Зачем же спросите вы переопределять стандартные методы TrySetMember и TryGetMember? Самый показательный и известный пример это работа с XML. Я приведу код, и вы сами увидите как же это удобно.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Dynamic;

using System.Xml.Linq;

namespace Dynamic

{

public class CarXml: DynamicObject

{

private XElement element = null;

public CarXml(XElement element)

{

this.element = element;

}

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

result = null;

if(binder.Name == "Model")

{

result = element.Attribute("Model").Value;

return true;

}

else if(binder.Name == "Year")

{

result = element.Attribute("Year").Value;

return true;

}

else if (binder.Name == "VIN")

{

result = element.Attribute("VIN").Value;

return true;

}

return false;

}

public override string ToString()

{

dynamic cell = this;

return string.Format("Model: {0}, Year: {1}, VIN: {2}", cell.Model, cell.Year, cell.VIN);

}

}

}

Теперь мы легко можем, скормив xml нашему классу CarXml обращаться к атрибутам тэга Car, как к полям объекта.

String myXml = "<Car Model=\"Volvo\" Year=\"1999\" VIN=\"IR8934GFddf34\"></Car>";

XElement elem = XElement.Parse(myXml);

dynamic car = new CarXml(elem);

Console.WriteLine("Value: {0}, {1}, VIN={2}", car.Model, car.Year, car.VIN);

Console.WriteLine(car.ToString());

Вот что мы увидим на экране, как и ожидали.

Value: Volvo, 1999, VIN=IR8934GFddf34

Model: Volvo, Year: 1999, VIN: IR8934GFddf34

Таким образом, класс ExpandoObject является реализацией концепции динамического объекта для получения, настройки и вызова членов. Для определения типов, которые имеют свои собственные семантики динамической отправки, лучше использовать класс DynamicObject. Для определения участия динамических объектов в протоколе взаимодействия и управления быстрой динамической отправкой кэширования DLR можно создать свою собственную реализацию интерфейса IDynamicMetaObjectProvider.

Комментариев нет:

Отправить комментарий