Наша совместная команда Banwar.org

Связаться с нами

  • (097) ?601-88-87
    (067) ?493-44-27
    (096) ?830-00-01

Статьи

Програмування на мові Delphi. Глава 3. Об'єктно-орієнтоване програмування (ООП). Частина 2

  1. спадкування
  2. Прабатько всіх класів
  3. Перекриття атрибутів в спадкоємців
  4. Сумісність об'єктів різних класів
  5. Контроль і перетворення типів
  6. Віртуальні методи
  7. Механізм виклику віртуальних методів
  8. Абстрактні віртуальні методи
  9. динамічні методи
  10. Методи обробки повідомлень
  11. додаткова інформація

попередня стаття серії

© А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четирько
Стаття була опублікована на сайті rsdn.ru

спадкування

поняття спадкування

Класи інкапсулюють (тобто включають в себе) поля, методи і властивості; це їх перша риса. Наступна не менш важлива риса класів - здатність успадковувати поля, методи і властивості інших класів. Щоб пояснити сутність спадкування звернемося до прикладу з читачем текстових файлів у форматі "delimited text".

Клас TDelimitedReader описує об'єкти для читання з текстового файлу елементів, розділених деяким символом. Він не придатний для читання елементів, що зберігаються в іншому форматі, наприклад у форматі з фіксованою кількістю символів для кожного елемента. Для цього необхідний інший клас:

type TFixedReader = class private // Поля FFile: TextFile; FItems: array of string; FActive: Boolean; FItemWidths: array of Integer; // Методи читання і запису властивостей procedure SetActive (const AActive: Boolean); function GetItemCount: Integer; function GetEndOfFile: Boolean; function GetItem (Index: Integer): string; // Методи procedure PutItem (Index: Integer; const Item: string); function ParseLine (const Line: string): Integer; function NextLine: Boolean; // Конструктори і деструктори constructor Create (const FileName: string; const AItemWidths: array of Integer); destructor Destroy; override; // Властивості property Active: Boolean read FActive write SetActive; property Items [Index: Integer]: string read GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; end; {TFixedReader} constructor TFixedReader.Create (const FileName: string; const AItemWidths: array of Integer); var I: Integer; begin AssignFile (FFile, FileName); FActive: = False; // Копіювання AItemWidths в FItemWidths SetLength (FItemWidths, Length (AItemWidths)); for I: = 0 to High (AItemWidths) do FItemWidths [I]: = AItemWidths [I]; end; destructor TFixedReader.Destroy; begin Active: = False; end; function TFixedReader.GetEndOfFile: Boolean; begin Result: = Eof (FFile); end; function TFixedReader.GetItem (Index: Integer): string; begin Result: = FItems [Index]; end; function TFixedReader.GetItemCount: Integer; begin Result: = Length (FItems); end; function TFixedReader.NextLine: Boolean; var S: string; N: Integer; begin Result: = not EndOfFile; if Result then // Якщо не досягнуто кінець файлу begin Readln (FFile, S); // Читання черговий рядки з файлу N: = ParseLine (S); // Розбір ліченої рядки if N <> ItemCount then SetLength (FItems, N); // Відсікання масиву (якщо необхідно) end; end; function TFixedReader.ParseLine (const Line: string): Integer; var I, P: Integer; begin P: = 1; for I: = 0 to High (FItemWidths) do begin PutItem (I, Copy (Line, P, FItemWidths [I])); // Установка елемента P: = P + FItemWidths [I]; // Перехід до наступного елементу end; Result: = Length (FItemWidths); // Кількість елементів постійно end; procedure TFixedReader.PutItem (Index: Integer; const Item: string); begin if Index> High (FItems) then // Якщо індекс виходить за межі масиву, SetLength (FItems, Index + 1); // то збільшення розміру масиву FItems [Index]: = Item; // Установка відповідного елемента end; procedure TFixedReader.SetActive (const AActive: Boolean); begin if Active <> AActive then // Якщо стан змінюється begin if AActive then Reset (FFile) // Відкриття файлу else CloseFile (FFile); // Закриття файлу FActive: = AActive; // Збереження стану в поле end; end;

Поля, властивості та методи класу TFixedReader практично повністю аналогічні тим, що визначені в класі TDelimitedReader. Відмінність полягає у відсутності властивості Delimiter, наявності поля FItemWidths (для зберігання розмірів елементів), іншої реалізації методу ParseLine і трохи відрізняється конструкторі. Якщо в майбутньому з'явиться клас для читання елементів з файлу ще одного формату (наприклад, зашифрованого тексту), то доведеться знову визначати загальні для всіх класів поля, методи і властивості. Щоб позбутися від дублювання загальних атрибутів (полів, властивостей і методів) при визначенні нових класів, скористаємося механізмом успадкування. Перш за все, виділимо в окремий клас TTextReader загальні атрибути всіх класів, призначених для читання елементів з текстових файлів. Реалізація методів TTextReader, крім методу ParseLine, повністю ідентична реалізації TDelimitedReader, наведеної в попередньому розділі.

type TTextReader = class private // Поля FFile: TextFile; FItems: array of string; FActive: Boolean; // Методи отримання і установки значень властивостей procedure SetActive (const AActive: Boolean); function GetItemCount: Integer; function GetItem (Index: Integer): string; function GetEndOfFile: Boolean; // Методи procedure PutItem (Index: Integer; const Item: string); function ParseLine (const Line: string): Integer; function NextLine: Boolean; // Конструктори і деструктори constructor Create (const FileName: string); destructor Destroy; override; // Властивості property Active: Boolean read FActive write SetActive; property Items [Index: Integer]: string read GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; end; ... constructor TTextReader.Create (const FileName: string); begin AssignFile (FFile, FileName); FActive: = False; end; function TTextReader.ParseLine (const Line: string): Integer; begin // Функція просто повертає 0, оскільки не відомо, // в якому саме форматі зберігаються елементи Result: = 0; end; ...

При реалізації класу TTextReader нічого не відомо про те, як зберігаються елементи в зчитувальних рядках, тому метод ParseLine нічого не робить. Очевидно, що створювати об'єкти класу TTextReader не має сенсу. Для чого тоді потрібен клас TTextReader? Відповідь: щоб на його основі визначити (породити) два інших класу - TDelimitedReader і TFixedReader, призначених для читання даних в конкретних форматах:

type TDelimitedReader = class (TTextReader) FDelimiter: Char; function ParseLine (const Line: string): Integer; override; constructor Create (const FileName: string; const ADelimiter: Char = ';'); property Delimiter: Char read FDelimiter; end; TFixedReader = class (TTextReader) FItemWidths: array of Integer; function ParseLine (const Line: string): Integer; override; constructor Create (const FileName: string; const AItemWidths: array of Integer); end; ...

Класи TDelimitedReader і TFixedReader визначені як спадкоємці TTextReader (про це говорить ім'я в дужках після слова class). Вони автоматично включають в себе всі описи, зроблені в класі TTextReader і додають до них деякі нові. В результаті формується дерево класів, показане на малюнку 1 (воно завжди малюється перевернутим).

В результаті формується дерево класів, показане на малюнку 1 (воно завжди малюється перевернутим)

Малюнок 1. Дерево класів

Клас, який успадковує атрибути іншого класу, називається породжених класом або нащадком. Відповідно клас, від якого відбувається спадкування, виступає в ролі базового, або предка. У нашому прикладі клас TDelimitedReader є прямим нащадком класу TTextReader. Якщо від TDelimitedReader породити новий клас, то він теж буде нащадком класу TTextReader, але вже не прямим.

Дуже важливо, що в стосунках спадкування будь-який клас може мати тільки одного безпосереднього предка і як завгодно багато нащадків. Тому все пов'язані ставленням спадкування класи утворюють ієрархію. Прикладом ієрархії класів є бібліотека VCL; з її допомогою в середовищі Delphi забезпечується розробка GUI-додатків.

Прабатько всіх класів

У мові Delphi існує зумовлений клас TObject, який служить неявним предком тих класів, для яких предок не вказано. Це означає, що оголошення

type TTextReader = class ... end;

еквівалентно наступному:

type TTextReader = class (TObject) ... end;

Клас TObject виступає коренем будь-якої ієрархії класів. Він містить ряд методів, які у спадок передаються всім іншим класам. Серед них конструктор Create, деструктор Destroy, метод Free і деякі інші методи.

Таким чином, повне дерево класів для читання елементів з текстового файлу в різних форматах виглядає так, як показано на малюнку 2.

Таким чином, повне дерево класів для читання елементів з текстового файлу в різних форматах виглядає так, як показано на малюнку 2

Малюнок 2. Повне дерево класів

Оскільки клас TObject є предком для всіх інших класів (в тому числі і для ваших власних), то не зайвим буде коротко ознайомитися з його методами:

type TObject = class constructor Create; procedure Free; class function InitInstance (Instance: Pointer): TObject; procedure CleanupInstance; function ClassType: TClass; class function ClassName: ShortString; class function ClassNameIs (const Name: string): Boolean; class function ClassParent: TClass; class function ClassInfo: Pointer; class function InstanceSize: Longint; class function InheritsFrom (AClass: TClass): Boolean; class function MethodAddress (const Name: ShortString): Pointer; class function MethodName (Address: Pointer): ShortString; function FieldAddress (const Name: ShortString): Pointer; function GetInterface (const IID: TGUID; out Obj): Boolean; class function GetInterfaceEntry (const IID: TGUID): PInterfaceEntry; class function GetInterfaceTable: PInterfaceTable; function SafeCallException (ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual; procedure AfterConstruction; virtual; procedure BeforeDestruction; virtual; procedure Dispatch (var Message); virtual; procedure DefaultHandler (var Message); virtual; class function NewInstance: TObject; virtual; procedure FreeInstance; virtual; destructor Destroy; virtual; end;

Деякі конструкції цього опису будуть вам незрозумілі, оскільки ми їх ще не вивчали. Зараз це не важливо. Знову поверніться до цього опису після прочитання всієї глави.

Короткий опис методів в класі TObject:

  • Create - стандартний конструктор.
  • Free - знищує об'єкт: викликає стандартний деструктор Destroy, якщо значення псевдопеременной Self не дорівнює nil.
  • InitInstance (Instance: Pointer): TObject - при створенні об'єкта инициализирует нулями виділену пам'ять. На практиці немає необхідності викликати цей метод явно.
  • CleanupInstance - звільняє пам'ять, займану полями з типом string, Variant, динамічний масив і інтерфейс. На практиці немає необхідності викликати цей метод явно.
  • ClassType: TClass - повертає описувач класу (метаклассом).
  • ClassName: ShortString - повертає ім'я класу.
  • ClassNameIs (const Name: string): Boolean - перевіряє, чи є заданий рядок ім'ям класу.
  • ClassParent: TClass - повертає описувач базового класу.
  • ClassInfo: Pointer - повертає покажчик на відповідну класу таблицю RTTI (від англ. Runtime Type Information). Таблиця RTTI використовується для перевірки типів даних на етапі виконання програми.
  • InstanceSize: Longint - повертає кількість байт, необхідних для зберігання в пам'яті одного об'єкта відповідного класу. Зауважимо, що значення, що повертається цим методом і значення, що повертається функцією SizeOf при передачі їй як аргумент об'єктної змінної - це різні значення. Функція SizeOf завжди повертає значення 4 (SizeOf (Pointer)), оскільки об'єктна змінна - це ні що інше, як посилання на дані об'єкта в пам'яті. Значення InstanceSize - це розмір цих даних, а не розмір об'єктної змінної.
  • InheritsFrom (AClass: TClass): Boolean - перевіряє, чи є клас AClass базовим класом.
  • MethodAddress (const Name: ShortString): Pointer - повертає адресу published-методу, ім'я якого задається параметром Name.
  • MethodName (Address: Pointer): ShortString - повертає ім'я published-методу за заданою адресою.
  • FieldAddress (const Name: ShortString): Pointer - повертає адресу published-поля, ім'я якого задається параметром Name.
  • GetInterface (const IID: TGUID; out Obj): Boolean - повертає посилання на інтерфейс через параметр Obj; ідентифікатор інтерфейсу задається параметром IID. (Інтерфейси розглянуті в главі 6)
  • GetInterfaceEntry (const IID: TGUID): PInterfaceEntry - повертає інформацію про інтерфейс, який реалізується класом. Ідентифікатор інтерфейсу задається параметром IID.
  • GetInterfaceTable: PInterfaceTable - повертає покажчик на таблицю з інформацією про всі інтерфейси, що реалізуються класом.
  • AfterConstruction - автоматично викликається після створення об'єкта. Метод не призначений для явного виклику з програми. Використовується для того, щоб виконати певні дії вже після створення об'єкта (для цього його необхідно перевизначити в похідних класах).
  • BeforeDestruction - автоматично викликається перед знищенням об'єкта. Метод не призначений для явного виклику з програми. Використовується для того, щоб виконати певні дії безпосередньо перед знищенням об'єкта (для цього його необхідно перевизначити в похідних класах).
  • Dispatch (var Message) - служить для виклику методів, оголошених з ключовим словом message.
  • DefaultHandler (var Message) - викликається методом Dispatch в тому випадку, якщо метод, відповідний повідомленням Message, не був знайдений.
  • NewInstance: TObject - викликається при створенні об'єкта для виділення динамічної пам'яті, щоб розмістити в ній дані об'єкта. Метод викликається автоматично, тому немає необхідності викликати його явно.
  • FreeInstance - викликається при знищенні об'єкта для звільнення зайнятої об'єктом динамічної пам'яті. Метод викликається автоматично, тому немає необхідності викликати його явно.
  • Destroy - стандартний деструктор.

Перекриття атрибутів в спадкоємців

У механізмі наслідування можна умовно виділити три основні моменти:

  • успадкування полів;
  • успадкування властивостей;
  • успадкування методів.

Будь породжений клас успадковує від батьківського все поля даних, тому класи TDelimitedReader і TFixedReader автоматично містять поля FFile, FActive і FItems, оголошені в класі TTextReader. Доступ до полів предка здійснюється по імені, як якщо б вони були визначені в нащадку. В нащадках можна визначати нові поля, але їх імена повинні відрізнятися від імен полів предка.

Спадкування властивостей і методів має свої особливості.

Властивість базового класу можна перекрити (від англ. Override) в похідному класі, наприклад щоб додати йому новий атрибут доступу або пов'язати з іншим полем або методом.

Метод базового класу теж можна перекрити в похідному класі, наприклад щоб змінити логіку його роботи. Звернемося до класів TDelimitedReader і TFixedReader. У них методи PutItem, GetItem, SetActive і GetEndOfFile успадковані від TTextReader, оскільки логіка їх роботи не залежить від того, в якому форматі зберігаються дані у файлі. А ось метод ParseLine перекритий, так як метод аналізу рядків залежить від формату даних:

function TDelimitedReader.ParseLine (const Line: string): Integer; var S: string; P: Integer; begin S: = Line; Result: = 0; repeat P: = Pos (Delimiter, S); // Пошук роздільник if P = 0 then // Якщо роздільник не знайдений, то вважається, що P: = Length (S) + 1; // роздільник знаходиться за останнім символом PutItem (Result, Copy (S, 1, P - 1)); // Установка елемента Delete (S, 1, P); // Видалення елемента з рядка Result: = Result + 1; // Перехід до наступного елементу until S = ''; // Поки в рядку є символи end; function TFixedReader.ParseLine (const Line: string): Integer; var I, P: Integer; begin P: = 1; for I: = 0 to High (FItemWidths) do begin PutItem (I, Copy (Line, P, FItemWidths [I])); // Установка елемента P: = P + FItemWidths [I]; // Перехід до наступного елементу end; Result: = Length (FItemWidths); // Кількість елементів постійно end;

У класах TDelimitedReader і TFixedReader перекритий ще й конструктор Create. Це необхідно для ініціалізації специфічних полів цих класів (поля FDelimiter в класі TDelimitedReader і поля FItemWidths в класі TFixedReader):

constructor TDelimitedReader.Create (const FileName: string; const ADelimiter: Char = ';'); begin inherited Create (FileName); FDelimiter: = ADelimiter; end; constructor TFixedReader.Create (const FileName: string; const AItemWidths: array of Integer); var I: Integer; begin inherited Create (FileName); // Копіювання AItemWidths в FItemWidths SetLength (FItemWidths, Length (AItemWidths)); for I: = 0 to High (AItemWidths) do FItemWidths [I]: = AItemWidths [I]; end;

Як видно з прикладу, в спадкоємця можна викликати перекритий метод предка, вказавши перед ім'ям методу зарезервоване слово inherited. Коли метод предка повністю збігається з методом нащадка по формату заголовка, то можна використовувати більш коротку запис. Скористаємося їй і перепишемо деструктор в класі TTextReader правильно:

destructor TTextReader.Destroy; begin Active: = False; inherited; // Еквівалентно: inherited Destroy; end;

Два останні приклади демонструють важливий принцип реалізації конструкторів і деструкторів. У конструкторах спочатку викликається конструктор предка, а потім не започатковано додаткові поля даних. У деструкторами застосовується зворотна послідовність дій: спочатку руйнуються дані, недоступні предку, а потім викликається успадкований деструктор. Завжди користуйтеся цими правилами в своїх програмах, щоб уникнути помилок.

Сумісність об'єктів різних класів

Для класів, пов'язаних ставленням спадкування, вводиться нове правило сумісності типів. Замість об'єкта базового класу можна підставити об'єкт будь-якого похідного класу. Зворотне невірно. Наприклад, змінної типу TTextReader можна привласнити значення змінної типу TDelimitedReader:

var Reader: TTextReader; ... Reader: = TDelimitedReader.Create ( 'MyData.del', ';');

Об'єктна змінна Reader формально має тип TTextReader, а фактично пов'язана з екземпляром класу TDelimitedReader.

Правило сумісності класів найчастіше застосовується при передачі об'єктів в параметрах процедур і функцій. Наприклад, якщо процедура працює з об'єктом класу TTextReader, то замість нього можна передати об'єкт класу TDelimitedReader або TFixedReader.

Зауважимо, що всі об'єкти є представниками відомого вам класу TObject. Тому будь-який об'єкт будь-якого класу можна використовувати як об'єкт класу TObject.

Контроль і перетворення типів

Оскільки реальний екземпляр об'єкту може виявитися спадкоємцем класу, вказаного при описі об'єктної змінної або параметра, буває необхідно перевірити, до якого класу належить об'єкт насправді. Щоб програміст міг виконувати такого роду перевірки, кожен об'єкт зберігає інформацію про свій клас. У мові Delphi існують оператори is і as, за допомогою яких виконується відповідно перевірка на тип (type checking) і перетворення до типу (type casting).

Наприклад, щоб з'ясувати, чи належить деякий об'єкт Obj до класу TTextReader або його спадкоємцю, слід використовувати оператор is:

var Obj: TObject; ... if Obj is TTextReader then ...

Для перетворення об'єкта до потрібного типу використовується оператор as, наприклад

with Obj as TTextReader do Active: = False;

Варто зазначити, що для об'єктів застосуємо і звичайний спосіб приведення типу:

with TTextReader (Obj) do Active: = False;

Варіант з оператором as краще, оскільки безпечний. Він генерує помилку (точніше виняткову ситуацію; про виняткові ситуації ми розповімо в розділі 4) при виконанні програми (run-time error), якщо реальний екземпляр об'єкту Obj не сумісний з класом TTextReader. Забігаючи наперед, скажемо, що помилку приведення типу можна обробити і таким чином уникнути дострокового завершення програми.

Віртуальні методи

Поняття віртуального методу

Всі методи, які до сих пір розглядалися, мають одну спільну рису - всі вони статичні. При зверненні до статичного методу компілятор точно знає клас, якому даний метод належить. Тому, наприклад, звернення до статичного методу ParseLine в методі NextLine (що належить класу TTextReader) компілюється в виклик TTextReader.ParseLine:

function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result: = not EndOfFile; if Result then begin Readln (FFile, S); N: = ParseLine (S); // компілює в виклик TTextReader.ParseLine (S); if N <> ItemCount then SetLength (FItems, N); end; end;

В результаті метод NextLine працює неправильно в спадкоємців класу TTextReader, так як всередині нього виклик перекритого методу ParseLine не відбувається. Звичайно, в класах TDelimitedReader і TFixedReader можна продублювати всі методи і властивості, які прямо або побічно викликають ParseLine, але при цьому втрачаються переваги успадкування, і ми повертаємося до того, що необхідно описати два класи, в яких велика частина коду ідентична. ООП пропонує витончене рішення цієї проблеми - метод ParseLine всього-на-всього оголошується віртуальним:

type TTextReader = class ... function ParseLine (const Line: string): Integer; virtual; // Віртуальний метод ... end;

Оголошення віртуального методу в базовому класі виконується за допомогою ключового слова virtual, а його перекриття в похідних класах - за допомогою ключового слова override. Перекритий метод повинен мати точно такий же формат (список параметрів, а для функцій ще й тип значення), що і перекривається:

type TDelimitedReader = class (TTextReader) ... function ParseLine (const Line: string): Integer; override; ... end; TFixedReader = class (TTextReader) ... function ParseLine (const Line: string): Integer; override; ... end;

Суть віртуальних методів в тому, що вони викликаються за фактичним типу примірника, а не за формальним типом, записаному в програмі. Тому після зроблених змін метод NextLine буде працювати так, як очікує програміст:

function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result: = not EndOfFile; if Result then begin Readln (FFile, S); N: = ParseLine (S); // Працює як <фактичний клас> .ParseLine (S) if N <> ItemCount then SetLength (FItems, N); end; end;

Робота віртуальних методів заснована на механізмі пізнього зв'язування (late binding). На відміну від раннього зв'язування (early binding), характерного для статичних методів, пізнє скріплення засноване на обчисленні адреси викликається методу при виконанні програми. Адреса методу обчислюється по зберігається в кожному об'єкті описателю класу.

Завдяки механізму наслідування і віртуальних методів в середовищі Delphi реалізується така концепція ООП як поліморфізм. Поліморфізм істотно полегшує працю програміста, оскільки забезпечує повторне використання коду вже написаних і налагоджених методів.

Механізм виклику віртуальних методів

Робота віртуальних методів заснована на непрямому виклику підпрограм. При непрямому виклику команда виклику підпрограми оперує не адресою підпрограми, а адресою місця в пам'яті, де зберігається адреса підпрограми. Ви вже стикалися з непрямим викликом при використанні процедурних змінних. Процедурна змінна і була тим місцем в пам'яті, де зберігався адреса підпрограми, що викликається. Для кожного віртуального методу теж створюється процедурна змінна, але її наявність і використання приховано від програміста.

Всі процедурні змінні з адресами віртуальних методів пронумеровані і зберігаються в таблиці, званої таблицею віртуальних методів (VMT - від англ. Virtual Method Table). Така таблиця створюється одна для кожного класу об'єктів, і всі об'єкти цього класу зберігають на неї посилання.

Структуру об'єкта в оперативній пам'яті пояснює малюнок 3:

Малюнок 3. Структура об'єкта TTextReader в оперативній пам'яті

Виклик віртуального методу здійснюється наступним чином:

  1. Через об'єктну змінну виконується звернення до зайнятого об'єктом блоку пам'яті;
  2. Далі з цього блоку витягується адреса таблиці віртуальних методів (він записаний в чотирьох перших байтах);
  3. На підставі порядкового номера віртуального методу витягується адреса відповідної підпрограми;
  4. Викликається код, що знаходиться за цією адресою.

Покажемо, як можна реалізувати непрямий виклик віртуального методу ParseLine (він має нульовий номер в таблиці віртуальних методів) звичайними засобами процедурного програмування:

type TVMT = array [0..9999] of Pointer; TParseLineFunc = function (Self: TTextReader; const Line: string): Integer; var Reader: TTextReader; // об'єктна змінна ObjectDataPtr: Pointer; // покажчик на яку він обіймав об'єктом блок пам'яті VMTPtr: ^ TVMT; // покажчик на таблицю віртуальних методів MethodPtr: Pointer; // покажчик на метод begin ... ObjectDataPtr: = Pointer (Reader); // 1) звернення до даних об'єкта VMTPtr: = Pointer (ObjectDataPtr ^); // 2) витяг адреси VMT MethodPtr: = VMTPtr ^ [0]; // 3) витяг адреси методу з VMT TParseLineFunc (MethodPtr) (Reader, S); // 4) виклик методу ... end.

Підтримка механізму виклику віртуальних методів на рівні мови Delphi позбавляє програміста від всієї цієї складності.

Абстрактні віртуальні методи

При побудові ієрархії класів часто виникає ситуація, коли робота віртуального методу в базовому класі не відома і наповнюється змістом тільки в спадкоємців. Так сталося, наприклад, з методом ParseLine, тіло якого в класі TTextReader оголошено порожнім. Звичайно, тіло методу завжди можна зробити порожнім або майже порожнім (так ми і вчинили), але краще скористатися директивою abstract:

type TTextReader = class ... function ParseLine (const Line: string): Integer; virtual; abstract; ... end;

Директива abstract записується після слова virtual і виключає необхідність написання коду віртуального методу для даного класу. Такий метод називається абстрактним, тобто має на увазі логічне дію, а не конкретний спосіб його реалізації. Абстрактні віртуальні методи часто використовуються при створенні класів-напівфабрикатів. Свою реалізацію такі методи отримують в закінчених спадкоємців.

динамічні методи

Різновидом віртуальних методів є так звані динамічні методи. При їх оголошенні замість ключового слова virtual записується ключове слово dynamic, наприклад:

type TTextReader = class ... function ParseLine (const Line: string): Integer; dynamic; abstract; ... end;

У спадкоємців динамічні методи перекриваються так само, як і віртуальні - за допомогою зарезервованого слова override.

За змістом динамічні і віртуальні методи ідентичні. Різниця полягає лише в механізмі їх виклику. Методи, оголошені до директиви virtual, викликаються максимально швидко, але платою за це є великий розмір системних таблиць, за допомогою яких визначаються їх адреси. Розмір цих таблиць починає позначатися із збільшенням числа класів в ієрархії. Методи, оголошені з директивою dynamic викликаються трохи довше, але при цьому таблиці з адресами методів мають більш компактний вигляд, що сприяє економії пам'яті. Таким чином, програмісту надаються два способи оптимізації об'єктів: по швидкості роботи (virtual) або за обсягом пам'яті (dynamic).

Методи обробки повідомлень

Спеціалізованою формою динамічних методів є методи обробки повідомлень. Вони оголошуються за допомогою ключового слова message, за яким слід целочисленная константа - номер повідомлення. Наступний приклад взятий з вихідних текстів бібліотеки VCL:

type TWidgetControl = class (TControl) ... procedure CMKeyDown (var Msg: TCMKeyDown); message CM_KEYDOWN; ... end;

Метод обробки повідомлень має формат процедури і містить єдиний var-параметр. При перекритті такого методу назву методу і ім'я параметра можуть бути будь-якими, важливо лише, щоб незмінним залишився номер повідомлення, що використовується для виклику методу. Виклик методу виконується не по імені, як зазвичай, а з допомогою звернення до спеціального методу Dispatch, який є в кожному класі (метод Dispatch визначено в класі TObject).

Методи обробки повідомлень застосовуються всередині бібліотеки VCL для обробки команд призначеного для користувача інтерфейсу і рідко потрібні при написанні прикладних програм.

наступна стаття серії

додаткова інформація

За додатковою інформацією звертайтеся в компанію Interface Ltd.

Обговорити на форумі Borland


Для чого тоді потрібен клас TTextReader?

Новости

Banwar.org
Наша совместная команда Banwar.org. Сайт казино "Пари Матч" теперь доступен для всех желающих, жаждущих волнения и азартных приключений.