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

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


Зміст



Об'єкти – це найбільше досягнення в сучасній технології програмування. Вони дозволили будувати програму не з жахливих за складністю процедур і функцій, а з цеглинок-об'єктів, заздалегідь наділених потрібними властивостями. Найприємніше в об'кти те, що їх внутрішня складність прихована від програміста, який просто користується готовим будівельним матеріалом.

Зараз переваги використання об'єктів очевидні для всіх. Проте так було не завжди. Спочатку стара гвардія не зрозуміла і не прийняла об'єкти, тому вони майже 20 років потихеньку розвивалися в різних мовах, першим з яких була Simula 67. Поступово об'єктно-орієнтований підхід знайшов собі місце і в більш потужних мовах, таких як C + +, Delphi і безлічі інших мов. Блискучим прикладом реалізації об'єктів була бібліотека Turbo Vision, призначена для побудови користувальницького інтерфейсу програм в операційній системі MS-DOS.

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

Наріжні камені ООП


Формула об'єкта


Автори сподіваються, що читач пам'ятає дещо з глави 2 і такі поняття як тип даних, процедура, функція, запис для нього не в новинку. Це прекрасно. Так от, в кінці 60-х років кому-то прийшло в голову об'єднати ці поняття, і те, що вийшло, назвати об'єктом. Розгляд даних в нерозривному зв'язку з методами їх обробки дозволило вивести формулу об'єкта:

Об'єкт = Дані + Операції

Природа об'єкта


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

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

Наприклад, об'єкт "кнопка" має властивість "колір". Значення кольору кнопка запам'ятовує в одному зі своїх полів. При зміні значення властивості "колір" викликається метод, який перемальовує кнопку.

До речі, цей приклад дозволяє зробити важливий висновок: властивості мають першорядне значення для програміста, що використовує об'єкт. Щоб зрозуміти суть і призначення об'єкту ви обов'язково повинні знати його властивості, іноді – методи, дуже рідко – поля (об'єкт і сам знає, що з ними робити).

Об'єкти і компоненти


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

Компоненти в середовищі Delphi – це особливі об'єкти, які є будівельними цеглинками візуального середовища розробки і пристосовані до візуальної установці властивостей. Щоб перетворити об'єкт в компонент, перший розробляється за певними правилами, а потім поміщається в палітру компонентів. Конструюючи додаток, ви берете компоненти з Палітри Компонентів, маєте на формі і встановлюєте їх властивості у вікні Інспектора Об'єктів. Зовні все виглядає просто, але щоб досягти такої простоти, потрібно було створити механізми, що забезпечують функціонування об'єктів-компонентів вже на етапі проектування додатку! Всі це було придумано і блискуче реалізовано в середовищі Delphi. Таким чином, компонентний підхід значно спростив створення додатків із графічним інтерфейсом і дав поштовх розвитку нової індустрії компонентів.

У цьому розділі ми розглянемо лише питання створення і використання об'єктів. Трохи пізніше ми навчимо вас перетворювати об'єкти в компоненти (див. розділ 13).

Класи об'єктів


Кожен об'єкт завжди належить деякому класу об'єктів. Клас об'єктів – Це узагальнена (абстрактне) опис безлічі однотипних об'єктів. Об'єкти є конкретними представниками свого класу, їх прийнято називати екземплярами класу. Наприклад, клас СОБАКИ – поняття абстрактне, а екземпляр цього класу МІЙ ПЄС Бобик – поняття конкретне.

Три кити ООП


Весь світ ООП тримається на трьох китах: інкапсуляції, спадкуванні та поліморфізм. Для початку про них треба мати тільки саме загальне уявлення.

Об'єднання даних і операцій в одну сутність – об'єкт – тісно пов'язано з поняттям інкапсуляції, Що означає приховування внутрішнього устрою. Інкапсуляція робить об'єкти схожими на маленькі програмні модулі, в яких приховані внутрішні дані і у яких є інтерфейс використання в вигляді підпрограм. Перехід від понять "структура даних" та "алгоритм" до поняття "об'єкт" значно підвищив ясність і надійність програм.

Другий кит ООП – спадкування. Цей простий принцип означає, що якщо ви хочете створити новий клас об'єктів, який розширює можливості вже існуючого класу, то немає необхідності в переписуванні заново всіх полів, методів і властивостей. Ви повідомляєте, що новий клас є нащадком (або дочірнім класом) наявного класу об'єктів, званого предком (або батьківським класом), і додаєте до нього нові поля, методи і властивості. Процес породження нових класів на основі інших класів називається спадкуванням. Нові класи об'єктів мають як успадковані ознаки, так і, можливо, нові. Наприклад, клас СОБАКИ успадкував багато властивості своїх предків – ВОЛКОВ.

Третій кит – це поліморфізм. Він означає, що в похідних класах ви можете змінювати роботу вже існуючих в базовому класі методів. При цьому весь програмний код, керуючий об'єктами батьківського класу, придатний для управління об'єктами дочірнього класу без всякої модифікації. Наприклад, ви можете породити новий клас кнопок з рельєфним написом, перевизначивши метод малювання кнопки. Нову кнопку можна "підсунути" замість стандартної в яку-небудь підпрограму, що викликає малювання кнопки. При цьому підпрограма "думає", що працює зі стандартною кнопкою, але насправді кнопка належить похідному класу кнопок і відображається в новому стилі. Поки досить самого поверхневого розуміння всіх наведених вище понять, нижче ми розглянемо їх докладніше і покажемо, як вони реалізовані в середовищі Delphi.

Класи


Для підтримки ООП в мову Delphi введені об'єктні типи даних, за допомогою яких одночасно описуються дані та операції над ними. Об'єктні типи даних називають класами, А їх екземпляри – об'єктами.

Класи об'єктів визначаються в секції type глобального блоку. Опис класу починається з ключового слова class і закінчується ключовим словом end. За формою оголошення класи схожі на звичайні записи, але крім полів даних можуть містити оголошення користувацьких процедур і функцій. Такі процедури і функції узагальнено називають методами, Вони призначені для виконання над об'єктами різних операцій. Наведемо приклад оголошення класу, який призначений для читання текстового файлу в форматі "delimited text" (файл у такому форматі являє собою послідовність рядків, кожна рядок складається зі значень, які відокремлені один від одного символом-роздільником):

type
TDelimitedReader = class
/ / Поля
FileVar: TextFile;
Items: array of string;
Delimiter: Char;
/ / Методи
procedure PutItem(Index: Integer; const Item: string);
procedure SetActive(const AActive: Boolean);
function ParseLine(const Line: string): Integer;
function NextLine: Boolean;
function GetEndOfFile: Boolean;
end;

Клас містить поля (FileVar, Items, Delimiter) і методи (PutItem, SetActive, ParseLine, NextLine, GetEndOfFile). Заголовки методів, (завжди) наступні за списком полів, грають роль запобіжних (forward) описів. Програмний код методів пишеться окремо від визначення класу і буде приведений пізніше.

Клас звичайно описує сутність, модельовану в програмі. Наприклад, клас TDelimitedReader являє собою "читач" текстового файлу з розбором зчитувальних рядків на елементи (підрядка), які відділені один від одного деяким символом, званим роздільником.

Клас містить кілька полів:


Клас також містить ряд методів (процедур і функцій):


Зверніть увагу, що наведене вище опис є нічим іншим, як декларацією інтерфейсу для роботи з об'єктами класу TDelimitedReader. Реалізація методів PutItem, SetActive, ParseLine, NextLine і GetEndOfFile на даний момент відсутня, однак для створення і використання примірників класу вона поки і не потрібна.

У певному сенсі об'єкти схожі на програмні модулі, для використання яких необхідно вивчити лише інтерфейсну частину, розділ реалізації для цього вивчати не потрібно. Тому далі від опису класу ми перейдемо не до реалізації методів, а до створення на їх основі об'єктів.

Об'єкти


Щоб від опису класу перейти до об'єкту, слід виконати відповідне оголошення в секції var:

var
Reader: TDelimitedReader;

При роботі із звичайними типами даних цього оголошення було б достатньо для отримання примірника типу. Однак об'єкти в середовищі Delphi є динамічними даними, тобто розподіляються в динамічній пам'яті. Тому мінлива Reader – це просто посилання на екземпляр (об'єкт в пам'яті), якого фізично ще не існує. Щоб сконструювати об'єкт (виділити пам'ять для екземпляра) класу TDelimitedReader і пов'язати з ним змінну Reader, потрібно в тексті програми помістити наступний оператор:

Reader := TDelimitedReader.Create;

Create – Це так званий конструктор об'єкта; він завжди присутній у класі і служить для створення і ініціалізації екземплярів. При створенні об'єкту в пам'яті виділяється місце тільки для його полів. Методи, як і звичайні процедури і функції, поміщаються в область коду програми; вони вміють працювати з будь-якими екземплярами свого класу і не дублюються в пам'яті.

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

Reader.NextLine;

Крім того, як і при роботі з записами, допустимо використання оператора with, Наприклад:

with Reader do
NextLine;

Якщо об'єкт стає непотрібним, він повинен бути вилучений викликом спеціального методу Destroy, наприклад:

 Reader.Destroy; / / Звільнення пам'яті, займаної об'єктом

Destroy – Це так званий деструктор об'єкта; він присутній в класі поряд з конструктором і служить для видалення об'єкту з динамічної пам'яті. Після виклику деструктора мінлива Reader стає незв'язаної і не повинна використовуватися для доступу до полів і методів вже неіснуючого об'єкта. Щоб відрізняти в програмі пов'язані об'єктні змінні від незв'язаних, останні слід ініціалізувати значенням nil. Наприклад, в наступному фрагменті звернення до деструктор Destroy виконується тільки в тому випадку, якщо об'єкт реально існує:

Reader := nil;

if Reader <> nil then Reader.Destroy;

Виклик деструктора для неіснуючих об'єктів недопустимий і при виконанні програми призведе до помилки. Щоб позбавити програмістів від зайвих помилок, в об'єкти ввели зумовлений метод Free, який слід викликати замість деструктора. Метод Free сам викликає деструктор Destroy, але тільки в тому випадку, якщо значення об'єктної змінної не дорівнює nil. Тому останній рядок у наведеному вище прикладі можна переписати наступним чином.

Reader.Free;

Після знищення об'єкта мінлива Reader зберігає своє значення, продовжуючи посилатися на місце в пам'яті, де об'єкту вже немає. Якщо цю змінну передбачається ще використовувати, то бажано привласнити їй значення nil, щоб програма могла перевірити, існує об'єкт чи ні. Таким чином, найбільш правильна послідовність дій при знищенні об'єкту повинна бути наступна:

Reader.Free;
Reader := nil;

За допомогою стандартної процедури FreeAndNil це можна зробити простіше і елегантніше:

 FreeAndNil (Reader); / / Еквівалентно: Reader.Free; Reader: = nil;

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

var
R1, R2: TDelimitedReader; / / Змінні R1 і R2 не пов'язані з об'єктом
begin
R1: = TDelimitedReader.Create; / / Зв'язування змінної R1 з новим об'єктом
/ / Змінна R2 поки що не пов'язана ні з яким об'єктом
R2: = R1; / / Зв'язування змінної R2 з тим же об'єктом, що і R1
/ / Тепер обидві змінні зв'язані з одним об'єктом
R2.Free; / / Знищення об'єкта
/ / Тепер R1 і R2 не пов'язані ні з яким об'єктом
end;

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

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

type
TReadersList = class; / / попереджуюче оголошення класу TReadersList

TDelimitedReader = class
Owner: TReadersList;

end;

TReadersList = class
Readers: array of TDelimitedReader;

end;


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

Отже, ви вже маєте деяке уявлення про об'єкти, перейдемо тепер до питання реалізації їх методів.

Конструктори і деструктори


Особливою різновидом методів є конструктори і деструктори. Нагадаємо, що конструктори створюють, а деструктори руйнують об'єкти. Створення об'єкта включає виділення пам'яті під екземпляр і ініціалізацію його полів, а руйнування – очищення полів і звільнення пам'яті. Дії з ініціалізації та очищення полів специфічні для кожного конкретного класу об'єктів. З цієї причини мова Delphi дозволяє перевизначити стандартний конструктор Create і стандартний деструктор Destroy для виконання будь-яких корисних дій. Можна навіть визначити кілька конструкторів і деструкторів (імена їм призначає сам програміст), щоб забезпечити різні процедури створення і руйнування об'єктів.

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

type
TDelimitedReader = class

/ / Конструктори і деструктори
constructor Create (const FileName: string; const ADelimiter: Char =;);
destructor Destroy; override;

end;

Наведемо їх можливу реалізацію:

constructor TDelimitedReader.Create(const FileName: string;
const ADelimiter: Char = ;);
begin
AssignFile(FileVar, FileName);
Delimiter := ADelimiter;
end;

destructor TDelimitedReader.Destroy;
begin
/ / Поки нічого не робимо
end;


Якщо об'єкт містить вбудовані об'єкти або інші динамічні дані, то конструктор – це якраз те місце, де їх потрібно створювати.

Конструктор застосовується до класу або до об'єкта. Якщо він застосовується до класу,

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

то виконується наступна послідовність дій:


Якщо конструктор застосовується до об'єкту,

Reader.Create(MyData.del, ;);

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

Деструктор знищує об'єкт, до якого застосовується:

Reader.Destroy;

У результаті:


Як і звичайні методи, деструктор може мати параметри, але ця можливість використовується рідко.

Методи


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

 procedure TDelimitedReader.SetActive (const AActive: Boolean);
begin
if AActive then
Reset (FileVar) / / Відкриття файлу
else
CloseFile (FileVar); / / Закриття файлу
end;

Зверніть увагу, що всередині методів звернення до полів та інших методів виконуються як до звичайних змінних і підпрограм без уточнення екземпляра об'єкту. Таке спрощення досягається шляхом використання в межах методу псевдопеременной Self (стандартний ідентифікатор). Фізично Self являє собою додатковий неявний параметр, переданий в метод при виклику. Цей параметр і вказує примірник об'єкта, до якого даний метод застосовується. Щоб пояснити сказане, перепишемо метод SetActive, представивши його у вигляді звичайної процедури:

 procedure TDelimitedReader_SetActive (Self: TDelimitedReader;
const AActive: Boolean);
begin
if AActive then
Reset (Self.FileVar) / / Відкриття файлу
else
CloseFile (Self.FileVar); / / Закриття файлу
end;

Погодьтеся, що метод SetActive виглядає лаконічніше процедури TDelimitedReader_SetActive.

Практика показує, що псевдопеременная Self рідко використовується в явному вигляді. Її необхідно застосовувати тільки тоді, коли при написанні методу може виникнути будь-яка двозначність для компілятора, наприклад при використанні однакових імен та для локальних змінних, і для полів об'єкта.

Якщо виконати метод SetActive,

Reader.SetActive(True);

то оброблюваний файл буде відкритий. При цьому неявний параметр Self буде містити значення змінної Reader. Такий виклик реалізується звичайними засобами процедурного програмування приблизно так:

TDelimitedReader_SetActive(Reader, True);

Властивості


Поняття властивості


Окрім полів і методів в об'єктах існують властивості. При роботі з об'єктом властивості виглядають як поля: вони приймають значення і беруть участь у виразах. Але на відміну від полів властивості не займають місця в пам'яті, а операції їхнього читання і запису асоціюються із звичайними полями або методами. Це дозволяє створювати необхідні супутні ефекти при зверненні до властивостей. Наприклад, в об'єкті Reader присвоювання властивості Active значення True викличе відкриття файлу, а присвоювання значення False – закриття файлу. Створення супутнього ефекту (відкриття або закриття файлу) досягається тим, що за присвоюванням властивості значення стоїть виклик методу.

Оголошення властивості виконується за допомогою зарезервованого слова property, Наприклад:

type
TDelimitedReader = class

FActive: Boolean;

/ / Метод запису (установки значення) властивості
procedure SetActive(const AActive: Boolean);
property Active: Boolean read FActive write SetActive; / / Властивість
end;

Ключові слова read і write називаються специфікаторами доступу. Після слова read указується поле або метод, до якого відбувається звернення при читанні (отриманні) значення властивості, а після слова write – поле або метод, до якого відбувається звернення при записі (установці) значення властивості. Наприклад, читання властивості Active означає читання поля FActive, а установка властивості – виклик методу SetActive. Щоб імена властивостей не збігалися з іменами полів, останні прийнято писати з літери F (від англ. field). Ми надалі також будемо користуватися цією угодою. Почнемо з того, що перейменуємо поля класу TDelimitedReader: поле FileVar перейменуємо в FFile, Items – в FItems, а поле Delimiter – в FDelimiter.

type
TDelimitedReader = class
/ / Поля
FFile: TextFile; // FileVar -> FFile
FItems: array of string; // Items -> FItems
FActive: Boolean;
FDelimiter: Char; // Delimiter -> FDelimiter

end;

Звернення до властивостей виглядає в програмі як звернення до полів:

var
Reader: TDelimitedReader;
IsOpen: Boolean;

Reader.Active: = True; / / Еквівалентно Reader.SetActive (True);
IsOpen: = Reader.Active; / / Еквівалентно IsOpen: = Reader.FActive

Якщо один з специфікатором доступу опущений, то значення властивості можна або тільки читати (заданий специфікатор read), або тільки записувати (заданий специфікатор write). У наступному прикладі оголошено властивість, значення якого можна тільки читати.

type
TDelimitedReader = class

FItems: array of string;

function GetItemCount: Integer;

property ItemCount: Integer read GetItemCount; / / Тільки для читання!
end;

function TDelimitedReader.GetItemCount: Integer;
begin
Result := Length(FItems);
end;


Тут властивість ItemCount показує кількість елементів в масиві FItems. Оскільки воно визначається в результаті читання та розбору чергового рядка файлу, користувачеві об'єкта дозволено лише дізнаватися кількість елементів.

На відміну від полів властивості не мають адреси в пам'яті, тому до них заборонено застосовувати операцію @. Як наслідок, їх не можна передавати в var– І out-Параметрах процедур і функцій.

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

Методи отримання і установки значень властивостей


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

type
TDelimitedReader = class
FActive: Boolean;

procedure SetActive(const AActive: Boolean);
function GetItemCount: Integer;

property Active: Boolean read FActive write SetActive;
property ItemCount: Integer read GetItemCount;
end;

Використання методів для отримання і установки властивостей дозволяє перевірити коректність значення властивості, зробити додаткові обчислення, встановити значення залежних полів і т.д. Наприклад, у методі SetActive цілком доцільно здійснити перевірку стану файлу (відкритий або закритий), щоб уникнути його повторного відкриття або закриття:

 procedure TDelimitedReader.SetActive (const AActive: Boolean);
begin
if Active <> AActive then / / Якщо стан зміниться
begin
if AActive then
Reset (FFile) / / Відкриття файлу
else
CloseFile (FFile); / / Закриття файлу
FActive: = AActive; / / Збереження стану в поле
end;
end;

Наявність властивості Active дозволяє нам відмовитися від використання методів Open і Close, традиційних при роботі з файлами. Погодьтеся, що відкривати і закривати файл за допомогою властивості Active набагато зручніше і природніше. Одночасно з цим властивість Active можна використовувати і для перевірки стану файлу (відкритий чи ні). Таким чином, для здійснення трьох дій потрібно всього лише одна властивість! Це робить використання Ваших класів іншими програмістами простішим, оскільки їм легше запам'ятати одне поняття Active, ніж, наприклад, три методи: Open, Close і IsOpen.

Значення властивості може не зберігатися, а обчислюватися при кожному зверненні до властивості. Прикладом є властивість ItemCount, значення якого обчислюється як Length (FItems).

Властивості-масиви


Крім звичайних властивостей в об'єктах існують властивості-масиви (array properties). Властивість-масив – це індексовані безліч значень. Наприклад, у класі TDelimitedReader безліч елементів, виділених з ліченої рядки, зручно представити у вигляді властивості-масиву:

type
TDelimitedReader = class

FItems: array of string;

function GetItem(Index: Integer): string;

property Items[Index: Integer]: string read GetItem;
end;

function TDelimitedReader.GetItem(Index: Integer): string;
begin
Result := FItems[Index];
end;


Елементи масиву Items можна тільки читати, оскільки клас TDelimitedReader призначений тільки для читання даних з файлу.

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

Основна вигода від застосування властивості-масиву – можливість виконання ітерацій з допомогою циклу for, Наприклад:

var
Reader: TDelimitedReader;
I: Integer;

for I := 0 to Reader.ItemCount – 1 do
Writeln(Reader.Items[I]);


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

Властивості-масиви мають дві важливі відмінності від звичайних масивів:

їх індекси не обмежуються діапазоном і можуть мати будь-який тип даних, а не тільки Integer. Наприклад, можна створити властивість-масив, в якому індексами будуть рядки. Звернення до такого властивості могло б виглядати приблизно так:

Reader.Items[FirstName] := Alexander;

операції над властивістю-масивом в цілому заборонені; дозволені операції тільки з його елементами.

Властивість-масив як основну властивість об'єкта


Властивість-масив можна зробити основною властивістю об'єктів даного класу. І тому в опис властивості додається слово default:

type
TDelimitedReader = class

property Items[Index: Integer]: string read GetItem; default;

end;

Таке оголошення властивості Items дозволяє розглядати сам об'єкт класу TDelimitedReader як масив і опускати ім'я властивості-масиву при зверненні до нього з програми, наприклад:

var
R: TDelimitedReader;
I: Integer;

for I := 0 to R.ItemCount – 1 do
Writeln(R[I]);


Слід пам'ятати, що тільки властивості-масиви можуть бути основними властивостями об'єктів; для звичайних властивостей це неприпустимо.

Методи, які обслуговують кілька властивостей


Один і той же метод може використовуватися для отримання (установки) значень декількох властивостей одного типу. У цьому випадку кожній властивості призначається цілочисельний індекс, який передається в метод читання (Записи) першим параметром.

У наступному прикладі вже відомий Вам метод GetItem обслуговує три властивості: FirstName, LastName і Phone:

type
TDelimitedReader = class

property FirstName: string index 0 read GetItem;
property LastName: string index 1 read GetItem;
property Phone: string index 2 read GetItem;
end;

Звернення до властивостей FirstName, LastName і Phone замінюються компілятором на виклики одного і того ж методу GetItem, але з різними значеннями параметра Index:

var
Reader: TDelimitedReader;

Writeln (Reader.FirstName) / / Еквівалентно: Writeln (Reader.GetItem (0));
Writeln (Reader.LastName); / / Еквівалентно: Writeln (Reader.GetItem (1));
Writeln (Reader.Phone); / / Еквівалентно: Writeln (Reader.GetItem (2));


Зверніть увагу, що метод GetItem обслуговує як властивість-масив Items, так і властивості FirstName, LastName і Phone. Зручно, чи не так!

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

type
TDelimitedReader = class
/ / Поля
FFile: TextFile;
FItems: array of string;
FActive: Boolean;
FDelimiter: Char;
/ / Методи читання і запису властивостей
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 ADelimiter: Char =;);
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;
property Delimiter: Char read FDelimiter;
end;

{ TDelimitedReader }

constructor TDelimitedReader.Create(const FileName: string;
const ADelimiter: Char = ;);
begin
AssignFile(FFile, FileName);
FActive := False;
FDelimiter := ADelimiter;
end;

destructor TDelimitedReader.Destroy;
begin
Active := False;
end;

function TDelimitedReader.GetEndOfFile: Boolean;
begin
Result := Eof(FFile);
end;

function TDelimitedReader.GetItem(Index: Integer): string;
begin
Result := FItems[Index];
end;

function TDelimitedReader.GetItemCount: Integer;
begin
Result := Length(FItems);
end;

function TDelimitedReader.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 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;

procedure TDelimitedReader.PutItem (Index: Integer; const Item: string);
begin
if Index> High (FItems) then / / Якщо індекс виходить за межі масиву,
SetLength (FItems, Index + 1); / / то збільшення розміру масиву
FItems [Index]: = Item; / / Установка відповідного елемента
end;

procedure TDelimitedReader.SetActive(const AActive: Boolean);
begin
if Active <> AActive then / / Якщо стан зміниться
begin
if AActive then
Reset (FFile) / / Відкриття файлу
else
CloseFile (FFile); / / Закриття файлу
FActive: = AActive; / / Збереження стану в поле
end;
end;

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


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



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


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

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

Ваш отзыв

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

*

*