На прошлой неделе я вместе с несколькими моими коллегами участвовал в громкой речи о том факте, что Go обрабатывает ошибки в ожидаемых сценариях посредством возвращения кода ошибки вместо использования исключений или другого схожего механизма. Это довольно спорная тема, потому что люди привыкли избегать ошибки с помощью исключений, а Go возвращает улучшенную версию хорошо известной модели, ранее принятой несколькими языками - включая C - при которой ошибки передаются через возвращаемые значения. Это значит, что ошибки маячат перед глазами программиста и вынуждают иметь с ними дело все время. Кроме того, спор переходит в направление того факта, что в языках с исключениями каждая ошибка безо всяких дополнительных действий несет в себе полную информацию о том, что и где произошло, а это может быть полезно в некоторых случаях.
Однако все эти удобства имею стоимость, которую легко сформулировать:
Исключения учат разработчиков не заботиться об ошибках.
Печальным следствием является то, что это актуально, даже если Вы блестящий разработчик, так как на Вас оказывает влияние окружающий мир, который снисходителен к ошибкам. Проблема проявится в библиотеках, которые Вы импортируете, в приложениях, установленных на ваш компьютер, а также на серверах, которые хранят ваши данные.
Реймонд Чен так описал эту проблему в 2004:
Написание корректного кода в модели с выбрасыванием исключений в некотором смысле труднее, чем в модели с возвращением кода ошибки, так как что угодно может потерпеть неудачу и Вы должны быть готовы к этому. В модели с возвращением кода ошибки, момент когда вы должны произвести проверку на наличие ошибок очевиден: как только вы получили код ошибки. В модели с исключениями Вы просто должны знать, что ошибки могут произойти в любом месте.
Другими словами, в модели с возвращением кода ошибки, когда кто-то пропускает обработку ошибки это происходит явно: они не проверяют код ошибки. В то же время в модели с выбрасыванием исключений при рассмотрении кода, в котором кто-то обрабатывает ошибку все не так ясно, так как ошибка не указана явно.
(…)
Когда Вы пишете код, задумываетесь ли Вы о том, каковы могут быть последствия каждого исключения, которое может возникнуть в каждой строчке кода? Вы должны делать это, если собираетесь писать корректный код.
Это абсолютно верно. Каждая строка, которая может вызвать исключение несет скрытую ветку "else" для ошибочного сценария, о которой очень легко забыть. Даже если внедрение кода для обработки ошибок кажется бессмысленным повторением, его написание заставляет разработчиков помнить об альтернативном сценарии, и довольно часто этот код оказывается не пустым.
Я не первый раз пишу об этом и, учитывая споры, которые окружают это заявление, поэтому я нашел пару примеров, которые подтверждают проблему. Лучший пример, который я смог найти на сегодняшний день находится в модуле pty стандартной библиотеки Python 3.3:
def spawn(argv, master_read=_read, stdin_read=_read):
"""Create a spawned process."""
if type(argv) == type(''):
argv = (argv,)
pid, master_fd = fork()
if pid == CHILD:
os.execlp(argv[0], *argv)
(...)
Каждый раз, когда кто-нибудь вызовет этот код с неправильным именем исполняемого файла в argv, будет порожден неиспользуемый, не подверженный сборки мусора и неизвестный приложению Python процесс, потому что execlp потерпит неудачу и форкнутый процесс будет проигнорирован. И будет ли клиент этого модуля ловить исключение или нет не имеет значения. Локальное обязательство не было выполнено. Конечно ошибка может быть исправлена тривиально добавлением try/except внутрь самой функции spawn. Однако, проблема в том, что это логика показалась нормальной всем, кто когда-либо видел эту функцию начиная с 1994 года, когда Гвидо ван Россум впервые закоммитил ее.
Вот другой интересный пример:
$ make clean
Sorry, command-not-found has crashed! Please file a bug report at:
https://bugs.launchpad.net/command-not-found/+filebug
Please include the following information with the report:
command-not-found version: 0.3
Python version: 3.2.3 final 0
Distributor ID: Ubuntu
Description: Ubuntu 13.04
Release: 13.04
Codename: raring
Exception information:
unsupported locale setting
Traceback (most recent call last):
File "/.../CommandNotFound/util.py", line 24, in crash_guard
callback()
File "/usr/lib/command-not-found", line 69, in main
enable_i18n()
File "/usr/lib/command-not-found", line 40, in enable_i18n
locale.setlocale(locale.LC_ALL, '')
File "/usr/lib/python3.2/locale.py", line 541, in setlocale
return _setlocale(category, locale)
locale.Error: unsupported locale setting
Это довольно серьезный крэш из-за отсутствия данных о локали в системном приложении, которое, по иронии судьбы, должно сообщать пользователям, какие пакеты надо установить, если команда отсутствует. Заметьте, что на вершине сетка ссылка на crash_guard. Это функция предназначена для перехвата всех исключений на краю стека и отображении детальной системной информации и трейсбека, чтобы помочь в решении проблемы.
Такой "парашютный перехват" довольно распространен в исключение-оринетриованном программировании и это подход, как правило, дает разработчикам ложное чувство хорошей обработки ошибок в приложении. Вместо настоящей защиты приложения он становится просто удобным способом крэша. В данном случае, правильнее было бы вывести предупреждение, если это вообще необходимо, и позволить программе работать как обычно. Это можно было бы сделать простым оборачиванием вот этой строки:
try:
locale.setlocale(locale.LC_ALL, '')
except Exception as e:
print("Cannot change locale:", e)
Очевидно, это легко сделать. Но, опять же, проблема в том, что это было естественно не делать этого сразу. На самом деле, это более чем естественно: действительно кажется лучше не рассматривать ошибочный путь. В этом случае произойдет сокращение кода, он будет более прямолинейным, и в результате остается только тот, который приводит к желаемому результату.
В следствие этого, к сожалению, мы погружаемся в мир хрупкого программного обеспечения и розовых слонов. Хотя более выразительный стиль возвращения ошибок выстраивает правильное мышление: вернет ли функция или метод ошибку в результате? Как она будет обработана? Действительно ли функция взаимодействующая с системой не вернет ошибку? Как решается проблема, которая наверняка может возникнуть?
Удивительное количество крэшэй и просто непредсказуемое поведение является результатом такой непроизвольной небрежности.
Комментариев нет:
Отправить комментарий