Операторы присваивания

Миф о простоте: почему := не всегда копирует значение
Многие разработчики, приходящие в Delphi из языков с простой семантикой, свято верят, что оператор присваивания := всегда создаёт независимую копию объекта. Это первая и самая дорогая ошибка. На практике, когда вы пишете MyObj2 := MyObj1 для экземпляра класса, вы не получаете второй экземпляр — вы получаете второй указатель (ссылку). Профессионал отличает ситуацию: если тип — запись (record) или простой тип, то присваивание действительно копирует данные. Если объект — класс (class), то присваивание копирует только ссылку. Не учитывая этот нюанс, вы рискуете модифицировать исходный объект через новую переменную, что часто приводит к трудноуловимым багам в многокомпонентных формах.
Неочевидное поведение с Variant и динамическими массивами
Ещё один камень преткновения — работа с Variant и динамическими массивами. Специалисты обращают внимание: присваивание Variant на самом деле выполняет глубокое копирование сложных типов (строки, интерфейсы), но для массивов внутри Variant работает подсчёт ссылок. То есть V2 := V1 для массива в Variant не копирует элементы — только увеличивает счётчик. Изменение данных через V1 неожиданно отразится на V2. Рекомендую всегда явно вызывать VarCopy или ручное клонирование, если вам нужна полная независимость. С динамическими массивами похожая история: Arr2 := Arr1 не копирует данные, а делит одну область памяти до первого изменения длины (copy-on-write). Однако, как только вы изменяете длину Arr2, массив становится независимым. Эксперт должен помнить: присваивание элемента массива (например, Arr2[0] := 5) не приводит к разделению, пока вы не измените длину.
Профессиональный трюк: избегайте лишних копирований record
Многие программисты не задумываются, что присваивание большой записи (record с многими полями) — затратная операция, особенно в цикле. Совет профи: вместо того чтобы писать BigRec := ComputeBigRec(...) внутри каждого шага, передавайте запись по ссылке (var) в функцию и модифицируйте её прямо на месте. Это не только ускоряет код, но и уменьшает нагрузку на стек. Однако будьте осторожны: запись, содержащая поля с управляемыми типами (строки, интерфейсы), при присваивании всё равно делает глубокое копирование этих полей — это неизбежная плата за безопасность. Если вам критична производительность, используйте записи с array of Byte или ручное управление памятью через Move.
Как не попасться на удочку с перегруженными операторами
Delphi позволяет перегружать оператор присваивания через operator Assign для записей и классов (с некоторыми ограничениями). Здесь кроется двойная ловушка. Во-первых, при перегрузке нельзя вернуть значение — оператор всегда присваивает. Во-вторых, многие разработчики забывают, что перегруженный оператор вызывается и при копировании полей внутри других операций (например, при передаче параметров). Это может привести к неожиданным эффектам вроде двойного вызова деструктора или утечки памяти, если внутри Assign вы не обработали старый экземпляр. Профессиональный совет: всегда реализовывайте Assign в паре с деструктором и проверяйте на самоприсваивание (Self <> Source). Это спасёт вас от крашей, когда переменная присваивается сама себе.
Оптимизация присваивания для строк и интерфейсов: нюанс подсчёта ссылок
Ошибка новичка: считать, что каждое присваивание строки (S2 := S1) копирует буфер. На самом деле, строки в Delphi — это ссылочные типы с автоматическим подсчётом ссылок. Присваивание лишь увеличивает счётчик, и только когда одна из строк изменяется (начинает писать в память), происходит копирование. Однако существует миф, что это абсолютно бесплатно. Эксперты знают: если вы в цикле много раз присваиваете одну и ту же строку разным переменным, счётчик ссылок бегает вверх-вниз — это добавляет оверхед из-за атомарных операций в многопоточной среде. Для интерфейсов ситуация аналогична, но критичнее: из-за неправильного использования IUnknown может возникнуть циклическая ссылка, которую сборщик не разрушит. Рекомендую для интерфейсов использовать weak атрибут (Delphi 10.3+), чтобы разорвать циклы.
Присваивание в процедурах и функциях: скрытые параметры
Мало кто обращает внимание, что присваивание результата функции (Result := something) — это тоже оператор присваивания, но с особенностями. Если функция возвращает тип record, а внутри вы используете Result, компилятор может сгенерировать лишнее копирование, если не включена оптимизация. Профессионалы советуют: для тяжёлых типов объявляйте результат как var-параметр, передавая его из вызывающего кода. Это полностью исключает операцию копирования при возврате. Другой совет: не полагайтесь на порядок присваивания в выражениях — Delphi гарантирует, что левая часть вычисляется до правой, но при работе с указателями и индексами, которые могут измениться из-за побочных эффектов, лучше разделить присваивание на отдельные строки. Например, MyArray[GetIndex] := GetValue может вызвать функцию GetIndex, которая изменит массив — это классический баг в сложных алгоритмах.
Заключение: что проверяет эксперт в коде на присваивание
Опытный Delphi-разработчик всегда смотрит на три вещи: 1) является ли тип ссылочным (class, string, interface) — если да, то присваивание не копирует данные; 2) не происходит ли присваивание объекта самому себе (это особенно критично в перегруженных операторах); 3) не вызывается ли оператор присваивание в узком цикле, что может быть узким местом. Избегайте копирования записей через := там, где можно передать указатель или var-параметр. И помните: среда Delphi — это не только простота, но и ответственность за понимание моделей памяти.
Добавлено: 27.04.2026
