Правильная обработка освобождения ресурсов через try...finally в Delphi
Есть много разных вариантов как можно использовать конструкцию try...finally
для освобождения ресурсов. Многие из них работают неверно в особых ситуациях. Рассмотрим несколько вариантов подробнее.
Все рассматриваемые случаи относятся к коду внутри методов, когда переменные объектов являются локальными переменными метода. Для примера рассматривается выделение о освобождение памяти для объектов, но тоже самое может быть применено к другим типам ресурсов.
Прежде всего, установим ReportMemoryLeaksOnShutdown := True
в dpr
файле, для того чтобы отслеживать утечки памяти.
program Project1;
uses
Vcl.Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
ReportMemoryLeaksOnShutdown := True;
2
3
4
5
6
7
8
9
10
Создание одного объекта
Правильная последовательность такая
- Сначала создание объекта и присвоение его переменной
- Работа с объектом в блоке
try
- Освобождение объекта в блоке
finally
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;
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
Принципиально важный момент: блок try
начинается сразу после создания объекта. Никаких дополнительных действий между ними нет. Рассмотрим ситуацию когда есть какие-то промежуточные действия, внутри них может возникнуть исключение.
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Проверяется утечка довольно просто, запустить приложение, нажать кнопку — появится сообщение об ошибке, закрыть приложение и появится сообщение об утечке памяти. Поэтому между создание объекта и try
не должно быть никаких действий.
Рассмотрим ещё одну ситуацию — объект создаётся внутри блока try
.
procedure TForm1.Button1Click(Sender: TObject);
var
Obj: TMyClass;
begin
try
Obj := TMyClass.Create;
Obj.Start;
finally
FreeAndNil(Obj);
end;
end;
2
3
4
5
6
7
8
9
10
11
В обычной ситуации, всё нормально. Но, предположим, что в конструкторе объекта возникает исключение.
constructor TMyClass.Create;
begin
raise Exception.Create('Big error');
end;
2
3
4
При возникновении исключения в конструкторе автоматически вызывается деструктор (подробнее описано в этой статьеopen in new window) а затем управление переходит в блок finally
. Важный момент: присвоения не происходит, значение в Obj
не меняется. Так как локальные переменные не инициализируются по умолчаниюopen in new window, то в Obj
находится случайна ссылка. В блоке finally
вызывается FreeAndNil
в ходе которого вызывается деструктор. Таким образом освобождается память по случайному адресу, что приводит к непредсказуемым последствиям и трудноуловимым ошибкам.
procedure TForm1.Button1Click(Sender: TObject);
var
Obj: TMyClass; // Obj не инициализируются и содержит случайны адрес
begin
try
Obj := TMyClass.Create; // Исключение, значение obj не меняется, управление передаётся в finally
Obj.Start;
finally
FreeAndNil(Obj); // попытка освобождения памяти по случайному адресу
end;
end;
2
3
4
5
6
7
8
9
10
11
Поэтому никогда нельзя создавать объект внутри try если переменная объекта не инициализирована. Можно дополнительно инициализировать переменную:
procedure TForm1.Button1Click(Sender: TObject);
var
Obj: TMyClass;
begin
Obj := nil;
try
Obj := TMyClass.Create;
Obj.Start;
finally
FreeAndNil(Obj);
end;
end;
2
3
4
5
6
7
8
9
10
11
12
Но это занимает одну лишнюю строку, не имеет дополнительного смысла. Таким образом правилен только первый описанный вариант.
Создание нескольких объектов
Ситуация когда создаётся несколько объектов немного сложнее. Рассмотрим создание двух объектов. Самый простой способ — использовать вложенные try
, с учётом все описанных выше особенностей:
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Такой способ имеет существенный недостаток — постоянно увеличивающийся уровень вложенности, особенно если нужно создать больше 2х объектов.
Попробуем убрать вложенность
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
В обычной ситуации всё работает правильно. Но если исключение возникает в конструкторе второго объекта, то снова получаем утечку памяти.
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Рассмотрим примерно такую же ситуацию, но когда создание двух объектов в блоке try
.
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Ситуация полностью совпадает с той, что происходит когда один объект создаётся в блоке try
— в случае исключения в конструкторе любого из объектов происходит освобождение памяти по случайному адресу.
В итоге правильный вариант:
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Сначала происходит инициализация переменных: потом их создание в защищённом блоке. При такой последовательности действий, при любых ситуациях, ресурсы будут освобождены.
Ссылки:
- Статья Заповеди молодого разработчика Delphiopen in new window
- Описаниеopen in new window ReportMemoryLeaksOnShutdown