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

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

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

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

program Project1;

uses
  Vcl.Forms,
  Unit1 in 'Unit1.pas' {Form1};

{$R *.res}

begin
  ReportMemoryLeaksOnShutdown := True;
1
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;
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

Принципиально важный момент: блок 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;
1
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;
1
2
3
4
5
6
7
8
9
10
11

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

constructor TMyClass.Create;
begin
  raise Exception.Create('Big error');
end;
1
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;
1
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;
1
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;
1
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;
1
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;
1
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;
1
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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

Ссылки:

Последниее изменение: 24.08.2023, 06:42:55