Подорожуючи
по TObject. Або як воно працює h2>
Максим Ігнатьєв p>
Кожен клас у
Delphi є спадкоємцем TObject, і, відповідно,
володіє усіма його властивостями і методами. Це, поза сумнівом, корисний факт, але
які його методи і властивості, які його основні властивості і як їх можна
використовувати? Як ми побачимо трохи пізніше, дуже багато чого в реалізації TObject
направлено на опис об'єктної моделі Delphi. p>
Розглянемо його
опис детальніше. p>
p>
TObject class = p>
constructor
Create; p>
procedure
Free; p>
class
function InitInstance (Instance: Pointer): TObject; p>
procedure
CleanupInstance; p>
function
ClassType: TClass; p>
class
function ClassName: ShortString; p>
class
function ClassNameIs (const Name: string): Boolean; p>
class
function ClassParent: TClass; p>
class
function ClassInfo: Pointer; p>
class
function InstanceSize: Longint; p>
class
function InheritsFrom (AClass: TClass): Boolean; p>
class
function MethodAddress (const Name: ShortString): Pointer; p>
class
function MethodName (Address: Pointer): ShortString; p>
function
FieldAddress (const Name: ShortString): Pointer; p>
function
GetInterface (const IID: TGUID; out Obj): Boolean; p>
class
function GetInterfaceEntry (const IID: TGUID): PInterfaceEntry; p>
class
function GetInterfaceTable: PInterfaceTable; p>
function
SafeCallException (ExceptObject: TObject; p>
ExceptAddr: Pointer): HResult; virtual; p>
p>
procedure
AfterConstruction; virtual; p>
procedure
BeforeDestruction; virtual; p>
procedure
Dispatch (var Message); virtual; p>
p>
procedure
DefaultHandler (var Message); virtual; p>
class
function NewInstance: TObject; virtual; p>
procedure
FreeInstance; virtual; p>
destructor
Destroy; virtual; p>
end; p>
Відразу видно
методи класу, а їх функціональність, як відомо, не залежить від факту
існування примірника. Розглянемо детальніше кожний з методів. P>
Відразу хочу
обмовитися, методи - конструктори і деструктори насправді є
операторами, то є внутрішніми, що не залежать від їх реалізації в коді,
конструкціями. p>
Constructor
Create; p>
Всі об'єкти
створюються за допомогою виклику конструктора. Власне конструктор не зобов'язаний
називатися Create, просто це прийняте назва даного методу. Конструктор на
насправді є методом
класу, і в процесі його роботи викликаються такі методи: p>
NewInstance p>
InitInstance p>
Create p>
AfterConstruction p>
Насправді
виклик цих методів відбувається досить цікаво. У TObject конструктор не
виконує ніякої діяльності, однак, як кореневої клас ієрархії він створюється
на рівні RTM. Що ж відбувається? Після виклику конструктора RTM викликає метод
NewInstance, який виділяє область в пам'яті, узгоджуючи при цьому з
значенням vmtInstanceSize, яке формується при компіляції. У рамках виклику
NewInstance виконується виклик InitInstance, який заповнює поля методу
значеннями, позначеними в модифікаторах default, далі виконується код,
описаний в тілі процедури
Create (або тієї, що заявлена як конструктора), після чого управління
передається в точку, визначену в точці vmtAfterConstruction, яка за
замовчуванням вказує на метод AfterConstruction. Всі ці маніпуляції дозволяють
максимально спростити процес гнучкого створення екземпляра класу в рамках
об'єктної моделі Delphi. Таким чином, при створенні екземпляра класу
(об'єкта) ви можете «бути присутнім» на будь-якій його фазі. Сенс процедури
AfterConstruction полягає в тому, щоб виявити момент закінчення конструювання
класу. Зручність його використання полягає в тому, що він викликається тільки при
вдалому виконанні конструктора, що, самі розумієте досить вигідно. На
сьогоднішній момент тільки TCustomForm і TCustomDataModule перевантажують цей
метод спеціально для того, щоб виконати специфічні для них функції, так що
заважає нам зробити те ж саме? Але це вже питання конструювання класу. P>
Що ж
відбудеться при виникненні виняткової ситуації в рамках конструктора?
Тут важливо знати про те, що всі елементи класу вже створені і при
виникненні виняткової ситуації ми знаємо, що можна видалити. Так от при
виникненні виключення викликаються всі дії, пов'язані з руйнуванням --
виклик деструктора, все по повній програмі. p>
Важливо знати,
що при виклику конструктора
класу він викликається як конструктор і створює екземпляр, при
виклику ж конструктора в об'єкта
він викликається як процедура і нового екземпляра не створює, звідси отримують
свій початок помилки наступного роду: p>
p>
Var p>
O: TObject; p>
Begin p>
O. Create;// Неправильний виклик p>
O: = TObject.Create;//
Коректний виклик p>
End; p>
При виклику
методу конструктора в об'єкта важливо відзначити той факт, що якщо в конструкторі
створюються поля-об'єкти, то тут виникає потенційна небезпека витоку пам'яті,
тому що нові екземпляри створюються, а старі не знищуються. p>
Procedure
Free; p>
Ця процедура ініціює
процес руйнування об'єкта в пам'яті. Чому ж не деструктор? Виклик деструктора
є коректним звільненням ресурсів для застарілого способу визначення
об'єктів object. Якщо ж подивитися на те, яким чином працює ця
процедура, то можна побачити дуже цікаву картину. p>
p>
procedure TObject.Free; p>
asm p>
TEST EAX, EAX p>
JE @ @ exit p>
MOV ECX, [EAX] p>
MOV DL, 1 p>
CALL dword ptr
[ECX]. VmtDestroy p>
@ @ exit: p>
end; p>
Що ж ми
бачимо? У першому рядку відбувається звірка покажчика на Self (себе) з нулем - а
не звільнили чи нас вже? Якщо ще ні, то відповідно вказівником на
vmtDestroy ми викликаємо реальний деструктор. В іншому випадку відбувається вихід
з процедури. Таким чином відбувається тривіальна "перевірка на дурня» з
боку RTM Delphi. Коли Ви робите ж деструктора ми безпосередньо звільняємо
(або не звільняємо, а даремно) ресурси об'єкта. Знову ж таки при звільненні ресурсів
виконується повний набір дій. p>
BeforeDestruction p>
FreeInstance
p>
Метод
FreeInstance викликає каскад процедур, спрямованих на звільнення всіх
захоплених ресурсів, у тому числі і динамічних масивів, Variant типів і
багато чого іншого. Це має бути корисно при виникненні виняткових
ситуаціях в конструкторі при вже створених внутрішніх динамічних структурах.
Це також дуже корисно як механізм збору сміття всередині об'єкта. P>
class function InitInstance (Instance: Pointer):
TObject; p>
Функція
ініціалізації екземпляра інформацією з VMT, при цьому враховується використання
інтерфейсів при спадкуванні. Важливо звернути увагу на те, що це функція
класу, фактично ця функція заповнює болванку об'єкта, створену функцією
NewInstance. P>
Procedure
CleanupInstance; p>
Процедура
повернення примірника до «невинному» змісту. При цьому використовуються
інформація, що зберігається в vmtInitTable і в vmtParent. p>
Function ClassType: TClass; p>
Повертає
клас об'єкта. А якщо бути більш точним, то повертається безпосередньо
покажчик на VMT. p>
class function ClassName: ShortString; p>
Повертає
назва класу. Використовується VMT. P>
class function ClassNameIs (const Name: string):
Boolean; p>
Виконує
звірку назви з назвою потрібного класу. Використовується при виконанні
оператора is. p>
class function ClassParent: TClass; p>
Віддає
вказівник на батьківський клас. Використовується при виконанні оператора is. P>
class function ClassInfo: Pointer; p>
Повертає
покажчик на RTTI інформацію про клас. Якщо клас скомпільований без
використання директиви $ M +, то повертається nil. p>
class function InstanceSize: Longint; p>
Розмір
екземпляра. Як видно з опису інформація про розмір і про RTTI зберігається в VMT
поза прив'язкою до конкретного екземпляру. Судячи з усього, ця інформація
формується під час компіляції. p>
class function InheritsFrom (AClass: TClass): Boolean; p>
Повертає
точна вказівка на те, що даний клас успадкований від шуканого. Ця функція
сканує VMT і батьків цього VMT на відповідність зазначеного класу. p>
class function MethodAddress (const Name: ShortString):
Pointer; p>
Сканує VMT
на наявність методу і при вдалому результаті повертає вказівник але нього. При не
знаходженні методу в "рідний" VMT сканується VMT батька і так до
тих пір, поки не буде знайдений (або не знайдено) адреса методу. Таким чином
здійснюється реалізація метаморфізму в об'єктної моделі Delphi. p>
class function MethodName (Address: Pointer): ShortString; p>
Функція обернено
попередньої. p>
Function FieldAddress (const Name: ShortString):
Pointer; p>
Доступ до полів.
Повертає вказівник на поле. Як завжди використовує VMT. p>
Function GetInterface (const IID: TGUID; out Obj):
Boolean; p>
Використовується
при спадкуванні інтерфейсів і повертає інтерфейс зазначених вище IID. p>
class function GetInterfaceEntry (const IID: TGUID):
PinterfaceEntry; p>
Повертає
точку входу інтерфейсу на вказаний IID. p>
class function GetInterfaceTable: PInterfaceTable; p>
Таблиця
інтерфейсів. Незважаючи на те, що заявлено використання нескінченного числа
інтерфейсів, у вихідному тексті ясно вказано на 10000 елементів таблиці
інтерфейсів. Я, зрозуміло, не хочу поставити експеримент і спробувати
перевищити цей ліміт, але прогрес йде такими темпами, що, боюся, через
деякий час цей ліміт буде вичерпано. p>
Function SafeCallException (ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual; p>
Безпечна
обробка переривання, однак, використання цього методу безпосередньо в
TObject поверне Вам E_UNEXPECTED, то є щось несподіване. Викликається кожен
разу при виникненні виключення всередині коду об'єкта з вказівкою на об'єкт
винятки та адреса, що викликав виняток. p>
Procedure AfterConstruction; virtual; p>
Процедура,
викликається після створення екземпляра. Виклик процедури здійснюється за адресою,
прописаному в VMT. Прямий виклик ніде не прописаний, судячи з усього, ця
можливість прописана в RTM, де вказані всі виклики. p>
Procedure BeforeDestruction; virtual; p>
Процедура,
викликається до руйнування об'єкта. p>
Procedure Dispatch (var Message); virtual; p>
Внаслідок
використання Windows у якості базової платформи розробники вирішили не
проходити мимо основного способу обробки межоб'ектного взаємодії --
системи повідомлень. Цей спосіб якраз і реалізується цим методом. Вельми
розумно було помістити його саме в TObject, адже він є базовим для всіх
класів, визначених в рамках об'єктної моделі Delphi. Цей метод сканує
VMT на наявність обробника повідомлення, ID якого зазначений у перших 4 байтах
(довге слово, Cardinal) параметра Message і якщо не знаходить, то викликає
DefaultHandler. Тобто можна відловлювати події, що відбуваються не тільки у
елементів управління, а й у класів нижчої ієрархії. p>
Procedure DefaultHandler (var Message); virtual; p>
Оброблювач
подій за замовчуванням. Викликається методом Dispatch при не знаходженні
методу-обробника відповідного повідомлення. p>
class function NewInstance: TObject; virtual; p>
Створює
екземпляр класу. Розумно скористатися цією функцією для клонування об'єктів,
тому що, не знаючи початкового класу, можна створювати нові екземпляри вже готових
об'єктів без використання RTTI. p>
procedure FreeInstance; virtual; p>
Звільняє
ресурси примірника. Використання цього методу не вітається через його
тісному взаємозв'язку з VMT, тобто перевантаження цього методу повинна проводитися
з великою обережністю. Виклик ж методу безпосередньо в сукупності з InitInstance
може служити для того, щоб створити екземпляр «у собі», адже деякі завдання
вимагають відкату стану об'єкта на момент створення. p>
destructor Destroy; virtual; p>
Власне
деструктор. Викликається методом Free після посвідчення в тому, що примірник
поки існує. Є одне зауваження з приводу іменування деструктора - він
повинен називатися Destroy, це пов'язано з його віртуальністю, а відповідно і
перевантаженням. Якщо Ви назвете деструктор іншим ім'ям, то при спробі викликати
успадкований метод RTM не знайде опис методу з вашим ім'ям, а це
потягне за собою порушення функціональності процедури руйнування об'єкта.
Проте цікаво відзначити одну деталь. Наявність виклику успадкованого
деструктора не обов'язково, хоча і бажано - адже не всі розробники люблять
обробляти події часу виконання, а звільнення пам'яті, відведеної під
примірник, відбудеться без участі коду, описаного в деструктор. p>
Даючи опис
методам базового класу TObject, я намагався дати уявлення про об'єктної
моделі Delphi, про життєвому циклі об'єкта, про методи використання об'єктів в
власних програмах та правила перевантаження. Як видно з вищесказаного
основою для роботи з екземплярами є VMT, і використання RTTI не завжди
необхідно для виконання деяких специфічних операцій з екземпляром.
Використання ж RTTI, на мій погляд, не завжди виправдовується, однак при
написанні RunTime редакторів компонент це засіб досить зручно. p>
У результаті
вивчення вихідного коду виявився цікавий момент - при виклику будь-якого
методу в EAX знаходиться покажчик ... на VMT! Чи не це є явним вказівкою
на об'єктну орієнтованість Delphi?! Вивчаючи матеріали книги "Delphi in
nutshell "Рея Лішнера (Ray Lischner) я наткнувся на цікавий факт --
таблицю порівняння об'єктних моделей деяких мов, дозволю собі навести її
з деяким перекладом і доповненнями: p>
Підтримувані
можливості об'єктних моделей деяких мов програмування. p>
Можливість p>
Delphi p>
Java p>
Visual
Basic p>
Спадкування p>
+ p>
+ p>
+ p>
Множинне спадкування p>
+ p>
Інтерфейси p>
+ p>
Як чисто абстрактні класи p>
+ p>
+ p>
Один базовий клас p>
+ p>
+ p>
Метакласси p>
+ p>
+ p>
Статичні поля класів p>
Як поля модуля p>
+ p>
+ p>
Віртуальні методи p>
+ p>
+ p>
+ p>
Абстрактні методи p>
+ p>
+ p>
+ p>
Статичні методи класів p>
+ p>
+ p>
+ p>
Динамічні методи p>
+ p>
Збір сміття p>
Інтерфейсні методи і динамічні
деструктори p>
+ p>
Інтерфейсні методи p>
Типи Variant p>
+ p>
+ p>
OLE automation p>
+ p>
+ p>
Статичний контроль типів p>
+ p>
+ p>
+ p>
Обробка винятків p>
+ p>
+ p>
+ p>
+ p>
Перевантаження методів p>
+ p>
+ p>
+ p>
поліморфні виклики p>
+ p>
+ p>
Перевантаження операторів p>
+ p>
Методи - не члени класу p>
+ p>
+ p>
+ p>
Змінні - не члени об'єкта p>
+ p>
+ p>
+ p>
Властивості p>
+ p>
+ p>
RTTI p>
+ p>
Тільки для операторів is і
as p>
+ p>
Загальні типи (шаблони) p>
+ p>
Підтримка ниток p>
+ p>
+ p>
Обробка повідомлень p>
+ p>
Вбудований асемблер p>
+ p>
У деяких реалізаціях p>
+ p>
Inline функції p>
+ p>
Пакети p>
+ p>
+ p>
Друзі класу p>
Модульна видимість p>
+ p>
Пакетна видимість p>
Тут видно
деякі особливості, які не є очевидними на перший погляд. Що ж таке
динамічні методи? Відразу варто зробити застереження - це модифікатор способу виклику
методу, і з цього його відразу треба поставити в один ряд з іншими способами
виклику методів - статичними, віртуальними і репредставітельнимі. Чим же вони
відрізняються і коли вони потрібні? p>
Статичні
методи (їхній аналог в Java - final,
фінальні) є не перевантажує методами, їх функціональність
остаточна, наприклад конструктор Create класу TObject - він порожній і ніякої додаткової діяльності не
несе, з цього виклик цього методу не поліморфа. З цього, якщо ви хочете
перевантажити статичний метод, то Вам доведеться заново описувати всю його
функціональність. p>
Віртуальні
методи це методи, які дозволяють формувати ланцюжки поліморфних викликів
за допомогою статичного зв'язування через таблицю віртуальних методів VMT. Це
виглядає приблизно так: p>
Метод p>
Посилання p>
DoOne p>
Self.DoOne p>
DoTwo p>
Self.Parent.DoTwo p>
DoThree
p>
Self.Parent.Parent.DoThree p>
Щоб бути
цілком точним, треба сказати, що в таблиці вказані всі віртуальні методи,
визначені в батьках плюс всередині самого класу. Якщо є перевантажені
методи, то в таблиці на відповідних місцях ставляться вхідні точки нових
методів, якщо ж ні - то вхідні точки методів батьків. Таким чином, можна
точно сказати, який метод треба викликати при зверненні до таблиці віртуальних
методів. p>
Динамічні
методи зберігають точки входу в спеціальній таблиці динамічних методів, ця
таблиця будується тільки для змінених або доданих методів. Таким чином
зберігатися менше інформації про клас, але виклик методів відбувається довше,
внаслідок лінійного пошуку методу в таблиці динамічних методів класу і його
батьків, на що витрачається деякий час. p>
Репредствавітельние
методи це методи, функціональність яких перевизначено повністю
щодо батьків. Взагалі-то вказівка директиви reintroduce не обов'язково, але це позбавить
Вас від зайвого попередження про перевантаження методу з боку компілятора. P>
Ще один
цікава особливість TObject
- Це зберігання на рівні VMT інформації про три методи з цікавою назвою: QueryInterface, AddRef і Release.
Тобто будь-який клас, створений в рамках об'єктної моделі COM Delphi є об'єктом!
Єдиним обмеженням тут є те, що для функціонування цих
методів необхідно успадкувати хоча б один інтерфейс, що і зроблено в рамках
іншого базового класу TInterfacedObject.
p>
Увага!
Забороняється передрук цієї статті або її частини без узгодження з автором.
Якщо ви хочете розмістити цю статтю на своєму сайті або видати в друкованому вигляді,
зв'яжіться з автором. p>
Список
літератури h2>
Для підготовки
даної роботи були використані матеріали з сайту http://coderpro.fatal.ru/
p>