Легко поддаться умонастроению «этого не может быть, потому что не может быть никогда». Большинство из нас создавало программы, которые не проверяют, успешно ли завершилась операция закрытия файла и правильно ли записан оператор трассировки. И все сводилось к одному (к тому, что мы и так знали) – рассматриваемая программа не откажет, если будет работать в нормальных условиях. Но мы пишем программы с осторожностью. Мы ищем инородные указатели в других частях нашей программы, очищая стек. Мы выясняем, какие версии библиотек совместного пользования загружались в действительности.
Все ошибки дают вам информацию. Вы могли внушить себе, что ошибка произойти не может, и проигнорировать эту информацию. В отличие от вас, прагматики говорят себе, что если ошибка имеет место, то произошло что-то очень скверное.
Подсказка 32: Пусть аварийное завершение работы программы произойдет как можно раньше
Аварийное завершение не означает «отправить в корзину для мусора»
Одним из преимуществ скорейшего обнаружения проблем является то, что аварийное завершение происходит как можно раньше. И во многих случаях такое завершение программы – это лучший выход из положения. Альтернативой может быть продолжение работы, запись поврежденных данных в жизненно важную базу данных или команда стиральной машине на проведение двадцатого по счету цикла отжима.
Эта философия воплощена в языке и библиотеках Java. Когда в системе выполнения случается что-то непредвиденное, происходит возбуждение исключения RuntimeException. Если это исключение не перехвачено, оно будет двигаться на верхний уровень программы и заставит ее прекратить работу, отобразив трассировку стека.
То же самое можно реализовать и на других языках программирования. Если механизм исключения отсутствует или библиотеки не возбуждают исключения, то убедитесь в том, что можете обрабатывать ошибки самостоятельно. В языке С для этого весьма полезны макрокоманды:
#define CHECK(LINE, EXPECTED) \
{int rc = LINE; \
if (rc!= EXPECTED) \
ut_abort(_FILE_, _LINE_, #LINE, rc, EXPECTED); }
void ut_abort(char *file, int In, char *line, int rc, int exp) {
fprintf(stderr, «%s line %d\n'%s': expected %d, got %d\n», file, In, line, exp, rc);
exit(1);
}
Тогда вы можете инкапсулировать вызовы, которые никогда подведут, с помощью строки:
CHECK(stat("/tmp», &stat_buff), 0);
Если бы это не удалось, то вы бы получили сообщение, записанное в stderr:
source.c line 19
"stat("/tmp», &stat_buff)' : expected 0, got -1
Ясно, что в ряде случаев выход из выполняющейся программы просто не уместен. Возможно, вы претендуете на ресурсы, которые не освобождены, или же вам необходимо записать сообщения в журнал, завершить открытые транзакции или взаимодействовать с другими процессами. Здесь будут полезны методики, обсуждаемые в разделе «Случаи, когда необходимо использовать исключения». Однако основной принцип остается тем же – если ваша программа обнаруживает, что произошло событие, которое считалось невозможным, программа теряет жизнеспособность. Начиная с этого момента, все действия, совершаемые программой, попадают под подозрение, так что выполнение программы необходимо прервать как можно быстрее. В большинстве случаев мертвая программа приносит намного меньше вреда, чем испорченная.
• Проектирование по контракту
• Когда использовать исключения
23
Программирование утверждений
В самобичевании есть своего рода сладострастие. И когда мы сами себя виним, мы чувствуем, что никто другой не вправе более винить нас.