Теория ошибок. Нестабильности первого рода.

Любые ошибки в программах – это результат нестабильностей.
Как правило, для фатального невыполнения задачи недостаточно
одной нестабильности – они возникают последовательно, одна за
другой, в конечно счете и приводя к невозможности решения задачи.

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

Какие варианты “неправильной” передачи наиболее распространены? Во первых, передаваемые параметры могут являться базовыми элементами (переменные, константы, указатели на массивы и т.п.), во вторых, передаваемые аргументы могут быть сложными структурами или экземплярами классов. В разных случаях возможны разные варианты обработки.

Для базовых элементов наиболее распространёнными вариантами нестабильности первого рода являются: передача граничных или запрещенных входных данных. Простейших вариантов из CRT довольно много: например strcpy(NULL, …) или memcpy() на пересекающиеся области памяти. Практически никакие CRT функции не делают ни ассертирования, ни корректной обработки таких нестабильностей, поэтому падения тут особо часто распространены. Win API вызовы гораздо более защищенные, хотя во многих случаях уронить систему при передаче неправильного указателя довольно легко.

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

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

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

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

То, что нестабильности надо ассертировать – как мне кажется, вообще вопросов не доложно вызывать. Но вот задача выбора простого ассертирования vs проверки и обработки всегда проблематична. Особенно если речь заходит про time-critical функции, которые вызываются много раз на фрейме. В таких случаях приходится анализировать не только саму вызываемую функцию, но еще и рассматривать потенциальные варианты, откуда могли придти невалидные данные.

Вообще если говорить про источник ошибочных данных, то можно выделить следующие основные классы:

  • Ошибочные данные, которые прошли из внешних источников (файл, TCP/IP пакет и т.п.). Сюда же относятся: прерывания, эвенты от внешнего железа и другие асинхронные события;
  • Ошибочные данные, которые были введены пользователем;
  • Ошибочные данные, которые были получены в результате ошибки алгоритма;
  • Ошибочные данные, которые были получены в результате отсутствия обработки вызываемых функций (типа вызвали CreateFile(), не проверили INVALID_HANDLE_VALUE).

При этом первый и второй вариант группируются в понятие “внешний источник”, третий и четвертый – в “ошибки программы” (такие ошибки программ приводят к появлению нестабильностей третьего рода).

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

Лирическое отступление – программы, которые падают при чтении битого файла – не редкость. Но мне известно еще и программы, которые падают при отсутствии в системе звуковой карты или DVD привода!!!

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

Вообще из всех рассмотренных вариантов источников ошибочных данных единственный “приятный” вариант ошибки – это третий пункт (ошибки алгоритма). Поскольку только он потенциально (да и то не всегда) является воспроизводимым при последовательных запусках приложения. Все остальное ошибки как правило глубоко запрятаны в системе и когда именно они станут причиной аварийного завершения программы – никому неизвестно.

Две проблемы с передачей аргументов стоят особняком: это аргументы с обязательным требованием на время жизни и возможность версионирования данных. Первая проблема описана здесь, один из способов решения второй проблемы можно увидеть в WinAPI вариантах – это sizeof(struct) или первое поле с mask на заполненные/используемые поля.

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

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

  • http://fahrain.blogspot.com Fahrain

    А как вы относитесь к вот такому решению? http://blogs.msdn.com/somasegar/archive/2009/02/23/devlabs-code-contracts-for-net.aspx

  • dDIMA

    2 Fahrain
    Бегло пробежался по статье. Понадеялся на то, что это что-то возбуждающе-автоматическое, но кроме того, что это вариант реализации пре/пост-кондишенов, ничего больше не нашел… :(

  • http://fahrain.blogspot.com Fahrain

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