Object Pascal
Как устроен язык Delphi: модули и классы, ручное управление памятью, интерфейсы с подсчётом ссылок и возможности, которые появились после Delphi 7. На собеседовании часто просят сравнить это с .NET, поэтому сравнения с C# здесь на каждом шаге.
Перед этой темой: Типы и память.
Из чего состоит программа на Object Pascal?
Заголовок раздела «Из чего состоит программа на 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 он виден всему модулю; аналог C#-ногоprivate—strict private. - Когда выполняются
initializationиfinalization? — При старте программы в порядкеusesи при завершении в обратном порядке. - Как разорвать циклическую ссылку модулей? — Перенести одну из ссылок в
usesсекцииimplementationили вынести общее в третий модуль. - Чем
virtualотличается отdynamic? — Семантика одна;virtualбыстрее (VMT),dynamicэкономит память (DMT). - Зачем нужен
reintroduce? — Явно скрыть виртуальный метод предка и подавить W1010, например у конструктора с другими параметрами.
Как в Delphi управляют памятью?
Заголовок раздела «Как в 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? — Его убрали для объектов на всех платформах; счётчик остался у строк, массивов и интерфейсов.
Как работают интерфейсы и подсчёт ссылок и какие у них ловушки?
Заголовок раздела «Как работают интерфейсы и подсчёт ссылок и какие у них ловушки?»Что это
Интерфейс описывает, что умеет объект, но не как: 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 и как их применять в старом коде?
Заголовок раздела «Какие возможности появились в современном 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 >= ...}.