Подорожуючи по TObject. Або як воно працює.
Подорожуючи по TObject. Або як воно працює.
Кожен клас в Delphi є спадкоємцем TObject. і, відповідно, має всі його властивостями і методами. Це, безсумнівно, корисний факт, але які його методи і властивості, які його основні властивості і як їх можна використовувати? Як ми побачимо трохи пізніше, дуже багато в реалізації TObject направлено на опис об'єктної моделі Delphi.
Розглянемо його опис детальніше.
Відразу видно методи класу, а їх функціональність, як відомо, не залежить від факту існування примірника. Розглянемо детальніше кожен з методів.
Відразу хочу обмовитися, методи - конструктори і деструктори насправді є операторами, тобто внутрішніми, що не залежать від їх реалізації в коді, конструкціями.
Всі об'єкти створюються за допомогою виклику конструктора. Власне конструктор не зобов'язаний називатися Create, просто це прийнята назва даного методу. Конструктор насправді є методом класу. і в процесі його роботи викликаються наступні методи: Насправді виклик цих методів відбувається досить цікаво. У TObject конструктор не виконує ніякої діяльності, проте, як кореневої клас ієрархії він створюється на рівні RTM. Що ж відбувається? Після виклику конструктора RTM викликає метод NewInstance, який виділяє область у пам'яті, узгоджуючи при цьому багатозначно vmtInstanceSize, яке формується при компіляції. В рамках виклику NewInstance виконується виклик InitInstance, який заповнює поля методу значеннями, позначеними в модифікаторах default, далі виконується код, описаний в тілі процедури Create (або тієї, що заявлена як конструктора), після чого управління передається в точку, визначену в точці vmtAfterConstruction , яка за замовчуванням вказує на метод AfterConstruction. Всі ці маніпуляції дозволяють максимально спростити процес гнучкого створення екземпляра класу в рамках об'єктної моделі Delphi. Таким чином, при створенні екземпляра класу (об'єкта) ви можете «бути присутнім» на будь-який його фазі. Сенс процедури AfterConstruction полягає в тому, щоб виявити момент закінчення конструювання класу. Зручність його використання полягає в тому, що він викликається тільки при вдалому виконанні конструктора, що, самі розумієте досить вигідно. На сьогоднішній момент тільки TCustomForm і TCustomDataModule перевантажують цей метод спеціально для того, щоб виконати специфічні для них функції, так що заважає нам зробити те ж саме? Але це вже питання конструювання класу.
Що ж станеться при виникненні виняткової ситуації в рамках конструктора? Тут важливо знати про те, що всі елементи класу вже створені і при виникненні виняткової ситуації ми знаємо, що можна видалити. Так ось при виникненні виключення викликаються всі дії, пов'язані з руйнуванням - виклик деструктора, все по повній програмі.
Важливо знати, що при виклику конструктора класу він викликається як конструктор і створює екземпляр, при виклику ж конструктора в об'єкта він викликається як процедура і нового екземпляра не створює, звідси отримують свій початок помилки наступного роду:
При виклику методу конструктора в об'єкта важливо відзначити той факт, що якщо в конструкторі створюються поля-об'єкти, то тут виникає потенційна небезпека витоку пам'яті, так як нові екземпляри створюються, а старі не знищуються.
Ця процедура ініціює процес руйнування об'єкта в пам'яті. Чому ж не деструктор? Виклик деструктора є коректним звільненням ресурсів для застарілого способу визначення об'єктів object. Якщо ж подивитися на те, яким чином працює ця процедура, то можна побачити цікаву картину.
Що ж ми бачимо? У першому рядку відбувається звірка покажчика на Self (себе) з нулем - а не звільнили чи нас вже? Якщо ще немає, то відповідно вказівником на vmtDestroy ми викликаємо реальний деструктор. В іншому випадку відбувається вихід з процедури. Таким чином відбувається тривіальна «перевірка на дурня» з боку RTM Delphi. Коли Ви телефонуєте ж деструктора ми безпосередньо звільняємо (або не звільняємо, а даремно) ресурси об'єкта. Знову ж при звільненні ресурсів виконується повний набір дій. Метод FreeInstance викликає каскад процедур, спрямованих на звільнення всіх захоплених ресурсів, в тому числі і динамічних масивів, Variant типів і багато чого іншого. Це повинно бути корисно при виникненні виняткових ситуаціях в конструкторі при вже створених внутрішніх динамічних структурах. Це також дуже корисно як механізм збору сміття всередині об'єкта.
class function InitInstance (Instance: Pointer): TObject;
Функція ініціалізації примірника інформацією з VMT, при цьому враховується використання інтерфейсів при спадкуванні. Важливо звернути увагу на те, що це функція класу, фактично ця функція заповнює болванку об'єкта, створену функцією NewInstance.
Процедура повернення примірника до «незайманому» змістом. При цьому використовуються інформація, що зберігається в vmtInitTable і в vmtParent.
Function ClassType: TClass;
Повертає клас об'єкта. А якщо бути більш точним, то повертається безпосередньо покажчик на VMT.
class function ClassName: ShortString;
Повертає назву класу. Використовується VMT.
class function ClassNameIs (const Name: string): Boolean;
Виконує звірку назви з назвою необхідного класу. Використовується при виконанні оператора is.
class function ClassParent: TClass;
Віддає покажчик на батьківський клас. Використовується при виконанні оператора is.
class function ClassInfo: Pointer;
Повертає покажчик на RTTI інформацію про клас. Якщо клас скомпільовано без використання директиви $ M +, то повертається nil.
class function InstanceSize: Longint;
Розмір примірника. Як видно з опису інформація про розмір і про RTTI зберігається в VMT поза прив'язкою до конкретного екземпляру. Судячи з усього, ця інформація формується під час компіляції.
class function InheritsFrom (AClass: TClass): Boolean;
Повертає точна вказівка на те, що даний клас успадкований від шуканого. Ця функція сканує VMT і батьків цього VMT на відповідність зазначеного класу.
class function MethodAddress (const Name: ShortString): Pointer;
class function MethodName (Address: Pointer): ShortString;
Функція обернена попередньої.
Function FieldAddress (const Name: ShortString): Pointer;
Доступ до полів. Повертає покажчик на поле. Як завжди використовує VMT.
Function GetInterface (const IID: TGUID; out Obj): Boolean;
Використовується при спадкуванні інтерфейсів і повертає інтерфейс зазначених вище IID.
class function GetInterfaceEntry (const IID: TGUID): PinterfaceEntry;
Повертає точку входу інтерфейсу на вказаний IID.
class function GetInterfaceTable: PInterfaceTable;
Таблиця інтерфейсів. Незважаючи на те, що заявлено використання нескінченного числа інтерфейсів, у вихідному тексті ясно вказано на 10000 елементів таблиці інтерфейсів. Я, зрозуміло, не хочу поставити експеримент і спробувати перевищити цей ліміт, але прогрес йде такими темпами, що, боюся, через деякий час цей ліміт буде вичерпано.
Function SafeCallException (ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual;
Procedure AfterConstruction; virtual;
Procedure BeforeDestruction; virtual;
Процедура, що викликається до руйнування об'єкта.
Procedure Dispatch (var Message); virtual;
Внаслідок використання Windows в якості базової платформи розробники вирішили не проходити повз основного способу обробки межоб'ектного взаємодії - системи повідомлень. Цей спосіб якраз і реалізується цим методом. Вельми розумно було помістити його саме в TObject, адже він є базовим для всіх класів, визначених в рамках об'єктної моделі Delphi. Цей метод сканує VMT на наявність обробника повідомлення, ID якого зазначений в перших 4 байтах (довге слово, Cardinal) параметра Message і якщо не знаходить, то викликає DefaultHandler. Тобто можна відловлювати події, що відбуваються не тільки у елементів управління, але і у класів нижчої ієрархії.
Procedure DefaultHandler (var Message); virtual;
Оброблювач подій за замовчуванням. Викликається методом Dispatch прі не знаходженні методу-обробника відповідного повідомлення.
class function NewInstance: TObject; virtual;
Створює екземпляр класу. Розумно використовувати цю функцію для клонування об'єктів, так як, не знаючи вихідного класу, можна створювати нові екземпляри вже готових об'єктів без використання RTTI.
procedure FreeInstance; virtual;
Звільняє ресурси примірника. Використання цього методу не вітається через його тісному взаємозв'язку з VMT, тобто перевантаження цього методу повинна проводитися з великою обережністю. Виклик ж методу безпосередньо в сукупності з InitInstance може служити для того, щоб створити екземпляр «в собі», адже деякі завдання вимагають відкату стану об'єкта на момент створення.
destructor Destroy; virtual;
Власне деструктор. Викликається методом Free після посвідчення в тому, що екземпляр поки існує. Є одне зауваження з приводу іменування деструктора - він повинен називатися Destroy, це пов'язано з його віртуальністю, а відповідно і перевантаженням. Якщо Ви назвете деструктор іншим ім'ям, то при спробі викликати успадкований метод RTM не знайде опис методу з вашим ім'ям, а це спричинить за собою порушення функціональності процедури руйнування об'єкта. Однак цікаво відзначити одну деталь. Наявність виклику успадкованого деструктора не обов'язково, хоча і бажано - адже не всі розробники люблять обробляти події часу виконання, а звільнення пам'яті, відведеної під примірник, відбудеться без участі коду, описаного в деструкції.
В результаті вивчення вихідного коду виявився цікавий момент - при виклику будь-якого методу в EAX знаходиться покажчик ... на VMT! Чи не це є явною вказівкою на об'єктну орієнтованість Delphi. Вивчаючи матеріали книги "Delphi in nutshell" Рея Лішнера (Ray Lischner) я натрапив на цікавий факт - таблицю порівняння об'єктних моделей деяких мов, дозволю собі навести її з деяким перекладом і доповненнями:
Доступні функції об'єктних моделей деяких мов програмування.