Изображение квадрата Дюрера

ООО АВТОМАТИКА плюс

Rambler's Top100

Рейтинг@Mail.ru

Описание оператора try..finally..end

Версия 2.0 от 28.04.2008 г.

Перевод справки

Часто бывают случаи, когда в программе должна выполниться определенная часть кода, несмотря на то, что выполняемый до нее код может быть прерван вследствие возникновения исключительной ситуации. Например, когда операция захватывает ресурс, очень часто бывает важным освобождение данного ресурса, в не зависивисимости от того, была ли очередная операция завершена нормально, или нет. В таких случаях вы можете использовать оператор try..finally.

В следующий примере показан код, осуществляющий открытие и обработку файла, который гарантирует, что файл будет закрыт, даже если во время его обработки возникла ошибка:

Reset(F);
try
 ...  // Обработка файла F
finally
  CloseFile(F);
end;
Синтаксис оператора try..finally
try
 Набор_операторов_1
finally
 Набор_операторов_2
end
Где Набор_операторов_х - последовательность операторов, разделяемых символом ";". Оператор try..finally выполняет Набор_операторов_1, находящихся в секции try. Если Набор_операторов_1 завершается без возникновения исключения, то выполняется Набор_операторов_2, находящихся в секции finally. Если в ходе выполнения Набор_операторов_1 произошло исключение, то управление передается в секцию finally. Как только секция finally закончила работу, происходит регенериция исключения. Если выход из секции try произошел с помощью одного из операторов - Exit, Break или Continue, то операторы секции finally будут выполнены автоматически. Таким образом, секция finally выполняется всегда, как только произошло завершение работы секции try. Если в секции finally произошло исключение, и оно не было обработано внутри этой секции, то произойдет прерывание работы оператора try..finally, и любое исключение, возникшее в секции try, будет потеряно. В секции finally должна выполняться обработка всех возникающих в ней исключений, чтобы не мешать распространению других исключений.

Примеры защиты ресурсов

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

SomeClass1 := TSomeClass.Create;
try
  SomeClass2 := TSomeClass.Create;
  try
    { Некоторый код }
  finally
    SomeClass2.Free;
  end;
finally
  SomeClass1.Free;
end;

Ниже приведен пример правильной организации защиты ресурсов:

List1 := TList.Create;
try
  List2 := TList.Create;
  try
    List3 := TList.Create;
    try
      { Некоторый код }
    finally
      List3.Free;
    end;
  finally
    List2.Free;
  end;
finally
  List1.Free;
end;

В принципе, возможно обойтись одним оператором try..finally. В этом случае предыдущий пример упрощается:

List1 := TList.Create;
List2 := TList.Create;
List3 := TList.Create;
try
  { Некоторый код }
finally
  List1.Free;
  List2.Free;
  List3.Free;
end;

Этот пример является некорректным и плох он тем, что не учитывается вероятность возникновения исключения в конструкторе при вызове TList.Create(). Что произойдет, если при вызове конструктора для List3 произойдет исключение? Правильно! Управление оператору try..finally так и не будет передано, в связи с чем никогда не будет выполнен код List1.Free и List2.Free, что приведет к утечке памяти. В памяти останутся 2 списка: List1 и List2, однако List3 окажется удаленным. Список List3 будет автоматически уничтожен, как только в его конструкторе возникнет исключение. Дело в том, что при создании объекта (при вызове конструктора) осуществляется вызов функции System._ClassCreate, в ходе выполнения которой выделяется необходимый объем памяти для объекта, осуществляется инициализация и необходимая настройка, после чего выполняются действия, необходимые для обработки возникающих в конструкторе исключений. В этой же функции (System._ClassCreate) расположен код обработки возникающих в конструкторе исключений. В частности, осуществляется вызов деструктора, а также процедура _ClassDestroy, отвечающая за очистку памяти, занимаемой динамическими объектами, и освобождение всей памяти, занятой под хранение объекта. Получается что-то вроде следующего псевдокода:

constructor TSomeClass.Create;
begin
  try
   { Ваш код конструктора }
  except
    Destroy;
    raise
  end;
end;

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

destructor TSomeClass.Destroy;
var
  I: Integer;
begin
  for I := 0 to FList.Count - 1 do
  begin
    { Код обработки списка }
  end;
  FList.Free;
  inherited;
end;

Если в конструкторе произойдет исключение до того, как объект FList был создан, то при вызове FList.Count произойдет ошибка доступа к памяти, т.к. здесь FList = nil.

Правильным будет такой вариант:

destructor TSomeClass.Destroy;
var
  I: Integer;
begin
  if Assigned(FList) then
  begin
    for I := 0 to FList.Count - 1 do
    begin
      { Код обработки списка }
    end;
    FList.Free;
  end;
  inherited;
end;

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

При каждом использовании try..finally необходимо учитывать следующее:

  • Теоретически исключение может возникнуть при любой операции, требующей выделения участка памяти ненулевой длины. Вызов конструктора для создания объекта приводит к выделению необходимой памяти, что может привести к исключению. Также к исключению может привести вызов AllocMem() / GetMem() / ReallocMem().
  • Вызов деструктора не должен приводить к исключениям, т.к. в нем обычно выполняется множество операций по очистке памяти, и все они должны быть выполнены обязательно.

Начиная с Delphi7, разработчики VCL тем или иным образом исправили организацию защиты ресурсов. В исходных кодах VCL сейчас врядли можно найти (или найти очень сложно) место, где друг за другом вподряд вызываются 2 и более конструктора перед TRY. Например, в Delphi6 в одной из функций VCL присутствует следующий блок кода:

OuterStream := TMemoryStream.Create;
InnerStream := TMemoryStream.Create;
try
  ......................
finally
  InnerStream.Free;
  OuterStream.Free;
end;

Но при исправлении исходников VCL для Delphi7 разработчики учли, что если при втором вызове TMemoryStream.Create произойдет исключение, то возникнет утечка памяти, так как OuterStream удалить уже никто не сможет, и теперь данный блок кода в Delphi7 выглядет следующим образом:

InnerStream := nil;
OuterStream := TMemoryStream.Create;
try
  InnerStream := TMemoryStream.Create;
  ......................  
finally
  InnerStream.Free;
  OuterStream.Free;
end;

Теперь риск утечки памяти при выполнении данного кода отсутствует. Лишние операции присвоения ухудшают читабильность кода. Следующий код в этом плане считается более предпочтительным:

InnerStream := TMemoryStream.Create;
try
  OuterStream := TMemoryStream.Create;
  try
     .....................
  finally
    OuterStream.Free;
  end;
finally    
  InnerStream.Free;
end;

При создании группы объектов с указанием в конструкторе их владельца следует поступать следующим образом:

ADataBase := TDataBase.Create(nil);
try
 AQuery1 := TQuery.Create(ADataBase);
 AQuery2 := TQuery.Create(ADataBase);
 AQuery3 := TQuery.Create(ADataBase);
 {Ваш код}
finally
 ADataBase.Free;
end;

В каком бы конструкторе не произошло исключение, это не приведет к утечке памяти, т.к. вызов ADataBase.Free уничтожит все созданные дочерние объекты.

Далее приведу пример, в котором с помощью одного оператора try..finally выполняется защита сразу множества ресурсов:

MonoInfo := nil;
MonoBits := nil;
ColorInfo := nil;
ColorBits := nil;
try
 MonoInfo := AllocMem(MonoInfoSize);
 MonoBits := AllocMem(MonoBitsSize);
 ColorInfo := AllocMem(ColorInfoSize);
 ColorBits := AllocMem(ColorBitsSize);
 ...........................
finally
 FreeMem(ColorInfo, ColorInfoSize);
 FreeMem(ColorBits, ColorBitsSize);
 FreeMem(MonoInfo, MonoInfoSize);
 FreeMem(MonoBits, MonoBitsSize);
end;

Данный код взят из VCL. Если возник вопрос "почему перед вызовом FreeMem() не неполняется проверка на равенство NIL", то адресуйте его разработчикам VCL (процедура FreeMem в действительности выполняет данную проверку, по крайней мере, в Delphi7, но в документации это не отображено).

Вероятность утечки памяти в данном коде равна нулю, даже если при вызове AllocMem() произойдет исключение. Однако такой подход не рекомендуют, т.к. каждой переменной значения присваиваются по 2 раза, а это ухудшает читабельность кода.

Логинов Дмитрий © 2005-2015