- PVSM.RU - https://www.pvsm.ru -

Крэши, вызванные исключениями

На прошлой неделе я вместе с несколькими моими коллегами учавствовал в громкой речи о том факте, что Go обрабатывает ошибки в ожидаемых сценариях посредством возвращения кода ошибки вместо использования исключений или другого схожего механизма. Это довольно спорная тема, потому что люди привыкли избегать ошибки с помощью исключений, а Go возвращает улучшенную версию [1] хорошо известной модели, ранее принятой несколькими языками — включая C — при которой ошибки передаются через возвращаемые значения. Это значит, что ошибки маячат перед глазами программиста и вынуждают иметь с ними дело все время. Кроме того, спор переходит в направление того факта, что в языках с исключениями каждая ошибка безо всяких дополнительных действий несет в себе полную информацию о том, что и где произошло, а это может быть полезно в некоторых случаях.

Однако все эти удобства имею стоимость, которую легко сформулировать:
Исключения учат разработчиков не заботиться об ошибках.

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

Реймонд Чен так описал эту проблему в 2004 [2]:

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

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

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

Я не первый раз пишу об этом и, учитывая споры, которые окружают это заявление, поэтому я нашел пару примеров, которые подтверждают проблему. Лучший пример, который я смог найти на сегодняшний день находится в модуле pty [3] стандартной библиотеки 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 [4] года, когда Гвидо ван Россум впервые закоммитил ее.

Вот другой интересный пример:

$ 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)

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

В следствие этого, к сожалению, мы погружаемся в мир хрупкого программного обеспечения и розовых слонов. Хотя более выразительный стиль возвращения ошибок выстраивает правильное мышление [5]: вернет ли функция или метод ошибку в результате? Как она будет обработана? Действительно ли функция взаимодействующая с системой не вернет ошибку? Как решается проблема, которая наверняка может возникнуть?

Удивительное количество крэшэй и просто непредсказуемое поведение является результатом такой непроизвольной небрежности.

Оригинал [6]

Автор: obyknovenius

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/perevod/33166

Ссылки в тексте:

[1] улучшенную версию: http://golang.org/doc/articles/error_handling.html

[2] в 2004: http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx

[3] pty: http://docs.python.org/3/library/pty.html

[4] с 1994: http://hg.python.org/cpython/rev/8c9cc054c5ed#l1.99

[5] мышление: http://www.braintools.ru

[6] Оригинал: http://blog.labix.org/2013/04/23/exceptional-crashes

[7] Источник: http://habrahabr.ru/post/178083/