Комплексный контроль за качеством кода
Страница 2. Процедура ASSERT


 

2. Процедура ASSERT

В Object Pascal введена специализированная процедура Assert, назначение которой - помощь в отладке кода и контроле над выполнением программы. Процедура является аналогом макроса ASSERT, который широко применяется практически во всех программах, написанных с использованием C и C++ и их библиотек. Синтаксис процедуры Assert (макрос имеет похожий синтаксис) описан ниже. 
procedure Assert(Condition: Boolean; [Msg: string]);

Процедура проверяет логическое утверждение, передаваемое первым аргументом и, если это утверждение ложно, то процедура выводит на экран диагностическое сообщение с номером строки и именем модуля, где расположен вызов этой процедуры, и сообщение пользователя, которое опционально передается вторым аргументом. В некоторых системах разработки, например (MSVC+MFC), макрос Assert принудительно завершает выполнение программы после выдачи соответствующего диагностического сообщения. В других системах (например Delphi) стандартная процедура ограничивается лишь выдачей диагностического сообщения.

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

В языках С и С++ отладочный режим компиляции задается определением (#define) соответствующей константы, обычно это _DEBUG. В Object Pascal отладочный режим включается специальной опцией компилятора. Кроме того, в С и С++ макрос ASSERT - это обычный макрос, ничем не отличающийся от множества других макросов. Макрос использует переменную компилятора __LINE__, что позволяет ему определить номер строки, в которой произошло нарушение проверки. В Object Pascal такой переменной нет, и за реализацию процедуры Assert полностью отвечает компилятор, что позволяет говорить о процедуре Assert, как об особенности компилятора и языка Object Pascal, а не как об обычной процедуре в составе библиотеки VCL.

Процедура Assert обычно применяется в следующих случаях:

  • в начале процедуры или функции для проверки правильности переданных аргументов;
  • в начале процедуры или функции для проверки правильности внутренних переменных;
  • в конце работы алгоритма для проверки правильности работы алгоритма;
  • для проверки правильности выполнения "надежных" функций, то есть тех функций, которые всегда должны выполняться успешно всегда, и их невыполнение рассматривается как фатальная ошибка программы. Хороший пример - функция CloseHandle вызываемая с верным дескриптором. Практически, можно не сомневаться в правильности выполнения этой функции, однако результат ее выполнения все-таки можно и нужно проверить. 
В любом из этих случаев невыполнение передаваемого в процедуру Assert условия рассматривается как совершенно неожиданная, фатальная ошибка алгоритма, которой не должно быть ни при каких условиях, и которая не оставляет никаких шансов на дальнейшее правильное выполнение программы. Например, возможен такой код. 
procedure AddElement(Elem: TObject; Index: Integer); 
begin
// Ссылка на объект не должна быть пустой
Assert(Elem <> nil, 'Пустая ссылка.');
// Индекс должен быть в пределах нормы
Assert((0 <= Index) and (Index < FCount), 'Неверный индекс');
// Что-то делаем
. . .
// Проверяем результат алгоритма
Assert(Result = 0, 'Ошибка в алгоритме вставки элемента');
end;

Первая процедура проверяет, является ли переданная ссылка на объект непустой. Вторая процедура проверяет индекс добавляемого элемента на принадлежность к ограниченному диапазону. Третья процедура проверяет правильность выполнения алгоритма. Ясно, что при невыполнении хотя бы одного из этих условий, необходимо считать, что данная процедура выполнилась неправильно, и дальнейшее правильное выполнение всей программы не представляется возможным. Ясно также, что, так как данную процедуру (процедуру AddElement) вызываем только мы (наша программа), такое событие никогда не должно происходить, в предположении, что алгоритм правилен и аргументы, передаваемые в функцию, верные. Однако, как известно: "Человек полагает, а Бог располагает", и при невнимательном программировании в большом проекте такой тип ошибок становиться основным.

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

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

Реализация процедуры Assert в Object Pascal полностью возложена на компилятор (вернее на "приближенный ко двору" модуль system.pas), что несколько затрудняет изменение логики работы. Возможны три основных варианта внесения изменений в логику работы. 

  1. Установка обработчика события TApplication.OnException и обработка в нем исключения с типом EAssertionFailed. Данный способ является наиболее простым, и менее гибким. Подходит, если все что необходимо - это вывести на экран особое сообщение и принудительно завершить программу.
  2. Прямая установка обработчика процедуры Assert, путем присвоения адреса своей процедуры системной переменной AssertErrorProc. Область применения практически та же самая, что и в предыдущем случае. Однако, в этом случае, возможны и более сложные манипуляции. Например, можно написать обработчик, который не генерирует исключение, а сразу выводит сообщение и принудительно завершает программу. Пример такого обработчика приведен ниже. 
    procedure AssertHandlerMSG(const Msg, Module: string; Line: Integer; 
    ErrorAddr: Pointer);
    begin
    // Выводим сообщение
    MessageBox(0,
    PChar(Format('Error "%s" at address 0x%8.8x in file %s, line %d.',
    [Msg, Integer(ErrorAddr), Module, Line])),
    'Error',
    MB_OK
    );
    // Выходим из программы
    ExitProcess(1);
    end;
    . . .
    // Настройка ссылки на глобальный обработчик процедуры Assert
    AssertErrorProc := @AssertHandlerMSG; 
  3. Написание процедуры Assert заново. Самый гибкий и удобный вариант. Например, если вас не устраивает стандартный синтаксис процедуры Assert, и вы хотите передать в процедуру дополнительные параметры (например, тип ошибки, тип генерируемого исключения, код, и т.п.). При этом возникает два особых момента связанных с тем фактом, что за процедуру Assert отвечает компилятор. Во-первых, вы не сможете управлять включением и включением вызовов процедуры Assert в программе через стандартные опции. Процедура Assert будет выполняться всегда. Единственное, что вы можете сделать - это сразу выйти из процедуры, если режим работы программы не отладочный. Во-вторых, в самой процедуре невозможно узнать номер строки, в которой произошел вызов процедуры. Тем не менее, обе этих трудности преодолимы.

Необходимость включения и выключения вызовов процедуры Assert, связанных с режимом работы программы является само собой подразумевающейся. Считается, что выключение вызовов Assert в отлаженной программе позволяет уменьшить размер программы и увеличить скорость ее работы. Однако, экономия на размере и увеличение быстродействия являются весьма малыми (если вы не включаете процедуру Assert в тело каждого цикла). Происходящее на этом фоне, молчаливое съедание ошибок в программе ставит под сомнение необходимость отключать вызовы процедуры Assert в "готовой" программе. То, что вы не нашли ошибок при разработке и отладке программы вовсе не означает, что там их нет. Ошибки могут появиться у пользователя программы, например, в условиях, в которых программа не тестировалась. Как вы о них узнаете, если отключите вызовы Assert? Конечно, лукавое отключение предупреждений может на время сохранить вашу репутацию, и пользователь может и не узнать о тех ужасах, которые происходят в недрах вашей программы. А внезапный безымянный сбой вашей программы вы можете списать на особенности работы Windows. Однако качества вашей программе это не прибавит. Честная регистрация всех ошибок намного улучшит ваши программы.

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

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