Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4466.html
 

Изучая RTTI #1

Опубликовано 17.04.2011 г. 22:12

Не так давно мне казалось, что механизм RTTI в Delphi это что то не понятное, и главное - не особо нужное. Т.е. казалось, что в основном это полезно, например, для разработчиков компонентов. По этой причине никогда не читал статей и справки по этой тематике. Но взгляды меняются...

Недавно случайно наткнулся на блог, в котором была опубликована серия статей по rtti. После этой серии прочитал также главу Delphi 2010 Handbook от Марко Канту про rtti. Уже во время чтения начал понимать, что зря не изучил это раньше (: Что нам дает rtti в кратце? Во время выполнения программы мы можем получать информацию о типе данных, получать список его свойств методов, обращаться к ним или вызывать. Т.е. можно работать с объектом, описания класса которого мы не знаем. Также очень значимой частью является использование атрибутов. Вот в этой статье как раз речь и пойдет про атрибуты. Начну как всегда с предыстории. Ситуация простая: необходимо реализовать набор классов, реализующих различные методы аппроксимации. Все они у нас будут унаследованы от одного предка - TCustomApproximation. Для простоты определим его таким образом:
    TCustomApproximation = class(TObject)
      strict private
        FTitle : string;
      public
        procedure Calculate(); virtual; abstract;
        property Title : string read FTitle write FTitle;
    end;
Здесь метод расчета Calculate будет перекрываться во всех классах потомках, реализующий непосредственный расчет. Собственно сами методы нас не интересуют, но оставим его, чтобы класс имел смысл. Наш (по крайней мере мой (: ) интерес относится к свойству title. Работая с набором подобных методов, мы иногда хотим знать названия самих методов. Например, чтобы заполнить выпадающий список для выбора метода. С этой целью логично было бы завести свойство title, все потомки конечно же его наследуют. Мы можем заполнять свойство при создании конечных объектов. И затем выводить его в меню. Каков минус такого подхода? Нам потребуется заполнять свойство вручную. Либо после создания объекта, либо в его конструкторе. Конструктор конечно выглядит предпочтительным и логичным. И таким образом, конечный класс аппроксимации выглядит упрощенно так:
    TExpSmoothing = class(TCustomApproximation)
      public
        constructor Create();
        procedure Calculate(); override;
    end;
...........
constructor TExpSmoothing.Create();
begin
    inherited;
    title := 'Экспоненциальное сглаживание';
end;
Здесь есть два отрицательных момента: все установки названий для конечных классов будут разбросаны по коду, что приводит к неудобности редактирования. Второй момент в том, что таким образом наше название привязано к объекту, а не к классу. Но ведь название по своей сути описывает именно класс, а не относится непосредственно к объекту. Вспомним про классовые методы и свойства. Это более близко. Мы можем получить именно название для класса: TExpSmoothing.title вернет нам "Экспоненциальное сглаживание". Да и если будем иметь два объекта a, b : TExpSmoothing, то оба они будут предоставлять имя класса, а не каждый "свою" копию названия. Но и тут минус. Даже если это классовая переменная/свойство то изначально заполнить его все таки где то надо. Для этих целей обычно используется class constructor. Но только ради идеи громоздить столько кода не кажется адекватным. В идеале было бы здорово видеть название класса в его описании, т.е прямо в секции interface. Ну например, как нибудь так:
    TLinearRegression = class(TCustomApproximation)
      strict private
       class const MethodTitle = 'Линейная регрессия';
       ...
    end;
Но вот ведь загвоздка, классовых констант нет. А это было бы хорошим решением проблемы. В своей работе на тот момент я решил эту проблему следующим образом: У меня был список "описаний" методов. Каждому методу был назначен числовой идентификатор. В общем говоря изначально вызывалась некоторая процедура регистрации доступных классов, и выглядело это примерно так:
 RegisteredClasses.Add( TApproxDefinition.Create(amLinear, TLinearApproximation, 'Линейная аппркосимация') );
RegisteredClasses.Add( TApproxDefinition.Create(amExpSmooth, TExpSmoothing, 'Экспоненциальное сглаживание') );
С точки зрения названий, которые мы сейчас обсуждаем, смысл в том, что все названия устанавливаются в одном месте. Конечно, это не было единственной причиной заведения подобного списка, но это уже другая история.

А теперь rtti

После изучения основ rtti, я понял что решить сию задачу можно было гораздо интересней. Для этих целей нам потребуется завести атрибут "название". Атрибут в rtti это класс унаследованный от TCustomAttribute:
    TApproximationTitleAttribute = class(TCustomAttribute)
      strict private
        FTitle : string;
      public
        constructor Create(aTitle:string);
        property Title : string read FTitle;
    end;
...
constructor TApproximationTitleAttribute.Create(aTitle : string);
begin
    inherited Create();
    FTitle := aTitle;
end;
Т.е мы определили класс атрибута, который содержит название. Название передается в конструктор. Теперь применим данный атрибут к классу. Атрибуты можно применять к классам, методам, свойствам класса, переменным. Объявляется атрибут в квадратных скобах, перед его "целью". Итак применим атрибуты к нашим классам:
    [TApproximationTitleAttribute('Линейная регрессия')]
    TLinearRegression = class(TCustomApproximation)
    end;

    [TApproximationTitle('Экспоненциальное сглаживание')]
    TExpSmoothing = class(TCustomApproximation)
    end;
Перед именем класса указан его атрибут. В скобках за именем атрибута указано значение параметра которое передается в конструктор. Можете обратить внимание, что во втором случае указано не полностью имя атрибута TApproximationTitleAttribute, а только TApproximationTitle. Такое сокращение является допустимым. Чтож наша задача выполнена - мы привязали название именно к классу, и сделали это именно там, где сам класс описывается. Конечно, это хорошо, но теперь необходимо эти названия все таки получить. Этот функционал мы можем оставить в родительском класса TCustomApproximation. Либо запомнить имя, либо получать его каждый раз при обращении к свойству title. Давайте сделаем это при обращении к свойству. Алгоритм действий таков:
  1. в методе getTitle для получения свойства title создаем контекст rtti. Контекст описывает структура TRttiContext. Называется она контекстом, ибо хранит все объекты созданные при работе с rtti. У данной структуры есть методы Create & Free. Поскольку это структура, то по большому счету они не нужны. Но сделаны они для технических целей - они очищают пул созданных объектов (пул в виде ссылки на интерфейс, и она зануляется уничтожая объект интерфейса, и все чем он владел).
  2. С помощью контекста для нашего объекта мы получаем информацию о rtti типе.
  3. Для заданного типа получаем список атрибутов.
  4. Среди списка пытаемся найти наш атрибут с названием.
  5. Для найденного атрибута берем значение его поля title.
Код, реализующий сей алгоритм:
function TCustomApproximation.getTitle: string;
var ctx : TRttiContext;
    rttiType : TRttiType;
    attr : TCustomAttribute;
begin
    result := 'Неизвестный метод аппроксимации';
    ctx.Create();
    try
        rttiType := ctx.GetType(self.ClassType);
        for attr in rttiType.GetAttributes() do begin
            if attr is TApproximationTitleAttribute then begin
                result := (attr as TApproximationTitleAttribute).Title;
            end;
        end;
    finally
        ctx.Free();
    end;
end;
Чтож, теперь остается для теста заполнить выпадающий список объектами и их названиями:
procedure TMainForm.FormCreate(Sender: TObject);

    procedure AddApproxItem(ApproxClass : TApproximationClass);
    var approxObj : TCustomApproximation;
    begin
        approxObj := ApproxClass.Create();
        ApproxCombo.Items.AddObject(ApproxObj.ClassName + ': ' +  approxObj.Title, ApproxObj);
    end;

begin
    AddApproxItem(TLinearRegression);
    AddApproxItem(TExpSmoothing);
end;
Для удобства была использована вложенная процедура. Кстати замечу, недавно обнаружил, что для generic-классов/методов нельзя использовать вложенные процедуры. Логичной заменой им является использование анонимных методов, но и здесь грабли. В итоге, таким стало для меня знакомство с использованием атрибутов RTTI в Delphi. Вероятно появится еще несколько статей по этому поводу, блог хоть оживится, а то статьи стали появляться редко.
Метки:  attributes  |  rtti 

Комментарии

ND
18.04.2011 в 11:04
Моё почтение!
ИМХО с
class const MethodTitle = '...';

Вы погорячились. Убрать слово "class" - и получиться именно то, что Вы хотели :)
ter
18.04.2011 в 12:17
:) действительно погорячился (: константа на то и есть константа, что доступна всегда и везде, ей не надо быть никакой классовой.

тут тогда получается такая вещь, что нам требуется иметь class property Title в Custom классе.
поскольку напрямую константу оно не читает, то надо иметь метод class function getTitle()
которая будет возвращать константу.
и в итоге получается, что если я в потомке создаю константу с тем же именем, но нужным значением, то в родительском классе она никак не учитывается. Т.е не перекрывается.

Т.е нам понадобится механизм получения значения константы перенести в конечные классы, а этого мы сделать не можем.
Алексей Тимохин
18.04.2011 в 18:18
Александр Божко, вроде выкладывал у себя в блоге переводы постов Роба об RTTI.
И на хабре в блоге Delphi публиковалась пара переводов статей об атрибутах в Delphi.
ter
18.04.2011 в 19:49
Ага, действительно, переводы здесь: delphi2010.ru/?tag=rtti если кому понадобится русский вариант. Оригиналы статей достаточно просты в изложении, и хорошо поясняют тему. Так что переводы будут еще понятней.

Статьи на хабре:
1) habrahabr.ru/blogs/delphi/85509/
2) habrahabr.ru/blogs/delphi/105776/
Денис
20.04.2011 в 03:57
Здравствуйте.

Спасибо за статью.

Как у вас описан класс TApproximationClass ?
ter
20.04.2011 в 10:17
Не за что (:
Это не класс, а метакласс. Описывается весьма просто
TApproximationClass = class of TCustomApproximation
Дмитрий
21.04.2011 в 13:40
Да, атрибуты мощная вещь.
Пару месяцев назад с Delphi перешел на C#, там атрибуты давно и уже есть много библиотек их использующих.
Например PostSharp.
Понадобилось ввести логирование вызовов методов - объявил наследника класса-аспекта, переопределил пару методов под запись в конкретную библиотеку логирования, пометил нужные методы атрибутом - и вуаля, все вызовы этих методов, аргументы, исключения и результаты логируются автоматически.
Надеюсь разработчики дельфи будут и дальше развивать язык.
Владислав Иванов
02.02.2012 в 16:01
Спасибо большое за статьи! Только начинаю разбираться в описанном вопросе, надеюсь Ваши статьи помогут усвоить тему...
ter
02.02.2012 в 17:37
не за что,
не забудьте почитать переводы статьей Роберта Лава - http://delphi2010.ru/?tag=rtti
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно