Pull to refresh

Контексты RTTI в Delphi 2010: как это работает и как их использовать

Reading time 4 min
Views 4.7K
Original author: Barry Kelly
Delphi 2010 включает в себя расширенную поддержку RTTI, также известную как информация о типах времени выполнения (run-time type info) или рефлексия. Множество подходов в проектированию раньше были доступны только в управляемых языках, таких как C# и Java, так как для них необходима аннотация кода (code annotation) и интроспекция (самоанализ). Теперь это возможно и в мире Delphi.


Что интересно в работе RTTI, так это его подход к организации пулов объектов. Delphi — это язык, не использующий «сборку мусора», так что пользователям нужно быть внимательными и уничтожать объекты, когда они больше не нужны, делая это явно, или создавая или используя какие-нибудь схемы владения (объектами), такие как используются TComponent-ом, где владелец (Owner) берет на себя ответственность за уничтожение объектов.

Порядок использования информации о типах не очень хорошо сочетается со схемой владения в стиле TComponent. Обычно, работая с RTTI, вам требуется выполнять поиск интересующих вас объектов, что-то с ними делать и продолжить работу дальше. Это означает, что множество объектов могут быть определены для проверки, но в действительности не использоваться. Управление временем существования этих объектов должно быть довольно утомительно, поэтому использован другой подход: единый глобальный RTTI пул объектов. Пока хотя бы один RTTI контекст активен в программе, пул объектов хранит все свои объекты в актуальном состоянии. Когда последний контекст выходит из области видимости — объекты освобождаются.

Управление пулом для работы использует запись Delphi, которая содержит ссылку на интерфейс. Когда любой переданный RTTI контекст используется в первый раз, он помещается в эту ссылку на интерфейс. Он помещается туда только в первый раз — однократно, потому что записи Delphi не поддерживают конструкторов по умолчанию, которые к тому же имеют свои собственные проблемы. Например, как вы обрабатываете исключения в конструкторе по умолчанию, во всех точках, где они могут возникнуть? Создание массивов, локальных для потока переменных, глобальных переменных, глобальных переменных в модулях, временных объектов в выражениях, и т. д. Это может стать отвратительным, а в C++ иногда и становится.

Таким образом, первое использование создает интерфейс, называющийся токен пула (pool token). Он действует как некий дескриптор со счетчиком ссылок, указывающий на глобальный пул объектов. До тех пор пока этот интерфейс актуален (существует), глобальный пул объектов будет оставаться актуальным. Даже если RTTI контекст куда-то скопирован, встроенная в Delphi логика управления интерфейсами, созданная на базе принципов COM, позволяет быть уверенными, что интерфейс не будет преждевременно удален, счетчик ссылок будет иметь верное значение. И когда RTTI контекст выходит из области видимости, или будучи локальной переменной в функции, которая завершилась, или полем в удаленном объекте, счетчик ссылок уменьшит свое значение. Когда счетчик ссылок достигает нуля — пул опустошается.

Наибольшее преимущество такого подхода состоит в том, что использование RTTI, в сущности, должно быть легким и интуитивно понятным. Нужно только в коде программы объявить переменную соответствующего типа и начать ее использовать:

procedure Foo;
var
ctx: TRttiContext;
t: TRttiType;
begin
t := ctx.GetType(TypeInfo(Integer));
Writeln(t.Name);
end;


Однако, обратная сторона, состоит в том, что «ленивая» инициализация может вызвать ошибку. Представим такой сценарий:

1. Библиотека A объявляет RTTI контекст A.C
2. Пользовательский код B объявляет RTTI контекст B.C
3. Код B запрашивает некоторые RTTI объекты O из B.C, для того чтобы передать их библиотеке A
4. B.C выходит из области видимости
5. Библиотека A сейчас пытается работать с O, но обнаруживает к своему удивлению, что объекты были преждевременно удалены, даже если A уже имеет RTTI контекст A.C

Проблема в том, что A никогда не использовала A.C, потому токен пула не был создан. Когда B.C использовал свой контекст, пул начал свое существование, и объекты O были назначены ему; но, после того как B.C вышел из области видимости, объекты были освобождены.

Решение этой проблемы — дать знать библиотеке A, что она использует долгоживущий RTTI контекст и что она рассчитывает взаимодействовать со сторонним кодом, который создает объекты из ее собственного RTTI контекста и передает их обратно, она должна быть уверена, что для этого долгоживущего контекста создан токен пула. Простой способ сделать это выглядит так:

type
TFooManager = class
FCtx: TRttiContext;
// ...
constructor Create;
// ...
end;

constructor TFooManager.Create;
begin
FCtx.GetType(TypeInfo(Integer));
// ...
end;


Это создаст только необходимый минимум RTTI объектов, которые нужны для представления типа System.Integer, но что более важно, даст уверенность в том, что FCtx имеет токен пула и оставит глобальный RTTI пул в актуальном состоянии.

В будущих версиях Delphi, статический метод TRttiContext.Create будет следить за тем, чтобы возвращаемое им значение получило токен пула; но пока что это не так. TRttiContext.Create был первоначально определен для того, чтобы сделать запись TRttiContext более похожей на класс для людей незнакомых с идиомой использования интерфейсов для автоматического детерминированного управления временем существования объектов. Соответствующий метод TRttiContext.Free удаляет токен пула, и должен остаться прежним.

Translated.by: переведено толпой

Перевод: © DreamerSole, r3code, debose.
Tags:
Hubs:
+2
Comments 1
Comments Comments 1

Articles