Есть много разных вариантов как можно использовать конструкцию try...finally
для освобождения ресурсов. Многие из них работают неверно в особых ситуациях. Рассмотрим несколько вариантов подробнее.
Все рассматриваемые случаи относятся к коду внутри методов, когда переменные объектов являются локальными переменными метода. Для примера рассматривается выделение о освобождение памяти для объектов, но тоже самое может быть применено к другим типам ресурсов.
Прежде всего, установим ReportMemoryLeaksOnShutdown := True
в dpr
файле, для того чтобы отслеживать утечки памяти.
1 2 3 4 5 6 7 8 9 10 |
program Project1; uses Vcl.Forms, Unit1 in 'Unit1.pas' {Form1}; {$R *.res} begin ReportMemoryLeaksOnShutdown := True; |
Создание одного объекта
Правильная последовательность такая
- Сначала создание объекта и присвоение его переменной
- Работа с объектом в блоке
try
- Освобождение объекта в блоке
finally
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
type TMyClass = class procedure Start; constructor Create; end; { TMyClass } procedure TMyClass.Start; begin end; constructor TMyClass.Create; begin end; procedure TForm1.Button1Click(Sender: TObject); var Obj: TMyClass; begin Obj := TMyClass.Create; try Obj.Start; finally FreeAndNil(Obj); end; end; |
Принципиально важный момент: блок try
начинается сразу после создания объекта. Никаких дополнительных действий между ними нет. Рассмотрим ситуацию когда есть какие-то промежуточные действия, внутри них может возникнуть исключение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure OtherAction; begin raise Exception.Create('Big error'); end; procedure TForm1.Button1Click(Sender: TObject); var Obj: TMyClass; begin Obj := TMyClass.Create; OtherAction; // Исключение, процедура дальше не выполняется, объект Obj не уничтожается. try Obj.Start; finally FreeAndNil(Obj); end; end; |
Проверяется утечка довольно просто, запустить приложение, нажать кнопку — появится сообщение об ошибке, закрыть приложение и появится сообщение об утечке памяти. Поэтому между создание объекта и try
не должно быть никаких действий.
Рассмотрим ещё одну ситуацию — объект создаётся внутри блока try
.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.Button1Click(Sender: TObject); var Obj: TMyClass; begin try Obj := TMyClass.Create; Obj.Start; finally FreeAndNil(Obj); end; end; |
В обычной ситуации, всё нормально. Но, предположим, что в конструкторе объекта возникает исключение.
1 2 3 4 |
constructor TMyClass.Create; begin raise Exception.Create('Big error'); end; |
При возникновении исключения в конструкторе автоматически вызывается деструктор (подробнее описано в этой статье) а затем управление переходит в блок finally
. Важный момент: присвоения не происходит, значение в Obj
не меняется. Так как локальные переменные не инициализируются по умолчанию, то в Obj
находится случайна ссылка. В блоке finally
вызывается FreeAndNil
в ходе которого вызывается деструктор. Таким образом освобождается память по случайному адресу, что приводит к непредсказуемым последствиям и трудноуловимым ошибкам.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.Button1Click(Sender: TObject); var Obj: TMyClass; // Obj не инициализируются и содержит случайны адрес begin try Obj := TMyClass.Create; // Исключение, значение obj не меняется, управление передаётся в finally Obj.Start; finally FreeAndNil(Obj); // попытка освобождения памяти по случайному адресу end; end; |
Поэтому никогда нельзя создавать объект внутри try если переменная объекта не инициализирована. Можно дополнительно инициализировать переменную:
1 2 3 4 5 6 7 8 9 10 11 12 |
procedure TForm1.Button1Click(Sender: TObject); var Obj: TMyClass; begin Obj := nil; try Obj := TMyClass.Create; Obj.Start; finally FreeAndNil(Obj); end; end; |
Но это занимает одну лишнюю строку, не имеет дополнительного смысла. Таким образом правилен только первый описанный вариант.
Создание нескольких объектов
Ситуация когда создаётся несколько объектов немного сложнее. Рассмотрим создание двух объектов. Самый простой способ — использовать вложенные try
, с учётом все описанных выше особенностей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
procedure TForm1.Button1Click(Sender: TObject); var Obj1: TMyClass1; Obj2: TMyClass2; begin Obj1 := TMyClass1.Create; try Obj2 := TMyClass2.Create; try Obj1.Start; Obj2.Start; finally FreeAndNil(Obj2); end; finally FreeAndNil(Obj1); end; end; |
Такой способ имеет существенный недостаток — постоянно увеличивающийся уровень вложенности, особенно если нужно создать больше 2х объектов.
Попробуем убрать вложенность
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm1.Button1Click(Sender: TObject); var Obj1: TMyClass1; Obj2: TMyClass2; begin Obj1 := TMyClass1.Create; Obj2 := TMyClass2.Create; try Obj1.Start; Obj2.Start; finally FreeAndNil(Obj1); FreeAndNil(Obj2); end; end; |
В обычной ситуации всё работает правильно. Но если исключение возникает в конструкторе второго объекта, то снова получаем утечку памяти.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
constructor TMyClass2.Create; begin raise Exception.Create('Big error'); end; procedure TForm1.Button1Click(Sender: TObject); var Obj1: TMyClass1; Obj2: TMyClass2; begin Obj1 := TMyClass1.Create; // Создаётся объект Obj2 := TMyClass2.Create; // Исключение, метод дальше не выполняется try Obj1.Start; Obj2.Start; finally FreeAndNil(Obj1); FreeAndNil(Obj2); end; end; |
Рассмотрим примерно такую же ситуацию, но когда создание двух объектов в блоке try
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
constructor TMyClass2.Create; begin raise Exception.Create('Big error'); end; procedure TForm1.Button1Click(Sender: TObject); var Obj1: TMyClass1; Obj2: TMyClass2; begin try Obj1 := TMyClass1.Create; Obj2 := TMyClass2.Create; Obj1.Start; Obj2.Start; finally FreeAndNil(Obj1); FreeAndNil(Obj2); end; end; |
Ситуация полностью совпадает с той, что происходит когда один объект создаётся в блоке try
— в случае исключения в конструкторе любого из объектов происходит освобождение памяти по случайному адресу.
В итоге правильный вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
procedure TForm1.Button1Click(Sender: TObject); var Obj1: TMyClass1; Obj2: TMyClass2; begin Obj1 := nil; Obj2 := nil; try Obj1 := TMyClass1.Create; Obj2 := TMyClass2.Create; Obj1.Start; Obj2.Start; finally FreeAndNil(Obj1); FreeAndNil(Obj2); end; end; |
Сначала происходит инициализация переменных: потом их создание в защищённом блоке. При такой последовательности действий, при любых ситуациях, ресурсы будут освобождены.
Ссылки:
- Статья Заповеди молодого разработчика Delphi
- Описание ReportMemoryLeaksOnShutdown
а как же 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. FreeAndNil (а точнее obj.Free) сама проверит объект на nill, поэтому проверка «if assigned(obj1) then» будет лишней.
2. В вашем примере, как и в последнем «правильном» примере из статьи возможен obj2 не будет освобожден если в деструкторе (или в BeforeDestroy) произойдет исключение …
если уж писать правильно то тогда
…
finally
try
FreeAndNil(Obj1);
finally
FreeAndNil(Obj2);
end;
end;
последний «правильный» вариант не освободит obj2 если в деструкторе (или BeforeDestroy) obj1 произойдет исключение
если уж писать правильно то
…
finally
try
FreeAndNil(Obj2);
finally
FreeAndNil(Obj1);
end;
end;
PS я еще поменял освобождение obj1 и obj2 местами, это может упростить дефрагментацию памяти
Спасибо, полезное замечание.