Комплексный контроль за качеством кода
Оформил: DeeCo
Автор: Николай Мазуркин
Оглавление
- Введение
- Процедура
Assert - Номера
строк или уникальные метки ? - Категории
ошибок - Тотальный
контроль - Заключение
- Пример
модуля с функциями комплексной обработки ошибок - Пример
использования системы комплексного контроля
1. Введение
Статья описывает систему методов и
действий, которые могут быть использованы при написании больших проектов
на Delphi. Основная цель системы заключается в эффективном повышении
качества кода и возможности быстрого поиска и исправления обнаруживаемых
ошибок. В настоящее время, несмотря на большие возможности системы Delphi
и библиотеки VCL, разработчикам не предложен простой и эффективный подход
к написанию качественных программ.
На основании личного опыта автора была составлена система методов,
которые позволяют значительно усилить контроль над выполнением программы,
ее сопровождением и исправлением обнаруживаемых ошибок. Статья может быть
полезна как для начинающих, так и для профессиональных программистов.
Однако, предполагается, что читатель хорошо знаком со средой разработки
Borland Delphi и языком Object Pascal, а также таким понятием как
исключение.
Все предложенные здесь методы не являются единственно возможными и
могут быть изменены и улучшены. Конкретный пример использования данной
системы приведен в прилагаемом проекте.
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), что
несколько затрудняет изменение логики работы. Возможны три основных
варианта внесения изменений в логику работы.
- Установка обработчика события TApplication.OnException и обработка в
нем исключения с типом EAssertionFailed. Данный способ является наиболее
простым, и менее гибким. Подходит, если все что необходимо — это вывести
на экран особое сообщение и принудительно завершить программу. - Прямая установка обработчика процедуры 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;
- Написание процедуры 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;
3. Номера строк или уникальные метки ?
В
большинстве систем разработки, в сообщении, выводимом при активизации
процедуры Assert, выводится три параметра: сообщение пользователя, имя
исходного модуля и номер строки, в котором находился вызов процедуры. При
этом основной упор при поиске ошибки делается на номер строки и имя
модуля, а сообщение пользователя является малоинформативным. Однако, если
вы решили оставить вызовы процедуры Assert в готовой распространяемой
программе, здесь таится проблема. При разработке программы вы будете
выпускать десятки и десятки версий, и естественно, что исходный код будет
сильно модифицироваться, а номера линий, на которых располагаются
проверочные вызовы, будут постоянно изменяться. И если вдруг ваш
пользователь сообщает о том, что в вашей программе сработала проверка на
линии N в исходном модуле M, вы должны задаться вопросом, какая версия
программы находится у пользователя? Даже если вы сохраните все исходные
тексты абсолютно всех версий, такой подход нельзя назвать удобным и ясным,
он таит в себе скрытую путаницу и неразбериху, поскольку вам придется
отслеживать номера строк в разных версиях программы при просмотре развития
программы.
Возникает идея — совсем отказаться от номеров строк — они таят в себе
хаос, и использовать уникальные сообщения пользователя, то есть уникальные
метки. В каждом вызове процедуры Assert передавать абсолютно уникальный,
не повторяющийся идентификатор, и выводить его на экран при срабатывании
проверки. После того как вам сообщат, что вашей программе активизировалась
проверка с идентификатором N, вы можете проследить развитие кода в этом
месте во всех версиях программы путем простого поиска идентификатора в
исходных текстах. Зачастую даже, вовсе не обязательно распаковывать ту же
версию исходных текстов программы, что и версия программы у пользователя.
Если код в этом месте давно не менялся, вы можете искать и исправлять
ошибку прямо в текущей версии исходных текстов.
Что выбрать в качестве уникальных идентификаторов? Можно выбрать любое
не повторяющееся число или строку. Например, последовательно передавать
числа от нуля и далее с инкрементом на единицу. Или передавать уникальные
строки, которые можно придумывать на ходу. Однако, это утомительное
занятие — помнить последний набранный номер или строку и не повторяться.
Можно написать специальный модуль для редактора в среде Delphi, который
будет автоматически вводить идентификаторы. Однако, если вы будете
работать над проектом на разных машинах, то возникнет проблема
синхронизации счетчиков.
Существует прекрасный вариант решения этой проблемы. В среде Delphi
нажмите клавиши <CTRL>+<SHIFT>+<G> и вы получите строку
похожую на эту: [‘{19619100-22B0-11D4-ACD0-009027350D25}’]. Это уникальный
идентификатор GUID (Global Unique IDentificator), шестнадцатибайтное
уникальное значение, переведенное в строковый вид с разграничителями и
заключенное в квадратные скобки. Такое значение уникально, оно зависит от
времени и от номера сетевой карты. Если сетевой карты нет, то номер
генерируется программно. По крайней мере, Microsoft утверждает, что такой
номер никогда не повторится. Каждый раз, когда вы нажмете соответствующую
комбинацию клавиш, система сгенерирует новый уникальный код. Если удалить
квадратные скобки по бокам, то получиться отличный уникальный строковый
идентификатор, готовый к использованию, при этом вызов процедуры AssertMsg
будет выглядеть подобным образом.
AssertMsg(FCount > 0, '{19619100-22B0-11D4-ACD0-009027350D25}');
В данном случае сообщение пользователя не передается — передается
только уникальная метка. В самом деле, какая разница пользователю, отчего
именно погибла ваша программа? Пользователь лишь должен сообщить, что ваша
программа работает с ошибкой и передать информацию, которая поможет эту
ошибку найти. Конечно, подобный уникальный номер занимает некоторое место
в сегменте констант, но, как показывает практика, не больше 5% от всего
объема программы, и в добавление, такой номер очень удобно записывать с
экрана. Кроме того, он действительно никогда не повторяется! Если же вы —
эконом, и вам жалко тратить 38 байт на каждый идентификатор, можете
передавать в процедуру AssertMsg четырехбайтное число, которое можно
получить, взяв первые восемь шестнадцатеричных цифр из строкового
идентификатора — именно они изменяются чаще всего.
AssertMsg(FCount > 0, $19619100);
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;
Таким образом, ключом к успеху является правильное разделение всех
вызовов функций в программе на две группы. Первая группа — потенциально
опасные, «ненадежные» функции должны быть обработаны с особой
тщательностью и возможностью восстановления программы после сбоя этих
функций. Ошибки при их выполнении вполне возможны и должны рассматриваться
как естественные. Вторая группа — «надежные» функции, невыполнение которых
должно приводить к выводу диагностического сообщения и закрытию программы.
Ошибки при выполнении надежных функций должны рассматриваться как
фатальные.
Вы либо полностью контролируете ситуацию, с возможностью отката к
нормальному состоянию программы — «нефатальная ошибка»; либо извиняетесь и
закрываете программу, так как вы не можете корректно обработать эту ошибку
— «фатальная» ошибка. Во втором случае вы должны сделать все возможное,
чтобы информация об ошибке была как можно содержательнее, для того, чтобы
вы могли ее исправить.
5. Тотальный контроль
В первой и второй главе
обсуждалась важность применения в программах процедуры Assert с
уникальными метками для быстрой локализации и идентификации ошибок. В
предыдущей главе обсуждался принцип разделения всех нештатных ситуаций на
две группы, требующих различного подхода. Следование этим принципам
поможет вам улучшить качество кода при написании больших проектов.
Однако существует еще одна проблема, связанная с тем, что широко
используемая библиотека VCL, а также множество других естественно не
используют выше указанные принципы. Например, при выполнении следующего
кода возникнет исключение от VCL с сообщением «List index out of bounds»,
так как мы запрашиваем на один элемент больше, чем есть в списке. Получив
сообщение о такой ошибке от пользователя, вы вряд ли сможете определить
место программы, где эта ошибка произошла. Поиск такой ошибки затруднен,
даже если она возникла на вашем компьютере, на том, на котором вы
разрабатываете программу. А если такая ошибка возникла у далекого
пользователя, сложность возрастает во много раз, так как вам предстоит еще
и «выбить» всю информацию об ошибке у человека далекого от Delphi в
частности, и от программирования вообще.
for i := 0 to FList.Count do FObj := TObject(FList[i]);
Та же самая ситуация произойдет, если вы попытаетесь обратиться по
«бешеному» указателю — указателю в котором хранится постороннее значение
указывающее в никуда. Вы получите малоинформативное сообщение, которое
никак не указывает на место, где эта ошибка произошла. Используя
вышеописанные принципы, вы сможете перехватить эту ошибку, распознать ее
как фатальную и закрыть программу. Однако вы не сможете выдать
информативное сообщение о месте где эта ошибка произошла. И ее локализация
и исправление будут сильно затруднены.
Одним из успешных вариантов решения такой проблемы является помещение
каждой (каждой !) процедуры в конструкцию try-except-end. При этом
локальный обработчик исключения ловит возникшую ошибку и заменяет ее своей
с указанием уникального идентификатора. При этом предыдущий пример может
выглядеть так.
try for i := 0 to FList.Count do FObj := TObject(FList[i]); except raise AssertInternal('{88A4613E-0ACB-11D4-ACCF-009027350D25}'); end;
Процедура AssertInternal «наследует» сообщение от возникшего
исключения, плюс, добавляет к нему свой уникальный идентификатор. При
возникновении ошибки, пользователю будет выдано соответствующее
предупреждение, после чего программа закроется. Возможна запись
предупреждающего сообщения в специальный файл, который впоследствии может
быть выслан вам пользователем.
Получив информацию о возникшей ошибке вы можете:
- с точностью до процедуры узнать место возникновения ошибки;
- узнать тип исключения и параметры ошибки (например ошибочный индекс
или адрес по которому произошел ошибочный доступ к памяти).
Такая информация значительно облегчает поиск и исправление
ошибок в очень большом проекте.
6. Заключение
Все вышеперечисленное являлось
лишь приблизительным рецептом и пищей для размышления. Точный вариант
комплексной системы обработки ошибок приведен в прилагаемом архиве. Модуль
ATSAssert.pas содержит примеры процедур и функций для обработки фатальных
ошибок. Модуль ATSDialogMain.pas содержит примеры их использования.
7. Пример модуля с функциями комплексной обработки
ошибок
//////////////////////////////////////////////////////////////////////////////// // ATSTest - иллюстрация комплексной обработки исключений // (c) Николай Мазуркин // _____________________________________________________________________________ // Процедуры обработки фатальных ошибок //////////////////////////////////////////////////////////////////////////////// unit ATSAssert; interface uses Windows, Messages, SysUtils, ActiveX, Forms;
Объявления деклараций используемых функций
Объявляется перечень и
формат функций для комлексного контроля за качеством кода.
//////////////////////////////////////////////////////////////////////////////// // Процедуры обработки фатальных ситуаций //////////////////////////////////////////////////////////////////////////////// procedure AssertMsg(Condition: Boolean; const Mark: string); procedure AssertWin32(Condition: Boolean; const Mark: string); function AssertInternal(const Mark: string): TObject; procedure HandleError(const Msg: string); implementation //////////////////////////////////////////////////////////////////////////////// // Utils // _____________________________________________________________________________ // Процедуры обработки фатальных ситуаций ////////////////////////////////////////////////////////////////////////////////
Глобальный обработчик исключений
Глобальный обработчик исключений
устанавливает процедуру обработки на событие Application.OnException. Это
позволяет перехватывать все исключения не пойманные конструкцией
try-except-AssertInternal-end. Хотя таких непойманных исключений быть не
должно, лучше все-таки поставить на них обработчик.
Если вы пишите библиотеку DLL в таком обработчике нет необходимости,
так как все непойманные исключения будут обрабатывать в EXE-модуле.
//////////////////////////////////////////////////////////////////////////////// // TExceptHandler // _____________________________________________________________________________ // Глобальный обработчик исключений //////////////////////////////////////////////////////////////////////////////// type TExceptHandler = class(TObject) procedure OnException(Sender: TObject; E: Exception); end; var ExceptHandler: TExceptHandler; //////////////////////////////////////////////////////////////////////////////// // Обработка исключений //////////////////////////////////////////////////////////////////////////////// procedure TExceptHandler.OnException(Sender: TObject; E: Exception); begin HandleError(PChar(E.Message)); end;
Основная процедура обработки ошибок
Основная процедура обработки
ошибок. Формирует и выводит сообщение, записывает его в посмертный лог и
закрывает программу. Если вы пишете библиотеку DLL, то для каждой
библиотеки можете добавить к сообщению свой дополнительный префикс,
например ‘A’, ‘B’, ‘C’ и т.д. Это поможет быстро определить в каком модуле
произошла ошибка.
//////////////////////////////////////////////////////////////////////////////// // Обработка ошибочной ситуации //////////////////////////////////////////////////////////////////////////////// procedure HandleError(const Msg: string); var DeadFile: TextFile; DeadName: string; ErrorMsg: string; begin // Формируем сообщение ErrorMsg := Msg + #13#13; // Если это исключение, то добавим его тип и адрес if ExceptObject <> nil then ErrorMsg := ErrorMsg + Format('Исключение типа %s, адрес 0x%8.8x'#13#13, [ExceptObject.ClassName, DWord(ExceptAddr)] ); ErrorMsg := ErrorMsg + 'Выполнение программы будет прекращено.'; // Показываем диалог MessageBox(0, PChar(ErrorMsg), 'Фатальная ошибка', MB_OK + MB_ICONWARNING + MB_APPLMODAL + MB_SETFOREGROUND + MB_TOPMOST); // Генерируем уникальное имя файла SetLength(DeadName, MAX_PATH + 10); GetTempFileName(PChar(ExtractFilePath(ParamStr(0))), 'err', 0, @DeadName[1]); // Записываем ошибку в посмертный лог - на случай если пользователь не догадается // записать код ошибки и сообщение. AssignFile(DeadFile, DeadName); ReWrite(DeadFile); WriteLn(DeadFile, ErrorMsg); CloseFile(DeadFile); // Немедленно закрываем процесс ExitProcess(1); end;
Альтернативная процедура Assert
//////////////////////////////////////////////////////////////////////////////// // Проверка выполнимости условия //////////////////////////////////////////////////////////////////////////////// procedure AssertMsg(Condition: Boolean; const Mark: string); begin // Если условие выполняется, то выходим сразу if Condition then Exit; // Обрабатываем возникшую ошибку HandleError('Фатальная ошибка ' + Mark); end;
Альтернативная процедура Assert для API-функций
//////////////////////////////////////////////////////////////////////////////// // Проверка выполнимости API-функции //////////////////////////////////////////////////////////////////////////////// procedure AssertWin32(Condition: Boolean; const Mark: string); var ErrorMsg: string; begin // Если условие выполняется, то выходим if Condition then Exit; // Получаем сообщение об ошибке ErrorMsg := SysErrorMessage(GetLastError); // Обрабатываем возникшую ошибку HandleError('Фатальная ошибка ' + Mark + #13#13+ErrorMsg); end;
Функция для тотального контроля
исключений
Функция для перехвата исключений от VCL и неудачных
обращений к памяти. Добаляет к тексту исключения уникальную метку и
генерирует ошибку.
//////////////////////////////////////////////////////////////////////////////// // Генерация фатального исключения //////////////////////////////////////////////////////////////////////////////// function AssertInternal(const Mark: string): TObject; begin // Проверяем тип сгенерированного исключения и обрабатываем возникшую ошибку if ExceptObject is Exception then HandleError('Фатальная ошибка ' + Mark + #13#13+ PChar(Exception(ExceptObject).Message)) else HandleError('Фатальная ошибка ' + Mark); // Следующая строчка никогда не выполнится, она нужна для успокоения компилятора Result := nil; end;
Инициализация модуля
Создание и настройка глобального обработчика
исключения на событии TApplication.OnException.
//////////////////////////////////////////////////////////////////////////////// // Автоматическое создание глобального обработчика исключения //////////////////////////////////////////////////////////////////////////////// initialization ExceptHandler := TExceptHandler.Create; Application.OnException := ExceptHandler.OnException; end.
8. Пример использования системы комплексного
контроля
В прилагаемом примере рассматриваются возможные ошибочные
ситуации и пути их обработки с использованием комплексного контроля за
качеством кода.
Исходные
тексты функций комплексного контроля и пример поясняющий их работу (8
кБ)
- Up to Parent: System
Delphi
procedure Assert(Condition: Boolean; [ Message: String ]); overload; procedure Assert(Condition: Boolean; [ Message: String ]); overload;
Contents
- 1 Properties
- 2 Description
- 2.1 See Also
- 2.2 Code Examples
Properties
Type | Visibility | Source | Unit | Parent |
---|---|---|---|---|
procedure | public | System.pas | System | System |
Description
Tests whether a Boolean expression is true.
In Delphi code, use Assert as a debugging tool to test that conditions assumed to be true are never violated. Assert provides an opportunity to intercept an unexpected condition and halt a program rather than allow execution to continue under unanticipated conditions.
Assert takes a Boolean expression and an optional message string as parameters. If the Boolean test fails, Assert raises an EAssertionFailed exception. If a message string was passed to Assert, the exception object is created with that string. Otherwise it is created with a default string indicating that the assertion failed. The message is displayed along with the complete path, filename, and the line number on which Assert failed.
The SysUtils unit causes run-time errors be turned into exceptions. If SysUtils is not used anywhere in your application, you will get a run-time error 227 rather than an EAssertionFailed exception. This run-time error will halt the program.
Because assertions are not usually used in shipping versions of a product, compiler directives are provided to disable the generation of assertion code:
$ASSERTIONS ON/OFF (long form) $C +/- (short form)
These are global switches that affect the entire source file where they occur, regardless of their position in the file. It is not possible to enable and disable assertions for something smaller than a source file. Assertions are on by default.
See Also
- EAssertionFailed
- Delphi Intrinsic Routines
- Assert directives (Delphi)
Code Examples
- SystemAssert (Delphi)
SourceLocalizer
Сообщ.
#5
,
08.04.13, 13:28
Newbie
Рейтинг (т): нет
Fr0sT,
>>С таким же успехом можно спеллчекить и выданную документацию.
Документация может быть представлена (javadoc) в виде большого набора html-файлов, что несколько затрудняет проверку.
при нахождении ошибки необходимо найти по классу файл, затем место в коде. Быстрей предлагаемый мной способ.
Но самое главное — правка/дополнение документации процесс не прекращающийся и как отслеживать
_изменение_ части документации, чтобы проверять только ее, а не _всю_ документацию.
>>1) Лично у меня обычно интерфейс весьма аскетичен на надписи, т.ч. 5-10 строк проверить не составляет труда
В больших проектах бывает много десятков форм и много сотен элементов на них.
При разработке группой программистов, как Вы предлагаете/делаете отслеживание появившихся новых названий хинтов/кнопок/пр.?
>>2) Еще раз, если проект нормальный — строковые ресурсы должны быть отделены как от кода, так и от дизайна…
Согласен. Поэтому и написал «Это не хорошо, но бывает.» Но этот пункт не главное, а просто лишний плюс моей программе за отслеживание такого кода
Программа создана для локализации в основном такого старого и не правильного подхода
с использованием жестко-закодированных строк, но может применяться и с ресурсными строками.
Но по этому пункту совершенно не буду спорить, так как такой подход не правильный «но бывает»,
и надо с таким кодом уметь работать (локализовывать/проверять орфографию)
без переделки больших проектов, только ради локалиазации/орфографии.
>>если же прога — поделка в основном для себя, то пара очепяток никакого вреда не нанесут и будут поправлены на основе багрепортов
При большом проекте будет неудобно перед клиентами за опечатки, поэтому багрепорт лучше оставить на крайний случай,
если останется, действительно, только пара опечаток.
>>если ваше средство дает какую-то степень автоматизации
Сначала попробовали автоматически исправлять, но тогда надо править код сразу для разных программистов и потом согласовывать(мержить) с ними файлы,
поэтому решили использовать приведенный отчет, так как по нему просто каждому поправить свои файлы.
Дальнейшие проверки будут производится только по измененному тексту,
т.е. с учетом ревизий системы контроля версий (например, svn).
>>Вот это вообще ужасть. Весь мир уже лет десять как перешел на Юникод,
Согласен. Лучше тогда привести пример с умлаутами и пр., например, для немецкого языка, который тоже нельзя использовать в 1251.
Полностью согласен. Будет возможность — перейдем. Пока используем delphi 7.
>>Кстати, какие именно настройки ОС командуют Дельфям сохранять текст на формах в cp1251
Сохранение в 1251 и юникоде зависит от версии — Delphi 5/6/7/… В D5 cp1251 (#000), D6/D7/.. юникод (#0000).
Использование простого текста в dfm позволяет легко переходить между D5/D6/D7, но это очень специфичная проблема.
Открытие dfm зависит от локализации системы — вместо кириллицы в юникоде будут знаки вопросов (для D7 на русской ОС и D7 на английской ОС).
>>настроек операционной системы
Поправлю фразу в описании на «локализации операционной системы» и дополню конкретными примерами. Спасибо.
Предлагаю вернуться к теме орфографии
Интересен, в первую очередь, подход к исправлению орфографии, с учетом контроля версий
и возможностей отслеживания изменений текста особенно на формах, а не проверки периодически всего проекта.
Спасибо.
Delphi Assert Function — A Debugging Tool in Delphi
Assert function in Delphi is used as a debugging tool. Assert Function is used to make sure that certain conditions which are assumed to be true are never violated. Assert is a Symbol not a Keyword in Delphi. You can set Assertion ON/OFF in RAD Studio.
Syntax of Assert Function:
function Assert(expr : Boolean [; const msg: string]);
Assert function tests whether a boolean expression expr is true. If not, Assert raises an EAssertionFailed exception. If a message string was passed to Assert, the exception object is created with that string (Exception.CreateFmt).
Assert provides an opportunity to intercept an unexpected condition and halt a program rather than allow execution to continue under unanticipated conditions.
Code Snippet for Assert Function
procedure TestAssert;
var
i : integer;
begin
i:=5;
assert(i<2,’Assert Message’);
end;
Now when you run the above Delphi code with Assertions enabled, you will get message ‘Assert Message’ because ‘i<2’ is false. If i = 1, the program will continue as nothing happened.
ASSERT is not a KEYWORD in DELPHI
Assert is not a keyword in Delphi. Assert is the symbol that is not reserved by the compiler. Assert exists as symbol within the «System» unit namespace. You can create your own Assert procedure or function and even though it’s not recommended, it will compile.
Assertion Settings in Delphi
We can put settings in the RAD Studio to tell the compiler whether to compile with assertions in a debug mode or not. Assertion are always «ON» in debug mode and «OFF» in release mode by default. Compiler directives are provided to disable the generation of assertion code: $ASSERTIONS ON/OFF(long form)
Июль 26th, 2013
admin
Процедура Assert используется в Delphi при отладке программы на предмет истинности утверждений. Эти утверждения по смыслу должны быть истинны, но почему-то могут нарушаться.
procedure Assert (expr : Boolean [; const msg : string]);
Если проверяемое утверждение будет ложным, то процедура прекратит работу и сгенерирует исключение EAssertionFailed с выдачей ошибки в сообщении. На входе у процедуры параметр expr, который проверяется на истинность. Также, в строку может передаться строка msg – строка сообщения. Исключение генерируется со стандартной строкой сообщения, если строка в вызове процедуры не указана. Если Вам понадобится современный и надежный платежный агрегатор интернет платежей net2pay.ru обладает простой интеграцией и удобным интерфесом, который позволяет вести свой бизнес в любой точке мира. Сообщение отображается с указанием имени файла и номером строки, в которой возникает ошибка.
Пример:
Assert (A <>nil, ‘Внимание A=nil !!!’);
Процедура Assert работает при включенной директиве {$C+} или {$ASSERTIONS ON}. В случае, если сообщение не было перехвачено процедурой, программа завершится с кодом 227. В итоговой программе, которая работает и отладка не требуется, следует отключить директивы {$C-} или {$ASSERTIONS OFF}. При этом компиляция проверок не производится, и в отлаженной программе не остается ничего лишнего.
.
assert delphi
assert pascal
дельфи Assert
delphi assert
Assert delphi
Комментирование и размещение ссылок запрещено.