Проекты Логинова Дмитрия | ||||||||||
|
Описание оператора 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 необходимо учитывать следующее:
Начиная с 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 |