Перейти к содержимому

Object Pascal

Как устроен язык Delphi: модули и классы, ручное управление памятью, интерфейсы с подсчётом ссылок и возможности, которые появились после Delphi 7. На собеседовании часто просят сравнить это с .NET, поэтому сравнения с C# здесь на каждом шаге.

Перед этой темой: Типы и память.

Junior+
Ответ

Что это

Проект (.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 нет сборщика мусора. Кто создал объект, тот его и освобождает.

Как пользоваться

  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+

Как работают интерфейсы и подсчёт ссылок и какие у них ловушки?

Заголовок раздела «Как работают интерфейсы и подсчёт ссылок и какие у них ловушки?»
Ответ

Что это

Интерфейс описывает, что умеет объект, но не как: 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 и как их применять в старом коде?

Заголовок раздела «Какие возможности появились в современном 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.