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

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


Зміст



Спадкування


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


Класи інкапсулюють (тобто включають в себе) поля, методи і властивості; це їхня перша риса. Наступна не менш важлива риса класів – здатність наслідувати поля, методи і властивості інших класів. Щоб пояснити сутність спадкування звернемося до прикладу з читачем текстових файлів у форматі "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. Дерево класів

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

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

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


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

type
TTextReader = class

end;

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

type
TTextReader = class(TObject)

end;

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

Таким чином, повне дерево класів для читання елементів з текстового файлу в різних форматах виглядає так, як показано на малюнку 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:


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


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


Будь породжений клас успадковує від батьківського всі поля даних, тому класи 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 для обробки команд користувальницького інтерфейсу і рідко потрібні при написанні прикладних програм.

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


Посилання по темі



Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*