Комплексный контроль за качеством кода
Страница 4. Категории ошибок


4. Категории ошибок

В последнее время, практически во всех развитых объектно-ориентированных языках, появилась структурная обработка исключений. Эта возможность поддерживается операционной системой и позволяет перехватывать нештатные ситуации возникающие, как в обычном, так и в защищенном режиме системы. Язык Object Pascal полностью поддерживает все возможности по обработке исключений. Использование исключений при разработке программ рекомендуется документацией и поддерживается широким использованием исключений в VCL. Типичный пример обработки исключений приведен ниже. 
try
  FS := TMemoryStream.Create;
   try
    FS.LoadFromFile('sound.dat'); 
    . . .
   finally
       FS.Free;
   end;
except
  FSoundData := nil;
   raise;
end;

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

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

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

Прежде всего, ошибки нужно разделить на две группы - фатальные ошибки и нефатальные или восстановимые ошибки. Фатальные ошибки - это ошибки "несовместимые с жизнью", после которых выполнение программы невозможно и бессмысленно. Фатальная ошибка всегда является неожиданной и подразумевается маловероятной. Например, любое срабатывание проверки Assert означает серьезное нарушение логики работы программы. Сбой при выделении памяти или ресурсов в большинстве случаев также является невосстановимым. Подавляющее большинство API функций при передаче в них правильных параметров можно отнести к "надежным" функциям, то есть таким функциям, в выполнении которых мы уверены, и невыполнение которых маловероятно, но приводит к фатальным ошибкам. Фатальные ошибки можно отнести к серьезным недостаткам либо в программе, либо в операционной системе.

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

Грань между фатальными и восстановимыми ошибками очень тонка и неопределенна. В одном случае ошибку можно отнести к фатальным ошибкам, а в другом - к восстановимым. Но, тем не менее, каждая нештатная ситуация должна быть четко отнесена к одному из этих двух классов.

В пределе такого подхода, все функции должны быть объявлены ненадежными, и должно быть предсмотрено восстановление нормальной работы программы после любой нештатной ситуации. Однако, такое вряд ли возможно, так как в таком случае 99% от объема программы будут занимать проверки и восстановления после сбоя при выполнения любой из функции. Таким образом, для успешной и эффективной работы, необходимо как можно больше функций отнести к разряду надежных, и как можно меньше - к разряду ненадежных, требующих специальной обработки.

Если ошибка отнесена к фатальным ошибкам, мы должны установить соответствующую проверку, например Assert, извиниться и закрыть программу в случае ее появления. Если ошибка отнесена к восстановимым ошибкам, то она должна быть обработана соответствующим образом с выводом предупреждения и возможностью восстановления исходного состояния программы.

Таким образом, мы пришли к пониманию того, что существуют разные группы ошибок, с разным алгоритмом их обработки. Как уже упоминалось, все ошибки в библиотеке VCL обрабатываются одинаковым образом. Изменить существующее положение вещей можно разными способами. Например, в можно анализировать все типы исключений в обработчике TApplication.OnException. Исключения таких типов как, например, EAccessViolation, EListError, EAbstractError, EArrayError, EAssertionFailed и многих других можно рассматривать как фатальные ошибки, а исключения остальных типов рассматривать как восстановимые ошибки. При этом откат при восстановимых ошибках выполнять путем локального перехвата исключения конструкцией try-except-end, обработки и дальнейшей генерации исключения инструкцией raise. Однако такой способ не является гибким, так как один и тот же тип исключения в одной операции может рассматриваться как восстановимый, а в другой - как фатальный.

Улучшенной разновидностью такого способа является способ, когда все восстановимые исключения от VCL перехватываются и обрабатываются на местах, а фатальные исключения транспортируются в глобальный обработчик TApplication.OnException. Таким образом, глобальный обработчик рассматривает любое исключение как фатальное. Перехват восстановимых исключений не занимает много места, так как и было указано раньше, подавляющее большинство операций можно и нужно рассматривать как "надежные" операции, то есть приводящие к фатальным ошибкам.

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

try
  // Потенциально опасная операция
  FS := TFileStream('sound.dat', fmOpenRead);
except
  ShowMessage('Невозможно открыть файл данных');
   Exit;
end;
// Проверка размера
if FS.Size <> 1000 then Exit;
// "Надежная" операция - все должно выполняться нормально
FS.Position := 0;
FS.ReadBuffer(Fbuffer, 1000);

Вызовы API функций не генерируют исключений, поэтому следует анализировать и обрабатывать результаты выполнения потенциально опасных функций, а "надежные" функции обертывать процедурой Win32Check, а еще лучше специальной процедурой Assert с указанием кода ошибки и уникальной метки. 

procedure AssertWin32(Condition: Boolean; Mark: string);
var
  ExceptAddr : Pointer;
  ExceptMsg  : string;
begin
  // Если условие выполняется, то выходим
  if Condition then Exit;
  // Получаем адрес вызвавшей нас инструкции Call
   asm
    mov EAX, [EBP+$04]
    mov ExceptAddr, EAX
   end;
  // Получаем сообщение об ошибке
  ExceptMsg := SysErrorMessage(GetLastError);
  // Возбуждаем исключение в месте вызова этой функции
  raise EFatalExcept.Create('Ошибка Win32: '+ExceptMsg+#13#13+Mark) at ExceptAddr;
end;

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

Вы либо полностью контролируете ситуацию, с возможностью отката к нормальному состоянию программы - "нефатальная ошибка"; либо извиняетесь и закрываете программу, так как вы не можете корректно обработать эту ошибку - "фатальная" ошибка. Во втором случае вы должны сделать все возможное, чтобы информация об ошибке была как можно содержательнее, для того, чтобы вы могли ее исправить.

 

 
« Предыдущая статья   Следующая статья »