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

      Комментарии к записи Обработка исключений отключены

Главная Форумы Программирование Обработка исключений

В этой теме 0 ответов, 1 участник, последнее обновление  questioner 6 мес., 3 нед. назад.

  • Автор
    Сообщения
  • #2993

    questioner
    Участник

    Исключения (исключительные ситуации, exceptions) являются механизмом обработки ошибок, который пришел на смену возврату кода ошибки функциями. Тем не менее, многие разработчики до сих пор используют возврат кода ошибки вместо исключений — в частности очень многие участники конференции C++ Russia 2015 говорили об этом, включая разработчиков Яндекса.

    В заметке мы рассмотрим:

    1. обработку каких ошибок можно выполнять с использованием исключений;
    2. синтаксические конструкции, введенные в языки для обработки исключений (примерно одинаковые в разных языках программирования — Java, C++, C# и др.).;
    3. рекомендации по использованию исключений (в т.ч. по книгам о чистоте кода);

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

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

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

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

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

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

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

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

    1. если вызывающий код не обрабатывает код ошибки (он ведь не обязан это делать), то он продолжает вычисления, но уже с некорректными данными. Такая ситуация выявится не сразу, ведь вы можете получить некорректный результат или аварийный останов совсем с другом месте. Например, в библиотеке Qt есть функция преобразования строки в целое число:
      int QString::toInt(bool * ok = 0, int base = 10) const
      В случае ошибки она вернет ноль и присвоит значение false аргументу ok (который вообще не является обязательным). Если наш код вызывает эту функцию для строки, не являющейся целым число — то он продолжит выполнение, получив в качестве результата ноль (вполне корректное, но неверное значение);
    2. далеко не всегда удается правильно обработать ошибку непосредственно в коде, вызвавшем функцию. В результате получается примерно следующая ситуация:
      bool foo(QString str) {
        bool ok;
        string.toInt(&ok);
        if (false == ok) {
          return false;
        }
        // ... some actions
      }
      bool bar() {
        QString str;
        // ... some actions
        if (false == foo(str)) 
          return fasle;
        }
      }

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

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

    Синтаксические конструкции механизма обработки исключений

    Фрагмент кода, внутри которого могут вырабатываться исключения должен быть помещен в блок try{}, к которому могут быть добавлены обработчики — блоки catch(ExceptionType) {}. Функция может вырабатывать исключения при помощи оператора throw. Например:

    try {
      socket = connect_to_server(host); // throw BadHostException or ServerNotAvailableException
      send_message(socket, message); // throw ServerNotAvailableException or BadMsgException
    }
    catch(BadHostException exception) {
      // BadHostException hadler
      // can charge other host from user
    }
    catch(ServerNotAvailableException exception) {
      // ServerNotAvailableException handler
      // can reconnect for example
    }
    catch(BadMsgException exception) {
      // BadMsgException hadler
      // can notify user by window
    }
    catch(...) {
      // other types of exceptions hadler
    }
    

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

    Исключения могут организовываться в иерархии посредством наследования. Например, в стандартной библиотеке C++ определен базовый класс std::exception, от которого наследуется std::logic_error (класс логических ошибок), являющий базовым для std::out_of_range. Для фрагмента кода, рассмотренного выше, мог быть выделен класс NetworkException, базовый для BadHostException и ServerNotAvailableException. При этом, если при обработке нам не важно какие именно проблемы с сетью произошли — мы можем написать обработчик для базового класса.

    К блоку try{} может быть написано несколько обработчиков, совместимых с одним и тем же типом исключения. Блоки catch обрабатываются в порядке их перечисления, поэтому будет выбран тот, который описан выше. Например:

    catch(BadHostException exception) {
      // BadHostException hadler
    }
    catch(NetworkException exception) {
      // all network exceptions, but not BadHostException
    }
    catch(ServerNotAvailableException exception) {
      // never call, because ServerNotAvailableException was hanled as NetworkException
    }

    Рекомендации по использованию исключений

    Выше описаны общие (подходящие для большинства языков) принципы обработки исключений. Однако, в каждом языке есть свои тонкости. Так, например, в языке Java есть ключевое слово finally, задающее фрагмент кода, который будет выполнен при возникновении исключения любого вида. В языке C++, например: надо учитывать то, что при обработке throw создается копия исключения; в обработчике исключения запись throw; позволяет передать текущее исключение дальше без копирования; и множество других тонкостей [3]. Во многих языках можно задать спецификацию исключений для функции (используйте спецификации исключений), т.е. список исключений, которые могут выходить из нее — если вдруг какой-либо участок кода начнет вырабатывать новый тип исключения — вы получите ошибку на этапе компиляции. Изучите особенности реализации механизма исключений для своего языка программирования.

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

    Роберт Мартин рекомендует начинать написание функции с try-catch-finally. Это заставит программиста еще раз задумать о том, какие ошибки могут произойти — на самом деле, почти любая функция может столкнуться с ситуациями, в которых не сможет продолжить вычисления [2].

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

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

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

    Тут же, можно вспомнить, что исключения не используются во многих проектах по причинам недостаточной эффективности — например в Яндекс.Браузере. Кроме того, исключения не использовались в библиотеке Qt (по крайней мере 4.х версии) из-за сложности их реализации на некоторых архитектурах. Тот факт, что в настоящее время библиотека Qt сейчас использует механизм исключений означает, что его поддерживает подавляющее большинство компиляторов С++ под самые разные платформы.

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

    Литература по теме обработки исключений:

    1. Б. Страуструп Язык программирования С++. Специальное издание. Пер. с англ. – М.: Издательство Бином, 2011 г. – 1136 с.
    2. Мартин Р. Чистый код. Создание, анализ и рефакторинг. Библиотека программиста. – СПб.: Питер, 2014. – 464 с.
    3. Мейерс С. Эффективное использование С++. 35 новых рекомендаций по улучшению ваших программ и проектов. – М.: ДМК Пресс, 2014. – 294с.

Для ответа в этой теме необходимо авторизоваться.