суббота, 19 мая 2012 г.

Исключения (исключительные ситуации)

Перевод раздела Exceptions из справочной системы Delphi

Об исключениях

Исключительная ситуация возникает при ошибке или прерывании нормального хода выполнения программы каким-либо событием. Исключение передает контроль выполнения программы обработчику исключительной ситуации, который позволяет отделить нормальную логику работы программы от обработки ошибок. Поскольку исключения являются объектами, они могут быть сгруппированы в иерархию, использующую наследование, а новые исключения могут объявляться без изменения уже готового кода. Исключение может передавать информацию (например, сообшение об ошибке) из точки возникновения исключительной ситуации к месту ее обработки.

Когда приложение подключает модуль SysUtils большая часть ошибок, возникающих при выполнении программы, автоматически преобразуется в исключения. Значительная часть ошибок, результатом которых могло бы стать завершение программы (такие как недостаток памяти, деление на ноль, ошибки общей защиты) перехватываются и обрабатываются.


Когда следует применять исключения

Исключения обеспечивают удобный способ отслеживать ошибки при выполнении программы без применения массивных условных конструкций. Требования, накладываемые семантикой обработки исключительных ситуаций, влекут за собой увеличение объема кода и данных, а также снижение производительности приложения. По этому, не смотря на то, что существует возможность создавать исключения практически в любой ситуации, защищая любой блок исходного кода заключением его в структуру try...except или try...finally, на практике эти инструменты лучше применять только в особых случаях.

Обработка исключительных ситуаций подходит для:

  • трудноопределимых ошибок, вероятность возникновения которых невелика, но последствия которых могут оказаться серьезными (например, аварийное завершение приложения);
  • ошибок, возникновение которых трудно отследить при помощи инструкций if...then ;
  • случаев, когда необходимо реагировать на исключительные ситуации, создаваемые операционной системой или другими программами, код которых вы не контролируете.

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

Инструкции условных переходов зачастую являются лучшим способом обработки ошибок. Например, если перед тем как открыть файл, вы хотите убедиться, что файл существует, можно сделать это следующим образом:

try
    AssignFile(F, FileName);
    Reset(F);     // если файл не найден, 
                       //возникает исключительная ситуация EInOutError
except
    on Exception do ...
end;

Но для экономии ресурсов можно использовать и другой способ:

if FileExists(FileName) then    // возвращает False если файл не найден
//исключительной ситуации не возникает

begin
    AssignFile(F, FileName);
    Reset(F);
end;

Утверждения (Assertions) дают еще одну возможность проверки логических условий в вашем исходном коде. Если при выполнении инструкции Assert происходит ошибка, приложение либо завершается, либо (в том случае, если подключен модуль SysUtils) создает исключение SysUtils.EAssertionFailed. Утверждения должны использоваться только для проверки условий, невыполнения которых вы не ожидаете.


Объявление типов исключений

Исключения объявляются как и прочие классы. На самом деле в качестве исключения вы можете использовать экземпляр любого класса, но рекомендуется наследовать исключения от класса SysUtils.Exception, который объявлен в модуле SysUtils.

Вы можете объединять исключения в семейства при помощи наследования. Приведенные ниже объявления из модуля SysUtils объявляют семейство исключений для обработки математических ошибок:

type
   EMathError = class(Exception);
   EInvalidOp = class(EMathError);
   EZeroDivide = class(EMathError);
   EOverflow = class(EMathError);
   EUnderflow = class(EMathError);

Сделав такие объявления, вы можете создать один обработчик исключений для SysUtils.EMathError, который сможет работать с SysUtils.EInvalidOp, SysUtils.EZeroDivide, SysUtils.Overflow и SysUtils.EUnderflow.

Классы исключений иногда содержат поля, методы или свойства для передачи дополнительной информации об ошибке:

type EInOutError = class(Exception)
       ErrorCode: Integer;
     end;

Инициирование и обработка исключений

Для инициирования исключения вам необходимо использовать экземпляр класса исключения с инструкцией raise:

raise EMathError.Create;

Вообще говоря, инструкция инициирования исключения имеет следующий вид:

raise object at address

где object и at address являются опциональными. Когда указывается address, им может быть любое выражение, которое можно обработать как указатель. Обычно это указатель на процедуру или функцию. Например:

raise Exception.Create('Missing parameter') at @MyFunction;

Этой опцией можно инициировать исключение из участка стека, предшествующего тому, в котором на самом деле случилась ошибка. Когда исключение инициировано, то есть после вызова инструкции raise, управление им осуществляется по специальному принципу обработки исключительных ситуаций. Инструкция raise не возвращает управление в точку своего выполнения. Вместо этого управление передается обработчику исключений соответствующего класса, который находится в самом большом уровне вложенности блоков try except (т.е. относящийся к блоку, вход в который был осуществлен позже всего).

Например, приведенная ниже функция преобразует строку в целое число и инициирует исключение в том случае, если результирующее значение не находится в обозначенном диапазоне.

function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
    Result := StrToInt(S);   // StrToInt объявлен в SysUtils
    if (Result < Min) or (Result > Max) then
       raise ERangeError.CreateFmt('%d is not within the valid range of %d..%d', 
         [Result, Min, Max]);
end;

Обратите внимание на метод CreateFmt , вызываемый в инструкции raise. Класс SysUtils.Exception и его потомки оснащены особыми конструкторами, которые предоставляют возможность создавать сообщения об ошибках и идентификаторы контекста.

Инициированное исключение автоматически разрушается после его обработки. Не следует пытаться разрушать исключения самостоятельно.

Замечание: Вызов (инициирование) исключения в секции инициализации модуля может не привести к ожидаемому результату. Обычно обработка исключительных ситуаций выполняется из модуля SysUtils, который должен быть инициализирован прежде чем выполнять такие функции. Если исключительная ситуация имеет место в процессе инициализации – все инициализированные модули (включая SysUtils) финализируются, а исключение инициируется повторно. Затем, после перехвата, исключение обрабатывается (обычно завершением приложения). Инициирование исключений в секции финализации модуля также может не привести к нужному результату, в том случае, если на момент инициирования исключения модуль SysUtils уже финализирован.


Инструкции Try...except

Исключительные ситуации обрабатываются обрабатываются внутри конструкций try...except. Например:

try
   X := Y/Z;
   except
     on EZeroDivide do HandleZeroDivide;
end;

Эта конструкция предпринимает попытку деления y на z, а в случае возникновения исключительной ситуации SysUtils.EZeroDivide вызывает подпрограмму HandleZeroDivide.

Синтаксис инструкции try...except:

try statements except exceptionBlock end

где statements – это последовательность инструкций, разделенных точками с запятой (;), а exceptionBlock – может быть:

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

Обработчик исключения имеет вид:

on identifier: type do statement

где identifier: необязателен (если присутствует – может быть любым допустимым идентификатором), type – это тип для представления исключений, а statement – это любая инструкция.

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

Если в процессе выполнения списка команд возникла исключительная ситуация (она может быть инициирована инструкцией raise в списке команд или внутри процедуры или функции, вызываемой из этого списка) предпринимается попытка обработки исключительной ситуации:

Если какой-либо из обработчиков в exception block подходит для обработки инициированного исключения, управление передается первому такому обработчику. Обработчик подходит для исключения только в том случае, когда тип, указанный в нем, является типом исключения или предком его класса.

Если обработчика не найдено, управление передается инструкции в секции else (если таковая присутствует).

Если блок обработчиков – это просто последовательность инструкций без каких либо обработчиков – управление передается первой инструкции в этой последовательности.

Если ни одно из указанных выше условий не выполнено, поиск продолжается в блоке обработчиков внешней инструкции try...except , выход из которой еще не выполнен. Если и там не находится соответствующего обработчика, секции else или последовательности инструкций, поиск продолжается в следующей внешней инструкции и так далее. Если в самой внешней инструкции исключительная ситуация не будет обработана – приложение завершается.

Когда исключительная ситуация обработана, в стеке находится процедура или функция, содержащая инструкцию try...except , в которой произошла обработка и управление передается выполненному обработчику, секции else или последовательности инструкций. Этот процесс отменяет все вызовы процедур и функций, имевшие место после входа в блок try...except, в котором исключительная ситуация была обработана. Объект исключения автоматически разрушается вызовом деструктора Destroy и упрвление передается инструкции, следующей за блоком try...except. (Если вызов стандартных процедур Exit, Break или Continue выводит управление из обработчика, объект исключения также разрушается).

В следующем примере первый обработчик обрабатывает исключение деления на ноль, второй – ошибки переполнения, а последний – остальные математические исключения. SysUtils.EMathError указан последним в блоке обработчиков, так как он является предком обоих этих классов. Если бы он указан первым, последующие обработчики никогда не были бы вызваны:

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
end;

В обработчике исключения перед именем класса исключения можно указать идентификатор. Таким образом объявляется идентификатор для представления объекта исключения в процессе выполнения инструкции, следующей за on...do. Видимость идентификатора ограничивается этой инструкцией. Например:

try
  ...
except
  on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;

Если блок исключения имеет секцию else, в этой секции происходит обработка всех исключений, которые не были обработаны в блоке обработчика исключения. То есть:

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
else
  HandleAllOthers;
end;

В этом примере секция else обрабатывает все исключения, не являющимися SysUtils.EMathError.

Блок исключения, не содержащий обработчиков, но содержащий список инструкций, обрабатывает любые исключения. Например:

try
   ...
except
   HandleException;
end;

Здесь подпрограмма HandleException обрабатывает все исключенительные ситуации, которые возникают при выполнении инструкций между try и except.


Повторное инициирование исключений

Когда ключевое слово raise включается в блок исключения без указания ссылки на объект, оно инициирует исключение, уже обработанное в блоке. Это позволяет обработчику исключения среагировать на ошибку и затем повторно инициировать исключение. Повторное инициирование исключения может оказаться полезным, когда процедура или функция должна освободить память после своего выполнения, но не может полностью обработать исключение.

Например, функция GetFileList создает объект TStringList и заполняет его именами файлов, расположенными в определенной для поиска папке:

function GetFileList(const Path: string): TStringList;
var
  I: Integer;
  SearchRec: TSearchRec;
begin
  Result := TStringList.Create;
  try
    I := FindFirst(Path, 0, SearchRec);
    while I = 0 do
      begin
          Result.Add(SearchRec.Name);
          I := FindNext(SearchRec);
      end;
  except
      Result.Free;
      raise;
  end;
end;

GetFileList создает объект TStringList, затем применяет функции FindFirst и FindNext (определены в модуле SysUtils) для его инициализации. Если в процессе инициализации происходит ошибка – например, из-за того, что путь задан неверно или для заполенения списка строк недостаточно памяти - GetFileList нужно высвободить память созданного списка при том, что вызывающая подпрограмма ничего не знает о его существовании. По этой причине, инициализация списка строк выполняется внутри инструкции try...except. Если возникает исключительная ситуация, блок обработки исключения разрушает список строк и повторно инициирует исключение.


Встроенные исключения

Код, выполняемый в обработчике исключения может сам инициировать и обрабатывать исключительные ситуации. Поскольку исключения обрабатываются внутри обработчика, они не оказывают влияния на первичное исключение. Тем не менее, если исключение инициированное внутри обработчика, не обрабатывается в нем, первичное исключение теряется. Эту ситуацию иллюстрирует функция Tan, код которой приведен ниже:

type
   ETrigError = class(EMathError);
   function Tan(X: Extended): Extended;
   begin
      try
        Result := Sin(X) / Cos(X);
      except
        on EMathError do
        raise ETrigError.Create('Invalid argument to Tan');
      end;
   end;

Если исключение SysUtils.EMathError инициируется при выполнении Tan, обработчик инициирует исключение ETrigError. Поскольку Tan не имеет обработчика для ETrigError, это исключение выходит из обработчика, тем самым вызывая разрушение исключения SysUtils.EMathError. Вызывающей подпрограмме передается ETrigErrorexception, инициированное внутри функции Tan.


Инструкции Try...finally

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

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

Reset(F);
try
   ... // process file F
finally
   CloseFile(F);
end;

Синтаксис инструкции try...finally выглядит следующим образом:

try statementList1 finally statementList2 end

где каждый statementList – это последовательность инструкций, разделенных точками с запятой (;). Инструкция try...finally выполняет инструкции в statementList1 (секция try). Если выполнение statementList1 завершается без ошибок, выполняется statementList2 (секция finally). Если в процессе выполнения statementList1 возникает исключительная ситуация, управление передается в statementList2, когда statementList2 завершает свою работу исключение инициируется повторно. Если в процессе выполнения statementList1 происходит вызов процедур Exit, Break или Continue – управление выходит из statementList1 , statementList2 выполняется автоматически. Таким образом секция finally выполняется всегда, вне зависимости от того, как завершается работа секции try.

Если в секции finally возникает исключительная ситуация, которая не обрабатывается, исключение выводится из из инструкции try...finally, а исключение, инициированное в секции try, разрушается. Таким образом, секция finally должна обработать все внутренние исключительные ситуации с тем, чтобы они не были переданы за пределы секции.


Стандартные классы подпрограммы для работы с исключениями

Модули SysUtils и System объявляют несколько стандартных подпрограмм для обработки исключений, включая ExceptObject, ExceptAddr и ShowException. SysUtils, System и прочие модули включают в себя множество классов исключений, которые наследуются от SysUtils.Exception (или от OutlineError).

Класс SysUtils.Exception имеет свойства Message и HelpContext, которые можно использовать для передачи описания ошибки и идентификатора контекста для контекстной онлайн документации. Кроме того, этот класс объявляет конструкторы, позволяющие различными способами задавать описание и идентификатор контекста.

Комментариев нет:

Отправить комментарий