Что должна сделать программа при ошибках

Этимология термина «баг»Править

«Так было со всеми моими изобретениями. Первый шаг — интуиция, которая приходит как вспышка, затем возникают трудности — устройство отказывается работать, и именно тогда проявляются «жучки» — как называют эти мелкие ошибки и трудности — и требуются месяцы пристального наблюдения, исследований и усилий, прежде чем дело дойдёт до коммерческого успеха или неудачи».

Во время Второй мировой войны словом именовали проблемы с радарной электроникой.

Предотвращение и обработка ошибок

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

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

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

// здесь инструкции, выполнение которых может вызвать исключение

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

// инструкции обработки исключения

— ключевое слово, показывающее, что далее следуют инструкции, при выполнении которых возможно возникновение исключений, и что обработку этих исключений берет на себя программа;

— ключевое слово, обозначающее начало секции обработки исключения указанного типа. Инструкции этой секции будут выполнены при возникновении исключения указанного типа.

В качестве примера в листинге 7.1 приведен конструктор формы программы «Линейная диаграмма», обеспечивающий обработку исключений.

Что должна сделать программа при ошибках

Листинг 7.1. Пример обработки исключений

// чтение данных из файла в массив

System::IO::StreamReader^ sr; // поток для чтения

sr = System::IO::StreamReader( Application::StartupPath + «\usd.dat»);

// создаем массив

Задаем функцию обработки события Paint

Что должна сделать программа при ошибках

// обработка исключений: // нет файла данных

// неверный формат данных

Основной характеристикой исключения является его тип. В табл. 7.1 перечислены наиболее часто возникающие исключения и указаны причины, которые могут привести к их возникновению.

Отчёты об ошибкахПравить

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

СсылкиПравить

  • Уязвимости в исходных кодах, «Компьютерная газета». Продолжение: Уязвимости в исходных кодах. Перепечатка: 1 часть (недоступная ссылка), 2 часть (недоступная ссылка).
  • 10 худших ошибок в программировании в истории человечества
  • 2010 CWE/SANS Top 25 Most Dangerous Software Errors частичный перевод на русский 25 самых опасных ошибок при создании программ
  • Ошибки, обнаруженные в Open Source проектах разработчиками PVS-Studio с помощью статического анализа. Можно найти полезные примеры при подготовки статей и презентаций.

Проверяемые исключенияПравить

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

С подобными эффектами можно бороться организационными мерами: описывая возможные исключения, возникающие в библиотечных модулях, в соответствующей документации. Но при этом всегда остаётся вероятность пропустить необходимый обработчик из-за случайной ошибки или несоответствия документации коду (что вовсе не редкость). Чтобы полностью исключить потерю обработки исключений, в обработчики приходится специально добавлять ветвь обработки «всех остальных» исключений (которая гарантированно перехватит любые, даже заранее неизвестные исключения), но такой выход не всегда оптимален. Более того, сокрытие всех возможных исключений может привести к ситуации, когда будут скрыты серьёзные и при этом трудно обнаруживаемые ошибки.

Механизм проверяемых исключений

Позже в ряде языков, например, в Java, появились проверяемые исключения. Сущность этого механизма состоит в добавлении в язык следующих правил и ограничений:

  • В описании функции (или метода класса) в явном виде перечисляются все типы исключений, которые она может сгенерировать.
  • Функция, вызывающая функцию или метод с объявленными исключениями, для каждого из этих исключений обязана либо содержать обработчик, либо, в свою очередь, указывать этот тип как генерируемый ею в своём описании.
  • Компилятор проверяет наличие обработчика в теле функции или записи исключения в её заголовке. Реакция на наличие неописанного и необработанного исключения может быть разной. Например, в Java, если компилятор обнаруживает возможность возникновения исключения, которое не описано в заголовке функции и не обрабатывается в ней, программа считается некорректной и не компилируется. В C++ возникновение в функции неописанного и необработанного исключения приводит к немедленному завершению программы; при этом отсутствие у функции списка объявленных исключений обозначает возможность возникновения любых исключений и стандартный порядок их обработки внешним кодом.

Внешне (в языке Java) реализация такого подхода выглядит следующим образом:

// код метода, возможно, содержащий вызовы, способные бросить исключение SQLException

// Ошибка при компиляции — исключение не объявлено и не перехвачено

// Правильно — исключение объявлено и будет передаваться дальше

// Правильно — исключение перехватывается внутри метода и наружу не выходит

// Обработка исключения

Здесь метод getVarValue объявлен как генерирующий исключение SQLException. Следовательно, любой использующий его метод должен либо перехватить это исключение, либо объявить его как генерируемое. В данном примере метод eval1 приведёт к ошибке компиляции, поскольку вызывает метод getVarValue, но не перехватывает исключение и не объявляет его. Метод eval2 объявляет исключение, а метод eval3 перехватывает и обрабатывает его, оба этих метода корректны в отношении работы с исключением, вызываемым методом getVarValue.

Преимущества и недостатки

Проверяемые исключения снижают количество ситуаций, когда исключение, которое могло быть обработано, вызвало критическую ошибку в программе, поскольку за наличием обработчиков следит компилятор. Это особенно полезно при изменениях кода, когда метод, который не мог ранее выбрасывать исключение типа X, начинает это делать; компилятор автоматически отследит все случаи его использования и проверит наличие соответствующих обработчиков.

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

У проверяемых исключений есть и недостатки.

Из-за перечисленных недостатков при обязательности использования проверяемых исключений этот механизм часто обходят. Например, многие библиотеки объявляют все методы как выбрасывающие некоторый общий класс исключений (например, Exception), и только на этот тип исключения создаются обработчики. Результатом становится то, что компилятор заставляет писать обработчики исключений даже там, где они объективно не нужны, и становится невозможно определить без чтения исходников, какие именно подклассы декларируемых исключений бросает метод, чтобы навесить на них разные обработчики. Более правильным подходом считается перехват внутри метода новых исключений, порождённых вызываемым кодом, а при необходимости передать исключение дальше — «заворачивание» его в исключение, уже возвращаемое методом. Например, если метод изменили так, что он начинает обращаться к базе данных вместо файловой системы, то он может сам ловить SQLException и выбрасывать вместо него вновь создаваемый IOException, указывая в качестве причины исходное исключение. Обычно рекомендуется изначально объявлять именно те исключения, которые придётся обрабатывать вызывающему коду. Скажем, если метод извлекает входные данные, то для него целесообразно объявить IOException, а если он оперирует SQL-запросами, то, вне зависимости от природы ошибки, он должен объявлять SQLException. В любом случае, набор выбрасываемых методом исключений должен тщательно продумываться. При необходимости имеет смысл создавать собственные классы исключений, наследуя их от Exception или других подходящих проверяемых исключений.

Исключения, не требующие проверки

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

  • Исключения, представляющие собой серьёзные ошибки, которые, «по идее», возникать не должны, и которые в обычных условиях не следует обрабатывать программой. Такие ошибки могут возникать как во внешней относительно программы среде, так и внутри неё. Примером такой ситуации может быть ошибка среды исполнения программы на Java. Она потенциально возможна при исполнении любой команды; за редчайшими исключениями в прикладной программе не может быть осмысленного обработчика подобной ошибки — ведь если среда исполнения работает неверно, на что указывает сам факт исключения, нет никакой гарантии, что и обработчик будет исполнен правильно.
  • Исключения времени выполнения, обычно связанные с ошибками программиста. Такие исключения возникают из-за логических ошибок разработчика или недостаточности проверок в коде. Например, ошибка обращения по неинициализированному (нулевому) указателю, как правило, означает, что программист либо пропустил где-то инициализацию переменной, либо при выделении динамической памяти не проверил, действительно ли память была выделена. Как первое, так и второе требует исправления кода программы, а не создания обработчиков.

Выносить подобные ошибки вообще за пределы системы обработки исключений нелогично и неудобно, хотя бы потому, что иногда они всё-таки перехватываются и обрабатываются. Поэтому в системах с проверяемыми исключениями часть типов исключений выводится из-под механизма проверки и работает традиционным образом. В Java это классы исключений, унаследованные от java.lang.Error — фатальные ошибки и java.lang.RuntimeException — ошибки времени выполнения, как правило, связанные с ошибками кодирования или недостаточностью проверок в коде программы (неверный аргумент, обращение по пустой ссылке, выход за границы массива, неверное состояние монитора и т. п.).

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

Действия в режиме паузы

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

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

если вы все-таки зашли в процедуру, выполнили там необходимые действия и хотите быстро довести ее выполнение до конца, то в вашем распо-

если в процессе просмотра кода вы пролистали код достаточно далеко и вам хочется вернуться к месту остановки (строка, выделенная желтым) без долгих поисков, то в вашем распоряжении команда Show Next Statement;

Чаще всего переход в режим остановки нужен, чтобы просмотреть текущие значения переменных или исправить код. Текущие значения переменных можно просмотреть при помощи окон , или (о них будет рассказано в следующих разделах), а можно воспользоваться подсказ-

Способы обработки ошибок

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

  • возврат кода ошибки. Например, если при отправке сообщения по сети выяснится, что соединение разорвано, функция отправки может вернуть единицу, а при успешной отправке — ноль;
  • вернуть заведомо некорректный результат. Например, функция malloc, выделяющая память в языке Си при ошибке (невозможности выделить память) возвращает ноль, а в остальных случаях — начальный адрес выделенного фрагмента;
  • вернуть любое допустимое значение и выставить глобальный флаг ошибки (в языке С++ для этого может использоваться глобальная переменная errno.
  • аварийно завершить работу (в С++ для этого используются функции abort или exit);
  • выработать исключение (подробнее написано ниже);

Конечно, помимо завершения работы, вы всегда можете записать описание сложившейся ситуации в log-файл (поток cerr в С++) или вывести на экран. Аварийное завершение работы — это худший способ обработки ошибки, т.к. программист фактически расписывается в своей неспособности что-то исправить. С точки зрения последовательности выполнения команд, возврат кода ошибки ничем не отличается от возврата некорректного значения или выставления флага ошибки в глобальный объект. Во всех этих случаях функция, которая не знает как корректно обработать входные данные передает эти обязанности непосредственно коду, который ее вызвал. Чаще всего это работает хорошо, однако:

Поиск и исправление ошибокПравить

Для отладки программы (англ. ) разработчиками ПО используются специальные программы-отладчики (англ. ). Например, в операционной системе Windows можно использовать программу WinDbg из пакета Microsoft Debugging Tools for Windows. Для GNU/Linux и ряда других UNIX-подобных операционных систем существует отладчик GDB (GNU Debugger).

Значение и классификация ошибок программного обеспеченияПравить

В зависимости от этапа разработки ПО, на котором выявляется ошибка, выделяют:

  • синтаксические ошибки (распознаваемые в качестве таковых транслятором и делающие компиляцию невозможной) — например отсутствие или несоответствие открывающей и закрывающей скобок;
  • предупреждения (warnings) компилятора — например, использование неинициализированной переменной. В этом случае компилятор может заметить, что программист делает что-то необычное (вероятно неверное), и сообщает об этом, однако программист сам принимает решение, игнорировать сообщение или нет;
  • ошибки времени исполнения, смысловые ошибки (семантические) — например вычитание переменных вместо сложения или ошибка сегментации.
  • Критические (иногда ) — лишающие программу всей полезности;
  • Серьёзные;
  • Незначительные;
  • Косметические.

По времени появления:

  • Постоянно, при каждом запуске;
  • Иногда («плавающий» тип);
  • Только на машине у клиента (зависит от локальных настроек у клиента).

По месту и направлению:

  • Ошибки пользовательского интерфейса;
  • Системы обработки ошибок;
  • Ошибки, связанные с граничными условиями (например, некорректная обработка пустой строки или максимального числового значения);
  • Ошибки вычислений;
  • Ошибки управления потоками;
  • Ошибки обработки или интерпретации данных;
  • При состоянии гонки;
  • Повышение нагрузки;
  • Ошибки контроля версии и идентификаторов;
  • Ошибки тестирования.

В зависимости от характера ошибки, программы и среды исполнения, ошибка может проявляться сразу или наоборот — долгое время оставаться незамеченной (например Проблема 2038 года).

Также ошибка может проявляться в виде уязвимости, делающей возможным несанкционированный доступ к системе или DoS-атаку.

Достоинства и недостаткиПравить

Достоинства использования исключений особенно заметно проявляются при разработке библиотек процедур и программных компонентов, ориентированных на массовое использование. В таких случаях разработчик часто не знает, как именно должна обрабатываться исключительная ситуация (при написании универсальной процедуры чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей процедуру программы), но ему это и не нужно — достаточно сгенерировать исключение, обработчик которого предоставляется реализовать пользователю компонента или процедуры. Единственная альтернатива исключениям в таких случаях — возврат кодов ошибок, которые вынужденно передаются по цепочке между несколькими уровнями программы, пока не доберутся до места обработки, загромождая код и снижая его понятность. Использование исключений в целях контроля ошибок повышает читаемость кода, так как позволяет отделить обработку ошибок от самого алгоритма, и облегчает программирование и использование компонентов других разработчиков. А обработка ошибок может быть централизована в аспектах.

К сожалению, реализация механизма обработки исключений существенно зависит от языка, и даже компиляторы одного и того же языка на одной и той же платформе могут иметь значительные различия. Это не позволяет прозрачно передавать исключения между частями программы, написанной на разных языках; например, поддерживающие исключения библиотеки обычно непригодны для использования в программах на языках, отличных от тех, для которых они разработаны, и, тем более, на языках, не поддерживающих механизм обработки исключений. Такое состояние существенно ограничивает возможности использования исключений, например, в ОС UNIX и её клонах и под Windows, так как большинство системного ПО и низкоуровневых библиотек этих систем пишется на языке Си, не поддерживающем исключений. Соответственно, для работы с API таких систем с применением исключений приходится писать библиотеки-обёртки, функции которых анализировали бы коды возврата функций API и в нужных случаях генерировали бы исключения.

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

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

Пишите код, безопасный с точки зрения исключений (exception-safe)

Что такое «код, безопасный с точки зрения исключений»? Пусть имеется какой-то произвольный фрагмент кода. Где-то в процессе выполнения этого кода генерируется исключение и перехватывается снаружи. Этот фрагмент кода безопасен с точки зрения исключений если он, в идеале, отвечает следующим требованиям:

  • Все ресурсы, выделенные внутри блока try до генерации исключения, должны быть корректно освобождены;
  • Все объекты, созданные внутри блока try до генерации исключения, должны быть корректно уничтожены;
  • Должен произойти полный откат всех изменений в системе, внесенных кодом, который выполнился от начала блока try до момента генерации исключения.

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

Транзакционность и «бизнес-логика»

Блоками try следует охватывать те участки кода, которые должны иметь транзакционное поведение с точки зрения «бизнес-логики» самой программы.

Рассмотрим в качестве примера графический редактор, имеющий multi-dialog интерфейс, и следующий сценарий работы: пользователь выбирает у главном меню «Open file(s)», выделяет десять картинок и нажимает «Open». Восемь картинок загружаются корректно, загрузка девятой генерирует исключение.

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

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

Обеспечение транзакционности

Некоторые авторитетные люди, чьи книги издаются чуть ли не миллионными тиражами, утверждают, что одна из причин, по которой исключения лучше чем коды ошибок, заключается в том, что код, занимающийся ремонтом программы можно отделить от логики работы программы, поместив его в блоки catch. К сожалению, этот подход устарел уже на следующий день после выхода первого Стандарта языка C++, а его использование говорит о полном непонимании всей прелести языка.

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

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

Типы данных

Практически во всех ситуациях наиболее естественный тип данных для пользовательских исключений с точки зрения C++ это тип, так или иначе производный от std::exception. Моя рекомендация состоит в том, чтобы в качестве типа данных для пользовательских исключений использовать std::runtime_error, или его наследников, если это необходимо.

Некоторые люди (не буду показывать пальцем) используют оператор new для генерации исключений и перехватывают их по указателю. Такая конструкция в свое время была (а может быть и до сих пор есть) в макросах Visual Assist-а. Такой подход в C++ в корне неверен — применять его можно только в языках со сборщиком мусора (gc). Использование такого подхода в C++ говорит о полном непонимании механизма исключений. Генерировать исключения следует по значению, перехватывать — по константной ссылке.

Классификация и оповещение

Любая ошибка имеет причину и влечет следствие. Используйте виртуальную функцию std::exception::what() для того, чтобы получить человеческое описание причины возникновения ошибки.

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

Следствие ошибки напрямую зависит от того места, где исключение было перехвачено. Следствие любого перехваченного исключения в коде, безопасном с точки зрения исключений, это откат системы на блок try, в котором это исключение было сгенерированно. То есть, говоря по-простому, следствие это «что не получилось сделать», в то время как причина это «почему это не получилось сделать». Место генерации исключения и его тип — это причина, место его перехвата — это следствие.

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

Не могу загрузить изображение (следствие): недостаточно прав для чтения файла 001.jpg (причина)

Тестирование

Главный способ обеспечения безошибочной работы программы — это ее тестирование. При создании крупных программных продуктов на их тестирование часто уходит не меньше времени, чем на создание. Поскольку в наших условиях рассчитывать на то, что тестировать вашу программу будет профессиональный тестер, не приходится, проверять ее придется вам самим. Приведу некоторые советы по тестированию:

попытайтесь запустить программу при работе с большим количеством документов или когда не открыто ни одного документа;

посмотрите, как работает программа, когда окно документа развернуто, свернуто или размер его изменен;

проверьте, как работает программа, когда выделены разные элементы или группы элементов;

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

попробуйте прервать работу программы в самый неподходящий момент и потом вновь запустить ее;

проверьте, как ведет себя программа, когда пропадает сеть, заканчивается свободное место на диске, заканчивается бумага в принтере и т. п.;

проверьте работу программы под разными версиями Office и операционных систем (в том числе англоязычных и локализованных);

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

Если есть возможность, всегда рекомендуется немного поработать, выполняя обязанности пользователя, для которого создается программа.

Мне очень нравится «диверсионный» подход при тестировании программ. Представьте себе, что вы — вредитель и диверсант, у которого цель — вывести программу из строя. Потом опробуйте те способы, которые вам пришли в голову. Если способ оказался удачным, придумайте для него защиту. Как ни удивительно, но реальная работа пользователей с вашей программой будет очень похожа на действия таких диверсантов.

Ошибки, которые могут быть в программе, принято делить на три группы:

ошибки времени выполнения;

Синтаксические ошибки, их также называют ошибками времени компиляции

(Compile-time error), наиболее легко устранимы. Их обнаруживает компилятор, а программисту остается только внести изменения в текст программы и выполнить повторную компиляцию.

Ошибки времени выполнения, они называются исключениями (exception), тоже, как правило, легко устранимы. Они обычно проявляются уже при первых запусках программы и во время тестирования.

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

Что должна сделать программа при ошибках

Пример сообщения об ошибке (программа запущена из среды разработки)

Если программа запущена из операционной системы, то при возникновении исключения на экране также появляется сообщение об ошибке, но тип ошибки в сообщении не указывается (рис. 7.2).

Сообщение об ошибке при запуске программы из операционной системы

С дело обстоит иначе. Компилятор обнаружить их не может. Поэтому даже в том случае, если в программе есть алгоритмические ошибки, компиляция завершается успешно. Убедиться в том, что программа работает правильно и в ней нет алгоритмических ошибок, можно только в процессе программы. Если программа работает не так, как надо, а результат ее работы не соответствует ожидаемому, то, скорее всего, в ней есть алгоритмические ошибки. Процесс поиска алгоритмических ошибок может быть достаточно трудоемким. Чтобы найти алгоритмическую ошибку, программисту приходится анализировать алгоритм, вручную «прокручивать» процесс его выполнения.

Поддержка в различных языкахПравить

Большинство современных языков программирования, такие как Ada, C++, D, Delphi, Objective-C, Java, JavaScript, Eiffel, OCaml, Ruby, Python, Common Lisp, SML, PHP, все языки платформы .NET и др., имеет встроенную поддержку структурной обработки исключений. В этих языках при возникновении исключения, поддерживаемого языком, происходит раскрутка стека вызовов до первого обработчика исключений подходящего типа, и управление передаётся обработчику.

За исключением незначительных различий в синтаксисе существует лишь пара вариантов обработки исключений. В наиболее распространённом из них исключительная ситуация генерируется специальным оператором (throw или raise), а само исключение, с точки зрения программы, представляет собой некоторый объект данных. То есть, генерация исключения состоит из двух этапов: создания объекта-исключения и возбуждения исключительной ситуации с этим объектом в качестве параметра. При этом конструирование такого объекта само по себе выброса исключения не вызывает. В одних языках объектом-исключением может быть объект любого типа данных (в том числе строкой, числом, указателем и так далее), в других — только предопределённого типа-исключения (чаще всего он имеет имя Exception) и, возможно, его производных типов (типов-потомков, если язык поддерживает объектные возможности).

Область действия обработчиков начинается специальным ключевым словом try или просто языковым маркером начала блока (например, begin) и заканчивается перед описанием обработчиков (catch, except, resque). Обработчиков может быть несколько, один за одним, и каждый может указывать тип исключения, который он обрабатывает. Как правило, никакого подбора наиболее подходящего обработчика не производится, и выполняется первый же обработчик, совместимый по типу с исключением. Поэтому порядок следования обработчиков имеет важное значение: если обработчик, совместимый с многими или всеми типами исключений, окажется в тексте прежде специфических обработчиков для конкретных типов, то специфические обработчики не будут использоваться вовсе.

Некоторые языки также допускают специальный блок (else), который выполняется, если ни одного исключения не было сгенерировано в соответствующей области действия. Чаще встречается возможность гарантированного завершения блока кода (finally, ensure). Заметным исключением является Си++, где такой конструкции нет. Вместо неё используется автоматический вызов деструкторов объектов. Вместе с тем существуют нестандартные расширения Си++, поддерживающие и функциональность finally (например в MFC).

В целом обработка исключений может выглядеть следующим образом (в некотором абстрактном языке):

«Строка, считанная из консоли, пустая!»

«Программа выполнилась без исключительных ситуаций»

В некоторых языках может быть лишь один обработчик, который разбирается с различными типами исключений самостоятельно.

Обработчики исключенийПравить

В отсутствие собственного механизма обработки исключений для прикладных программ наиболее общей реакцией на любую исключительную ситуацию является немедленное прекращение выполнения с выдачей пользователю сообщения о характере исключения. Можно сказать, что в подобных случаях единственным и универсальным обработчиком исключений становится операционная система. Например, в операционную систему Windows встроена утилита Dr. Watson, которая занимается сбором информации о необработанном исключении и её отправкой на специальный сервер компании Microsoft.

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

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

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

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

Существует два варианта подключения обработчика исключительных ситуаций к программе: структурная и неструктурная обработка исключений.

Неструктурная обработка исключений

Неструктурная обработка исключений реализуется в виде механизма регистрации функций или команд-обработчиков для каждого возможного типа исключения. Язык программирования или его системные библиотеки предоставляют программисту как минимум две стандартные процедуры: регистрации обработчика и разрегистрации обработчика. Вызов первой из них «привязывает» обработчик к определённому исключению, вызов второй — отменяет эту «привязку». Если исключение происходит, выполнение основного кода программы немедленно прерывается и начинается выполнение обработчика. По завершении обработчика управление передаётся либо в некоторую наперёд заданную точку программы, либо обратно в точку возникновения исключения (в зависимости от заданного способа обработки — с возвратом или без). Независимо от того, какая часть программы в данный момент выполняется, на определённое исключение всегда реагирует последний зарегистрированный для него обработчик. В некоторых языках зарегистрированный обработчик сохраняет силу только в пределах текущего блока кода (процедуры, функции), тогда процедура разрегистрации не требуется. Ниже показан условный фрагмент кода программы с неструктурной обработкой исключений:

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

Структурная обработка исключений требует обязательной поддержки со стороны языка программирования — наличия специальных синтаксических конструкций. Такая конструкция содержит блок контролируемого кода и обработчик (обработчики) исключений. Наиболее общий вид такой конструкции (условный):

Здесь «НачалоБлока» и «КонецБлока» — ключевые слова, которые ограничивают блок контролируемого кода, а «Обработчик» — начало блока обработки соответствующего исключения. Если внутри блока, от начала до первого обработчика, произойдёт исключение, то произойдёт переход на обработчик, написанный для него, после чего весь блок завершится и исполнение будет продолжено со следующей за ним команды. В некоторых языках нет специальных ключевых слов для ограничения блока контролируемого кода, вместо этого обработчик (обработчики) исключений могут быть встроены в некоторые или во все синтаксические конструкции, объединяющие несколько операторов. Так, например, в языке Ада любой составной оператор (begin — end) может содержать обработчик исключений.

«ОбработчикНеобработанных» — это обработчик исключений, которые не соответствуют ни одному из описанных выше в данном блоке. Обработчики исключений в реальности могут описываться по-разному (один обработчик на все исключения, по одному обработчику на каждый тип исключения), но принципиально они работают одинаково: при возникновении исключения находится первый соответствующий ему обработчик в данном блоке, его код выполняется, после чего выполнение блока завершается. Исключения могут возникать как в результате программных ошибок, так и путём явной их генерации с помощью соответствующей команды (в примере — команда «СоздатьИсключение»). С точки зрения обработчиков такие искусственно созданные исключения ничем не отличаются от любых других.

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

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

Блоки с гарантированным завершением

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

Заключённые между ключевыми словами «НачалоБлока» и «Завершение» операторы (основной код) выполняются последовательно. Если при их выполнении не возникает исключений, то затем выполняются операторы между ключевыми словами «Завершение» и «КонецБлока» (код завершения). Если же при выполнении основного кода возникает исключение (любое), то сразу же выполняется код завершения, после чего весь блок завершается, а возникшее исключение продолжает существовать и распространяться до тех пор, пока его не перехватит какой-либо блок обработки исключений более высокого уровня.

Принципиальное отличие блока с гарантированным завершением от обработки — то, что он не обрабатывает исключение, а лишь гарантирует выполнение определённого набора операций перед тем, как включится механизм обработки. Нетрудно заметить, что блок с гарантированным завершением легко реализуется с помощью обычного механизма структурной обработки (для этого достаточно поставить команду генерации исключения непосредственно перед завершением контролируемого блока и правильно написать код обработчика), но наличие отдельной конструкции позволяет сделать код более прозрачным и защищает от случайных ошибок.

  • Log Book With Computer Bug . National Museum of American History. Дата обращения: 26 июля 2019. Архивировано 1 июня 2019 года.
  • Danis, Sharron Ann: «Rear Admiral Grace Murray Hopper». ei.cs.vt.edu (16 февраля 1997). Дата обращения: 20 января 2015. Архивировано 15 июня 2010 года.
  • . Google. Дата обращения: 11 августа 2009. Архивировано 3 февраля 2012 года.
  • . Архивировано 3 февраля 2012 года.
  • Popper, Nathaniel. Knight Capital Says Trading Glitch Cost It $440 Million (англ.), New York Times (2 August 2012). Архивировано 5 октября 2017 года. Дата обращения: 13 ноября 2017.

Что следует понимать под ошибкой?

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *