Правильная обработка освобождения ресурсов через try…finally в Delphi

Есть много разных вариантов как можно использовать конструкцию try...finally для освобождения ресурсов. Но многие из них работают неверно в особых ситуациях. Рассмотрим несколько вариантов подробнее.

Все рассматриваемые случаи относятся к коду внутри методов, когда переменные объектов являются локальными переменными метода. Для примера рассматривается выделение о освобождение памяти для объектов, но тоже самое может быть применено к другим типам ресурсов.

Прежде всего, для того чтобы отслеживать утечки памяти, установим ReportMemoryLeaksOnShutdown в dpr файле.

Создание одного объекта

Правильная последовательность такая

  • Сначала создание объекта и присвоение его переменной
  • Работа с объектом в блоке try
  • Освобождение объекта в блоке finally

Принципиально важный момент: блок try начинается сразу после создания объекта. Никаких дополнительных действий между ними нет. Рассмотрим ситуацию когда есть какие-то промежуточные действия, внутри них может возникнуть исключение.

Проверяется утечка довольно просто, запустить приложение, нажать кнопку — появится сообщение об ошибке, закрыть приложение и появится сообщение об утечке памяти. Поэтому между создание объекта и try не должно быть никаких действий.

Рассмотрим ещё одну ситуацию — объект создаётся внутри блока try.

В обычной ситуации, всё нормально. Но, предположим, что в конструкторе объекта возникает исключение.

При возникновении исключения в конструкторе автоматически вызывается деструктор (подробнее описано в этой статье) а затем управление переходит в блок finally. Важный момент: присвоения не происходит, значение в Obj не меняется. Так как локальные переменные не инициализируются по умолчанию, то в Obj находится случайна ссылка. В блоке finally вызывается FreeAndNil в ходе которого вызывается деструктор. Таким образом освобождается память по случайному адресу, что приводит к непредсказуемым последствиям и трудноуловимым ошибкам.

Поэтому никогда нельзя создавать объект внутри try если переменная объекта не инициализирована. Можно дополнительно инициализировать переменную:

Но это занимает одну лишнюю строку, не имеет дополнительного смысла. Таким образом правилен только первый описанный вариант.

Создание нескольких объектов

Ситуация когда создаётся несколько объектов немного сложнее. Рассмотрим создание двух объектов. Самый простой способ — использовать вложенные try, с учётом все описанных выше особенностей:

Такой способ имеет существенный недостаток — постоянно увеличивающийся уровень вложенности, особенно если нужно создать больше 2х объектов.

Попробуем убрать вложенность

В обычной ситуации всё работает правильно. Но если исключение возникает в конструкторе второго объекта, то снова получаем утечку памяти.

Рассмотрим примерно такую же ситуацию, но когда создание двух объектов в блоке try.

Ситуация полностью совпадает с той, что происходит когда один объект создаётся в блоке try — в случае исключения в конструкторе любого из объектов происходит освобождение памяти по случайному адресу.

В итоге правильный вариант:

Сначала происходит инициализация переменных: потом их создание в защищённом блоке. При такой последовательности действий, при любых ситуациях, ресурсы будут освобождены.

Ссылки:

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

4 thoughts on “Правильная обработка освобождения ресурсов через try…finally в Delphi”

  1. а как же try except?
    obj1: tmyclass1;
    obj2: tmyclass2;
    begin
    obj1 := nil;
    obj2 := nil;
    try
    obj1…create;
    obj2…create;
    except
    showmessage(ошибка);
    end;
    if assigned(obj1) then
    freeandnil(obj1);
    if assigned(obj2) then
    freeandnil(obj2);
    end;

    1. 1. FreeAndNil (а точнее obj.Free) сама проверит объект на nill, поэтому проверка «if assigned(obj1) then» будет лишней.

      2. В вашем примере, как и в последнем «правильном» примере из статьи возможен obj2 не будет освобожден если в деструкторе (или в BeforeDestroy) произойдет исключение …
      если уж писать правильно то тогда

      finally
      try
      FreeAndNil(Obj1);
      finally
      FreeAndNil(Obj2);
      end;
      end;

  2. последний «правильный» вариант не освободит obj2 если в деструкторе (или BeforeDestroy) obj1 произойдет исключение
    если уж писать правильно то

    finally
    try
    FreeAndNil(Obj2);
    finally
    FreeAndNil(Obj1);
    end;
    end;

    PS я еще поменял освобождение obj1 и obj2 местами, это может упростить дефрагментацию памяти

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *