Перейти до вмісту

Object Pascal

Як влаштована мова Delphi: модулі й класи, ручне керування пам’яттю, інтерфейси з підрахунком посилань і можливості, що з’явилися після Delphi 7. На співбесіді часто просять порівняти це з .NET, тому порівняння з C# тут на кожному кроці.

Перед цією темою: Типи й пам’ять.

Junior+

Із чого складається програма на Object Pascal?

Section titled “Із чого складається програма на Object Pascal?”
Відповідь

Що це

Проєкт (.dpr) — головна програма, модуль (.pas) — файл із типами й підпрограмами, поділений на секції.

Секція Що в ній
interface оголошення, видимі іншим модулям
implementation код методів і приховані деталі
initialization, finalization код під час запуску й під час завершення

Як користуватися

  1. Підключіть модуль в uses — стануть доступні оголошення з його interface.
  2. Оголосіть клас із полями, методами й властивостями. Властивість пов’язує ім’я з полем або методом: property Number: string read FNumber write SetNumber;.
  3. Розподіліть члени за видимістю: private і protected — для реалізації, public — для всіх, published — для Object Inspector.

Типова помилка

Оголосити в нащадку метод з тією самою сигнатурою, але без override. Компілятор видасть попередження, а виклик через змінну типу предка піде в метод предка.

Як влаштовано

  • Порядок секцій. initialization модулів виконується в порядку підключення через uses, finalization — у зворотному порядку.
  • Циклічні посилання. Два модулі не можуть перелічити один одного в uses секцій interface. Цикл дозволений, якщо хоча б одне посилання стоїть в uses секції implementation.
  • Видимість. private і protected видно всьому модулю, де оголошено клас, — це «дружба» на рівні модуля. strict private обмежує доступ самим класом, strict protected — класом і нащадками.
  • published. Видимість як у public, але компілятор генерує RTTI. Клас для цього компілюється з {$M+} або успадковується від TPersistent.

Коли що обирати

Зв’язування Як викликається Коли
статичне (за замовчуванням) адреса відома під час компіляції метод не перевизначають
virtual через таблицю VMT звичайний поліморфізм
dynamic через пошук у таблиці DMT багато методів, які перевизначають рідко

Пастки

  • Приховування замість перевизначення. Метод з тією самою сигнатурою без override приховує метод предка, компілятор видає W1010. Якщо приховування навмисне, пишуть reintroduce.
  • Обхід властивості. Через модульну видимість private сусідній клас того самого модуля може писати просто в поле й обійти сетер.

Правило

Поля — у strict private, доступ — через властивості, поліморфізм — через virtual і override.

Під капотом

  • Кожен модуль компілюється в .dcu. Якщо змінилася лише implementation, модулі-клієнти не перекомпілюються: їм важливий тільки інтерфейс.
  • Об’єкт починається з вказівника на свій клас, тобто на VMT. У ній адреси всіх віртуальних методів класу й предків, тому виклик virtual — одне непряме звернення.
  • DMT зберігає лише методи, які клас оголосив або перевизначив. Виклик dynamic шукає метод ланцюжком предків: таблиці менші, виклик повільніший. VCL спирається на них і на message-методи для віконних повідомлень.
  • published-властивості потрапляють у RTTI. За ним потік .dfm відновлює значення властивостей форми, а Object Inspector їх показує.

Ціна

  • Довгі uses в interface подовжують збирання й породжують цикли, які доводиться розносити по implementation. Із Delphi 12 компілятор уміє вивантажувати граф uses у формат GraphViz.
  • Модульна видимість private заохочує модулі на тисячі рядків, де класи лізуть у поля один одного.

Компроміси

  • dynamic виправданий в ієрархіях на кшталт VCL: сотні перевизначуваних методів у базовому класі й сотні нащадків. У прикладному коді майже завжди вистачає virtual.
  • final у методу й sealed у класу забороняють перевизначення й успадкування. Це документує задум і дає компілятору привід для оптимізації.

Як перевірити в продакшені

  • Попередження W1010 варто вважати помилкою збирання: кожне — можливо, загублений override.
  • Перехресні залежності модулів видно в графі uses, який вивантажує компілятор, або в сторонніх аналізаторах.

Підсумок: межа interface/implementation задає залежності й ціну перезбирання, а вибір між virtual, dynamic і reintroduce — явне рішення про наміри й вартість.

Приклад
unit Docs;
interface
type
TDocument = class
strict private
FNumber: string; // видно лише всередині класу
procedure SetNumber(const Value: string);
protected
function GetKind: string; virtual; // слот у VMT
public
property Number: string read FNumber write SetNumber;
property Kind: string read GetKind; // лише читання
end;
TInvoice = class(TDocument)
protected
function GetKind: string; override; // заміщує слот предка
end;
implementation
uses
System.SysUtils; // потрібен лише реалізації
procedure TDocument.SetNumber(const Value: string);
begin
FNumber := Trim(Value);
end;
function TDocument.GetKind: string;
begin
Result := 'Document';
end;
function TInvoice.GetKind: string;
begin
Result := 'Invoice';
end;
end.

Змінна типу TDocument, яка вказує на TInvoice, поверне з Kind значення 'Invoice': властивість читає через віртуальний GetKind.

Middle+

Як у Delphi керують пам’яттю?

Section titled “Як у Delphi керують пам’яттю?”
Відповідь

Що це

Для об’єктів у Delphi немає збирача сміття. Хто створив об’єкт, той його й звільняє.

Як користуватися

  1. Створіть об’єкт до try: List := TStringList.Create;.
  2. Працюйте з ним усередині try.
  3. Звільніть його у finally: List.Free;. Блок finally виконається й у разі винятку.

Що звільняється саме

  • Рядки й динамічні масиви — за лічильником посилань.
  • Об’єкт за інтерфейсним посиланням — коли зникло останнє посилання.
  • Компонент із власником: TButton.Create(Form) звільниться разом із формою.
  • Локальні числа й записи — під час виходу з процедури.

Типова помилка

Викликати Create усередині try. Якщо конструктор упаде, finally викличе Free у неініціалізованої змінної — access violation.

Як влаштовано

  • Free і Destroy. Free перевіряє посилання на nil і викликає віртуальний деструктор Destroy. Сам Destroy напряму не викликають, а в нащадках перевизначають з override і викликають у ньому inherited.
  • FreeAndNil. Звільняє об’єкт і обнуляє змінну. Потрібен, коли посилання живе довше за об’єкт: поле класу або глобальна змінна.
  • Власник. TComponent.Create(AOwner) додає компонент до списку власника, і деструктор власника звільняє весь список. Форма так звільняє свої кнопки й запити.
  • Керовані типи. Для рядків, динамічних масивів, інтерфейсів і варіантів компілятор сам змінює лічильник і додає неявний try..finally. Рядок копіюється лише під час зміни (copy-on-write).

Коли що обирати

Що Delphi .NET
Звільнення об’єкта Free, одразу й детерміновано GC, коли вважатиме за потрібне
Файли, з’єднання деструктор IDisposable і using
Рядки й масиви лічильник посилань GC
Циклічні посилання витік під час підрахунку посилань GC збирає

Пастки

  • Використання після Free. Access violation виникає не одразу, а пізніше: пам’ять уже може займати інший об’єкт.
  • Висячі поля. Компонент, звільнений вручну, сам виходить зі списку власника, але поле форми, яке на нього посилалося, далі вказує на звільнену пам’ять.

Правило

Кожному Create без власника — свій try..finally з Free. У налагоджувальній збірці — ReportMemoryLeaksOnShutdown := True.

Під капотом

  • Динамічну пам’ять — об’єкти, рядки, масиви — виділяє менеджер пам’яті. На Win32 і Win64 це FastMM, вбудований із Delphi 2006 у полегшеній версії; на macOS, Linux, iOS і Android — менеджер POSIX.
  • Для керованих локальних змінних компілятор додає неявний try..finally з фіналізацією наприкінці процедури. Ці інструкції виконуються й без винятків.
  • Лічильник рядків і лічильник TInterfacedObject змінюються атомарно: це потокобезпечно, але не безкоштовно на гарячому шляху.

Історія ARC

Мобільні компілятори (iOS, Android), а з 10.2 Tokyo і компілятор Linux рахували посилання й на об’єкти: Free лише обнуляв посилання, а для негайного виклику деструктора був DisposeOf. У 10.4 Sydney модель уніфікували: об’єкти знову звільняють вручну на всіх платформах, а підрахунок посилань залишився в рядків та інтерфейсів. Мобільний код, написаний з розрахунку на ARC, після переходу починає текти.

Компроміси

  • Детермінізм проти безпеки: немає пауз GC і фіналізаторів, але будь-яка помилка володіння — витік або пошкодження пам’яті.
  • Переведення об’єкта на інтерфейсні посилання знімає ручний Free, але приносить пастки підрахунку посилань і цикли.

Як перевірити в продакшені

  • ReportMemoryLeaksOnShutdown := True у налагодженні показує під час закриття програми, які класи витекли.
  • Повна версія FastMM у налагоджувальному режимі (FullDebugMode у FastMM4) ловить звернення до звільненої пам’яті й пише в лог стек викликів місця виділення.
  • У продакшені зростання пам’яті видно за Private Bytes процесу, а GetMemoryManagerState дає статистику блоків зсередини застосунку.

Підсумок: пам’ять у Delphi передбачувана й дешева, а коректність володіння тримається на дисципліні та FastMM.

Приклад
program MemoryDemo;
{$APPTYPE CONSOLE}
uses
System.Classes;
var
List: TStringList;
Owner, Child: TComponent;
S1, S2: string;
begin
ReportMemoryLeaksOnShutdown := True; // FastMM покаже витоки під час виходу
List := TStringList.Create; // створюємо до try
try
List.Add('order-1');
finally
List.Free; // виконається й у разі винятку
end;
Owner := TComponent.Create(nil);
Child := TComponent.Create(Owner); // Child звільнить власник
Child.Name := 'Child';
Owner.Free; // деструктор Owner звільняє й Child
S1 := StringOfChar('a', 3); // рядок у купі, лічильник посилань = 1
S2 := S1; // копіюється посилання, лічильник = 2
S2[1] := 'x'; // copy-on-write: S2 отримує свою копію
Writeln(S1, ' ', S2); // aaa xaa
end.
Middle+

Як працюють інтерфейси й підрахунок посилань і які в них пастки?

Section titled “Як працюють інтерфейси й підрахунок посилань і які в них пастки?”
Відповідь

Що це

Інтерфейс описує, що вміє об’єкт, але не як: ILogger = interface procedure Log(const Msg: string); end;. Клас перелічує інтерфейси, які реалізує: TFileLogger = class(TInterfacedObject, ILogger).

Навіщо

  • Код залежить від абстракції: у тесті TFileLogger підміняють заглушкою.
  • Час життя об’єкта керується автоматично, без Free.

Як користуватися

  1. Успадкуйте клас від TInterfacedObject — він уже реалізує підрахунок посилань.
  2. Зберігайте об’єкт лише в змінній інтерфейсного типу: Logger := TFileLogger.Create;, де Logger: ILogger.
  3. Не викликайте Free: об’єкт знищиться, коли останнє посилання вийде з області видимості або отримає nil.

Типова помилка

Зберегти об’єкт у змінній класу TFileLogger, передати його як ILogger, а потім викликати Free. Лічильник на цей момент уже знищив об’єкт, і Free звільняє його вдруге.

Як влаштовано

  1. Присвоєння інтерфейсній змінній викликає _AddRef у нового об’єкта і _Release у старого.
  2. Коли змінна виходить з області видимості, компілятор викликає _Release у неявному try..finally.
  3. TInterfacedObject._Release зменшує RefCount і на нулі викликає Destroy.
  4. TComponent._AddRef і _Release повертають -1 і нічого не роблять, якщо компонент не обгортка COM-об’єкта. Часом життя компонента керує власник.

Пастки

  • Змішані посилання. Об’єкт створено в об’єктну змінну (RefCount = 0), присвоєно інтерфейсу й відпущено: лічильник 1 → 0, об’єкт знищено, а об’єктна змінна висить.
  • Цикли. Документ тримає рядки через інтерфейс, рядки тримають документ через IDocument — лічильники не обнуляться, і обидва об’єкти витечуть.
  • Немає GUID. Без ['{...}'] в оголошенні не працюють as, Supports і QueryInterface: компілятор видає помилку E2232.
  • Інтерфейс на компонент. Інтерфейсне посилання на форму не подовжує життя форми: після її звільнення посилання висить.

Коли що обирати

Задача Рішення
Об’єкт живе, поки на нього посилаються нащадок TInterfacedObject, лише інтерфейсні змінні
Об’єкт живе за власником або до Free TComponent або TNoRefCountObject
Зворотне посилання від дитини до батька [weak] або [unsafe]

Правило

Одна модель володіння на об’єкт. Перевірка інтерфейсу — Supports(Intf, IFoo, Foo) з вихідним параметром, а не жорстке приведення.

Під капотом

  • Інтерфейсна змінна вказує на таблицю методів інтерфейсу всередині об’єкта. IInterface — той самий COM-івський IUnknown, тому інтерфейси Delphi підходять для COM без обгорток.
  • TInterfacedObject змінює лічильник атомарно: посилання можна передавати між потоками, але кожне присвоєння коштує interlocked-інструкції.
  • [weak] (на Win32 і Win64 — із 10.1 Berlin) не збільшує лічильник, а рантайм обнуляє такі посилання під час знищення об’єкта. Для цього він веде облік слабких посилань — це пам’ять і синхронізація.
  • [unsafe] — посилання без підрахунку й без обнулення, по суті вказівник.

Ціна

  • const-параметр інтерфейсного типу не викликає _AddRef. Якщо передати в нього TFoo.Create просто у виклику, лічильник може так і не піднятися, і об’єкт витече. Звіт про це Embarcadero закрила як поведінку «by design».
  • Supports(Obj, IFoo) для об’єкта з лічильником 0 піднімає його до 1 й опускає до 0 — об’єкт знищується. Документація прямо про це попереджає.
  • Анонімний метод — теж об’єкт із лічильником посилань. Якщо він захопив інтерфейсне посилання на об’єкт, у полі якого сам зберігається, виходить цикл.

Компроміси

Зворотне посилання Плюс Мінус
[weak] обнулиться саме облік у рантаймі
[unsafe] безкоштовно висить після знищення об’єкта
посилання на об’єкт, а не інтерфейс просто теж висить, потрібна ручна дисципліна

Як перевірити в продакшені

  • Цикли видно у звіті ReportMemoryLeaksOnShutdown: витікають пари об’єктів, які посилаються один на одного.
  • FastMM у налагоджувальному режимі ловить виклик методу у звільненого об’єкта — типовий слід змішаних посилань.
  • На рев’ю шукають Free у нащадків TInterfacedObject і Supports від об’єктних змінних.

Підсумок: підрахунок посилань дешевий і детермінований, але не бачить циклів та об’єктних посилань, тому володіння проєктують явно.

Приклад
program InterfaceDemo;
{$APPTYPE CONSOLE}
type
ILogger = interface
['{6A1E2B7C-3D4F-4A5B-9C8D-1E2F3A4B5C6D}'] // GUID потрібен для as і Supports
procedure Log(const Msg: string);
end;
TConsoleLogger = class(TInterfacedObject, ILogger)
public
procedure Log(const Msg: string);
destructor Destroy; override;
end;
procedure TConsoleLogger.Log(const Msg: string);
begin
Writeln(Msg);
end;
destructor TConsoleLogger.Destroy;
begin
Writeln('destroyed');
inherited;
end;
procedure Run;
var
Obj: TConsoleLogger;
Logger: ILogger;
begin
Obj := TConsoleLogger.Create; // RefCount = 0
Logger := Obj; // _AddRef: RefCount = 1
Logger := nil; // _Release: 0 → destroyed
// Obj тепер висяче посилання: Obj.Free звільнив би пам'ять удруге
Logger := TConsoleLogger.Create; // правильно: лише інтерфейсне посилання
Logger.Log('ok'); // ok
end; // Logger виходить з області видимості: destroyed
begin
Run;
end.
Senior+

Які можливості з’явилися в сучасному Delphi і як їх застосовувати в старому коді?

Section titled “Які можливості з’явилися в сучасному Delphi і як їх застосовувати в старому коді?”
Відповідь

Що це

Можливість Версія Навіщо
generics Delphi 2009 типобезпечні колекції
анонімні методи Delphi 2009 код як параметр
атрибути й розширений RTTI Delphi 2010 метадані для серіалізації й тестів
inline-змінні 10.3 Rio var I := 0; просто в коді
managed records 10.4 Sydney автоматичні Initialize і Finalize
умовний оператор 13 Florence X := if A > B then A else B;

Як користуватися

  • Замість TList з приведенням TOrder(List[I]) — TObjectList<TOrder>, який сам звільняє елементи.
  • Замість окремого методу заради одного виклику — анонімний метод, наприклад функція порівняння для сортування.
  • Замість блоку var на тридцять рядків — змінна на місці: for var Item in Orders do.

Типова помилка

Додати новий синтаксис у модуль, який ще збирають старим компілятором. Delphi 7 не знає ні generics, ні inline-змінних.

Як влаштовано

  • Generics. Компілятор створює код для кожного набору аргументів типу. Обмеження: class, record, constructor, інтерфейс або базовий клас; Delphi 13 додав interface і unmanaged.
  • Анонімні методи. Їхній тип — reference to procedure або reference to function, і це керований тип із лічильником посилань. Захоплені локальні змінні переїжджають зі стеку в купу. Захоплюється змінна, а не її значення.
  • Inline-змінні. Видимі від місця оголошення до кінця свого блоку begin..end, тип виводиться з ініціалізатора.
  • Managed records. class operator Initialize і Finalize викликаються, коли запис з’являється й зникає, Assign — під час присвоєння :=.

Коли що обирати

Старий прийом Заміна
TList і приведення TOrder(List[I]) TObjectList<TOrder>
метод форми заради одного обробника анонімний метод
запис з Init і Done, які забувають викликати managed record

Пастки

  • Захоплення в циклі. Усі анонімні методи, створені в циклі, бачать одну змінну і її значення на момент виклику, а не створення. Значення фіксують, створюючи замикання в допоміжній функції з параметром.
  • Події. Анонімний метод не можна присвоїти події на кшталт OnClick: подія — procedure of object, а не reference to.

Правило

Новий синтаксис — у нових модулях і під час переписування функції, а не масовою заміною.

Під капотом

  • Анонімний метод компілюється в метод прихованого об’єкта-фрейму. Фрейм зберігає захоплені змінні, рахує посилання й живе, поки живе хоч одне посилання на замикання.
  • Розширений RTTI (System.Rtti, TRttiContext) описує поля, методи й властивості та зберігає атрибути. На ньому побудовані JSON-серіалізація RTL і DUnitX.
  • Generic-код конкретизується на кожен набір аргументів: без boxing, але зі зростанням exe й часу збирання.

Версії 12 і 13

  • 12 Athens (листопад 2023): рядкові літерали довші за 255 символів і багаторядкові рядки в потрійних лапках '''; порівняння з NaN за IEEE.
  • 13 Florence (вересень 2025): умовний оператор if ... then ... else у виразах; is not і not in; NameOf, як у C#; директива noreturn; {$PUSHOPT} і {$POPOPT}; повноцінна 64-бітна IDE.
  • 13.1 (березень 2026): нова цільова платформа Windows on Arm (Arm64EC).

Ціна

  • Кожна нова конструкція піднімає мінімальну версію компілятора. Модуль, спільний для старої й нової гілки, залишається на старому синтаксисі або закриває нове через {$IF CompilerVersion >= ...}.
  • Замикання подовжують життя захоплених змінних і можуть утворити цикл зі своїм власником.

Компроміси

  • Переписувати робочий код заради синтаксису — ризик без вигоди для бізнесу. Нове застосовують у новому коді й у функціях, які все одно змінюють.
  • Managed record і атрибути роблять код коротшим, але ховають логіку: виклики не видно в коді, а помилки атрибутів спливають лише в рантаймі.

Як перевірити в продакшені

  • Витоки замикань видно у звіті ReportMemoryLeaksOnShutdown як незвільнені об’єкти-фрейми.
  • Unicode, 64 біти й стратегія переходу зі старих версій — у темі Міграція Delphi 6 → 12/13.

Підсумок: сучасний Object Pascal близький до C#, але в legacy його вводять на межах, пам’ятаючи про мінімальну версію компілятора й семантику замикань.

Приклад
program ModernDemo;
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.Generics.Collections;
procedure Run;
var
Total: Integer;
AddTo: TProc<Integer>;
begin
var Stock := TDictionary<string, Integer>.Create; // тип виведено (10.3+)
try
Stock.Add('milk', 3);
Stock.Add('bread', 2);
Total := 0;
AddTo := procedure(Value: Integer)
begin
Total := Total + Value; // захоплено змінну Total, а не її значення
end;
for var Pair in Stock do // Pair: TPair<string, Integer>, видимий лише в циклі
AddTo(Pair.Value);
Writeln(Total); // 5
Writeln(if Total > 4 then 'big order' else 'small order'); // big order (Delphi 13+)
finally
Stock.Free; // generic-колекція — звичайний об'єкт
end;
end;
begin
Run;
end.
Managed record (10.4+)
type
TScopeLog = record
Name: string;
class operator Initialize(out Dest: TScopeLog);
class operator Finalize(var Dest: TScopeLog);
end;
class operator TScopeLog.Initialize(out Dest: TScopeLog);
begin
Dest.Name := 'scope';
Writeln('enter');
end;
class operator TScopeLog.Finalize(var Dest: TScopeLog);
begin
Writeln('leave ', Dest.Name);
end;
procedure PostDocument;
var
Log: TScopeLog; // Initialize: enter
begin
Log.Name := 'post';
Writeln('posting'); // posting
end; // Finalize, навіть у разі винятку: leave post

У Delphi 13 параметр можна не писати: class operator Initialize; і class operator Finalize; працюють із неявним Self.