RTTI#4
Опубликовано 30.07.2011 г. 00:49
Недавно Дэвид Интерсимоне написал пост, о том, откуда приходит вдохновение (: Так что наравне с вариантом "Inspiration happens in the shower" идея этой заметки явилась, когда я мыл посуду пару дней назад.
Допустим, что мы активно используем атрибуты в которых храним некоторые значения для классов. И нам не очень удобно извлекать их стандартным путем. Т.е необходмио создать контекст RTTI, получить тип для нашего класса, извлечь список атрибутов, и, наконец, найти тот, что удовлетворяет нашим критериям. А идея, пришедшая в голову, состояла в том, что можно расширить TObject для извлечения значений свойств атрибутов. На помощь в этом вопросе нам придут классы помощники (class helpers) и обобщения (дженерики/generics). Классы помощники как раз и были созданы с целью, чтобы расширять функционал классов, которые мы не можем изменить. Никто в здравом уме не будет ведь расширять TObject. Хотя в личных целях, можно завести собственный предок для классов. Но в данном случае мы используем помощника для базового класса, хотя мне такой подход не нравится (такое используется в библиотеке SuperObject для JSON). Итак, наш тестовый атрибут будет иметь следующий вид (просто пара тестовых полей):
TTestAttribute = class(TCustomAttribute) strict private FIntValue : integer; FStringValue : string; public constructor Create(iValue : integer; sValue : string); property IntValue : integer read FIntValue; property StringValue : string read FStringValue; end;а для теста будем использовать такой класс с указанием атрибута:
[TTestAttribute(123, 'qweasdzxc')] TTest = class(TObject);суть задачи в том, что допустим в коде мы хотим быстро получать значения поля атрибута для класса. Т.е у меня есть экземпляр объекта t класса TTest. и я хочу извлечь атрибут TTestAttribute и значение его свойства IntValue. Рассмотрит такой вариант класса помощника:
TAttributeGetter = class helper for TObject public function getAttribute<T : TCustomAttribute>() : T; function getAttributeValue<T : TCustomAttribute; V>(PropName : string) : V; overload; function getAttributeValue<T : TCustomAttribute>(PropName : string) : TValue; overload; end;Для только изучающих Delphi, наверное, стоит обратить внимание на то, что здесь использованы следующие вещи:
- Непосредственно описан класс помощник для TObject. Классы помощники позволяют расширять функционал исходного класса не прибегая к наследованию в случаях, когда менять базовый класс крайне не желательно
- Функции используют обобщения. (сам класс при этом не является обобщенным)
- Обобщения имеют ограничения а также используется не только один, но и два нетипизированных параметра.
- Используются функции с одинаковым именем, но разной сигнатурой вызова
var t : TTest; a : TTestAttribute; begin t := TTest.Create(); a := t.getAttribute<TTestAttribute>();Начальная реализация метода имела такой вид:
function TAttributeGetter.getAttribute<T>(): T; var ctx : TRttiContext; rt : TRttiType; at : TRttiType; a : TCustomAttribute; begin ctx := TRttiContext.Create(); try rt := ctx.GetType(self.ClassType); at := ctx.GetType(typeInfo(T)); for a in rt.GetAttributes() do begin if a is at.AsInstance.MetaclassType then exit(T(a)); end; finally ctx.Free(); end; end;С помощью контекста мы получаем RTTI информацию о классе, к которому применяем действие т.е self. А также получаем информацию о запрашиваем классе параметра - т.е T. Следующим шагом мы извлекаем список атрибутов. Здесь нам надо определить нужный атрибут. Обычно мы делаем проверку вида a is TTestAttribute но в данном случаем мы не знаем имя конечного атрибута, он у нас T. С подобной реализацией возникает одна проблема. Все что касается RTTI работает в контексте TRttiContext. За очистку памяти отвечает ctx.Free(). Так что наши атрибуты будут разрушены. Я не совсем знаю как создавать копии объектов, но все сводится к тому, что нам необходимо создать копию объекта атрибута и вернуть ее. В этом сложность, поскольку обычно атрибуты имеют собственные параметры конструктора. По идее нам нужно выделить память для объекта атрибута, и скопировать туда информацию из текущего объекта. В моем случае написан следующий код, но я совсем не уверен, что он корректен:
if a is at.AsInstance.MetaclassType then begin c := TCustomAttribute(a.NewInstance()); s := c.InstanceSize(); CopyMemory(c, a, s); exit(T(c)); end;Локальная переменная С также имеет тип TCustomAttribute как и A. Вообще по идее это не корректно, просто в данном случае атрибут имеет весьма простую структуру. А если бы не было члена StringValue то вобще бы не заметил ибо IntValue можно будет получить, даже если выйти через exit(T(a)) и не пробовать создать копию. А строка за неимением новых ссылок финализируется. Если атрибут будет содержать ссылки на другие объекты (иметь объекты внутри себя), то видимо скопируется только ссылка. Реальный же экземпляр разрушится (в деструкторе имеется в виду), и ссылка будет не корректна. Ну да ладно. Задачка немного не для моих мозгов (: В общем таким образом мы получаем тот атрибут который нам нужен. Вернее на самом деле, объект может содержать несколько атрибутов одного типа, тогда мы получим только первый объект из списка. Но моей целью было только прикинуть такую возможность работы, а не реализовать какую надстройку для реальной работы. В общем при работе с атрибутами таким образом, контроль за утечками памяти остается на усмотрение пользователя. Мы должны вызывать деструктор полученного атрибута после использования. Второй метод - получение знания свойства атрибута по его имени в качестве TValue. Данный методы использует вышеописаный getAttribute.
function TAttributeGetter.getAttributeValue<T>(PropName: string): TValue; var a : TCustomAttribute; ctx : TRttiContext; rt : TRttiType; p : TRttiProperty; begin a := getAttribute<T>(); if not assigned(a) then exit; ctx := TRttiContext.Create(); try rt := ctx.GetType(a.ClassType); for p in rt.GetProperties() do begin if p.Name = PropName then begin result := p.GetValue(a); end; end; finally a.Free(); ctx.Free(); end; end;Здесь все достаточно просто. Сначала получаем нужный атрибут. Затем перебираем его свойства. Определив нужное по имени получаем его значение с помощью TRttiProperty.getValue(), где в качестве параметра передается экземпляр объекта. Возвращаемая структура TValue может хранить различные типы значения, но в отличие от варианта не может производить конвертацию. Т.е в какое поле мы записали информацию, из того надо и получать. Здесь кстати в блоке Finally мы уничтожаем полученный объект-атриубут. Использование:
tv : TValue; begin tv := t.getAttributeValue<TTestAttribute>('IntValue'); ShowMessage(IntToStr(tv.AsInteger));Третий метод - получание свойства объекта, но уже не в виде TValue а используя обобщения. Метод еще проще остальных:
function TAttributeGetter.getAttributeValue<T; V>(PropName: string): V; var value : TValue; begin value := getAttributeValue<T>(PropName); result := value.AsType<V>; end;Здесь используется два обобщения, одно из них описывает сам атрибут, другое - возвращаемое значение.
var v : integer; begin v := t.getAttributeValue<TTestAttribute, integer>('IntValue'); end;Еще меня посетила мысль, о том, что атрибуты могут быть рекурсивными. Ведь мы наверное можем написать что-нить типа:
TestAttr1 = class; TestAttr2 = class; [TestAttr2()] TestAttr1 = class(TCustomAttribute); [TestAttr1()] TestAttr2 = class(TCustomAttribute);Хотя практического интереса это никакого не представляет. По крайней мере на первый взгляд. Также я думаю что вполне удобно будет иногда иметь обобщенные атрибуты, если мы храним разнородную инфомарцию. Что то вида:
TSimpleValueAttribue<T> = class(TCustomAttribute) private FValue : T; public property value : T read FValue write Fvalue; constructor Create(InitValue : T); end;на этом рассуждения про RTTI в данной заметке заканчиваются (:
30.07.2011 в 05:05
30.07.2011 в 14:29
про то что использовать его следует вообще в крайних случаях согласен.