Object Pascal
Як влаштована мова Delphi: модулі й класи, ручне керування пам’яттю, інтерфейси з підрахунком посилань і можливості, що з’явилися після Delphi 7. На співбесіді часто просять порівняти це з .NET, тому порівняння з C# тут на кожному кроці.
Перед цією темою: Типи й пам’ять.
Із чого складається програма на Object Pascal?
Section titled “Із чого складається програма на Object Pascal?”Що це
Проєкт (.dpr) — головна програма, модуль (.pas) — файл із типами й підпрограмами, поділений на секції.
| Секція | Що в ній |
|---|---|
interface |
оголошення, видимі іншим модулям |
implementation |
код методів і приховані деталі |
initialization, finalization |
код під час запуску й під час завершення |
Як користуватися
- Підключіть модуль в
uses— стануть доступні оголошення з йогоinterface. - Оголосіть клас із полями, методами й властивостями. Властивість пов’язує ім’я з полем або методом:
property Number: string read FNumber write SetNumber;. - Розподіліть члени за видимістю:
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.
- Чим
privateу Delphi відрізняється відprivateу C#? — У Delphi його видно всьому модулю; аналогprivateіз C# —strict private. - Коли виконуються
initializationіfinalization? — Під час запуску програми в порядкуusesі під час завершення у зворотному порядку. - Як розірвати циклічне посилання модулів? — Перенести одне з посилань в
usesсекціїimplementationабо винести спільне в третій модуль. - Чим
virtualвідрізняється відdynamic? — Семантика одна;virtualшвидший (VMT),dynamicзаощаджує пам’ять (DMT). - Навіщо потрібен
reintroduce? — Явно приховати віртуальний метод предка й придушити W1010, наприклад у конструктора з іншими параметрами.
Як у Delphi керують пам’яттю?
Section titled “Як у Delphi керують пам’яттю?”Що це
Для об’єктів у Delphi немає збирача сміття. Хто створив об’єкт, той його й звільняє.
Як користуватися
- Створіть об’єкт до
try:List := TStringList.Create;. - Працюйте з ним усередині
try. - Звільніть його у
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 xaaend.- Чому
Createпишуть доtry? — Якщо конструктор упаде,finallyне має звільняти неініціалізовану змінну. - Що буде, якщо виняток виникне в конструкторі? — Delphi сам викличе деструктор недобудованого об’єкта, тому
Destroyмає витримувати неініціалізовані поля. - Коли потрібен
FreeAndNil? — Коли посилання переживає об’єкт і його потім перевіряють наnil: поле або глобальна змінна. - Де лежить запис? — Там само, де змінна: локальний — на стеку, поле об’єкта — у купі всередині об’єкта.
- Що сталося з ARC у 10.4? — Його прибрали для об’єктів на всіх платформах; лічильник залишився в рядків, масивів та інтерфейсів.
Як працюють інтерфейси й підрахунок посилань і які в них пастки?
Section titled “Як працюють інтерфейси й підрахунок посилань і які в них пастки?”Що це
Інтерфейс описує, що вміє об’єкт, але не як: ILogger = interface procedure Log(const Msg: string); end;. Клас перелічує інтерфейси, які реалізує: TFileLogger = class(TInterfacedObject, ILogger).
Навіщо
- Код залежить від абстракції: у тесті
TFileLoggerпідміняють заглушкою. - Час життя об’єкта керується автоматично, без
Free.
Як користуватися
- Успадкуйте клас від
TInterfacedObject— він уже реалізує підрахунок посилань. - Зберігайте об’єкт лише в змінній інтерфейсного типу:
Logger := TFileLogger.Create;, деLogger: ILogger. - Не викликайте
Free: об’єкт знищиться, коли останнє посилання вийде з області видимості або отримаєnil.
Типова помилка
Зберегти об’єкт у змінній класу TFileLogger, передати його як ILogger, а потім викликати Free. Лічильник на цей момент уже знищив об’єкт, і Free звільняє його вдруге.
Як влаштовано
- Присвоєння інтерфейсній змінній викликає
_AddRefу нового об’єкта і_Releaseу старого. - Коли змінна виходить з області видимості, компілятор викликає
_Releaseу неявномуtry..finally. TInterfacedObject._ReleaseзменшуєRefCountі на нулі викликаєDestroy.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'); // okend; // Logger виходить з області видимості: destroyed
begin Run;end.- Чим
IInterfaceвідрізняється відIUnknown? — Це той самий інтерфейс;IInterface— платформно-нейтральна назва. - Навіщо інтерфейсу GUID? — Для
QueryInterface: без нього не працюютьasіSupports(E2232). - Чому об’єкт знищився під час виклику
Supports? — Його лічильник був 0:Supportsпідняв його до 1 і відпустив до 0. - Чим
[weak]відрізняється від[unsafe]? —[weak]обнуляється під час знищення об’єкта й потребує обліку;[unsafe]— голий вказівник. - Чи рахує посилання
TComponent? — Ні, його_AddRefі_Releaseповертають -1; часом життя керує власник.
Які можливості з’явилися в сучасному 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.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: enterbegin Log.Name := 'post'; Writeln('posting'); // postingend; // Finalize, навіть у разі винятку: leave postУ Delphi 13 параметр можна не писати: class operator Initialize; і class operator Finalize; працюють із неявним Self.
- Що захоплює анонімний метод — значення чи змінну? — Змінну; вона переїжджає в купу й живе, поки живе замикання.
- Чи можна присвоїти анонімний метод події
OnClick? — Ні: подія —procedure of object, а анонімний метод —reference to procedure. - Чим managed record відрізняється від звичайного запису з рядковим полем? — Звичайний теж фіналізує компілятор, але власний код під час створення, копіювання й знищення є лише в managed record.
- Як атрибути використовують у тестах? — DUnitX знаходить тести за
[TestFixture]і[Test]через RTTI. - Як писати код, що збирається і в 10.x, і в 13? — Не використовувати новий синтаксис або закривати його
{$IF CompilerVersion >= ...}.