Исключение не обработано visual studio c

Обновлено: 06.07.2024

Что такое исключение? Это ситуация, которая не предусмотрена стандартным поведением программы. Например, попытка доступа к элементу в классе Vector (который мы разбирали в статье про классы ), который не существует. То есть происходит выход за пределы вектора. В данном случае можно воспользоваться исключениями, чтобы прервать выполнение программы. Это необходимо потому, что

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

Для разрешения таких ситуация в C++ можно использовать технику исключений.

Рассмотрим, как написать вызов исключения в случае попытки доступа к элементу по индексу, который не существует в классе Vector.

Здесь применяется исключение out_of_range. Данное исключение определено в заголовочном файле <stdexcept >.

Оператор throw передаёт контроль обработчику для исключений типа out_of_range в некоторой функции, которая прямо или косвенно вызывает Vector::operator[]() . Для того, чтобы обработать исключения необходимо воспользоваться блоком операторов try catch.

Инварианты

Также блоки try catch позволяют производить обработку нескольких различных исключений, что вносит инвариантность в работу механизма исключений C++.

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

Данный конструктор может выбросить исключение в двух случаях:

  • Если в качестве аргумента size будет передано отрицательное значение
  • Если оператор new не сможет выделить память

length_error - это стандартный оператор исключений, поскольку библиотека std часто использует данные исключения при своей работе.

Обработка исключений будет выглядеть следующим образом:

Также можно выделить свои собственные исключения.

Виды исключений

Все исключения стандартной библиотеки наследуются от std::exception.

На данный момент существуют следующие виды исключений:

  • logic_error
    • invalid_argument
    • domain_error
    • length_error
    • out_of_range
    • future_error (C++11)
    • range_error
    • overflow_error
    • underflow_error
    • system_error (C++11)
      • ios_base::failure (начиная с C++11)
      • bad_array_new_length (C++11)

      std::logic_error

      Исключение определено в заголовочном файле <stdexcept>

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

      Этот класс используется как основа для ошибок, которые могут быть определены только во время выполнения программы.

      std::invalid_argument

      Исключение определено в заголовочном файле <stdexcept>

      Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в случае неправильного аргумента.

      Например, на MSDN приведён пример, когда в объект класса bitset из стандартной библиотеки

      В данном примере передаётся неправильная строка, внутри которой имеется символ 'b', который будет ошибочным.

      std::domain_error

      Исключение определено в заголовочном файле <stdexcept>

      Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в случае если математическая функция не определена для того аргумента, который ей передаётся, например:

      std::length_error

      Исключение определено в заголовочном файле <stdexcept>

      Наследован от std::logic_error. Определяет исключение, которое должно быть броше в том случае, когда осуществляется попытка реализации превышения допустим пределов для объекта. Как это было показано для размера вектора в начале статьи.

      std::out_of_range

      Исключение определено в заголовочном файле <stdexcept>

      Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в том случае, когда происходит выход за пределы допустимого диапазона значений объекта. Как это было показано для диапазона значений ветора в начале статьи.

      std::future_error

      Исключение определено в заголовочном файле <future>

      Наследован от std::logic_error. Данное исключение может быть выброшено в том случае, если не удалось выполнить функцию, которая работает в асинхронном режиме и зависит от библиотеки потоков. Это исключение несет код ошибки совместимый с std::error_code .

      std::runtime_error

      Исключение определено в заголовочном файле <stdexcept>

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

      std::range_error

      Исключение определено в заголовочном файле <stdexcept>

      Исключение используется при ошибках при вычислении значений с плавающей запятой, когда компьютер не может обработать значение, поскольку оно является либо слишком большим, либо слишком маленьким. Если значение является значение интегрального типа, то должны использоваться исключения underflow_error или overflow_error .

      std::overflow_error

      Исключение определено в заголовочном файле <stdexcept>

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

      std::underflow_error

      Исключение определено в заголовочном файле <stdexcept>

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

      std::system_error

      Исключение определено в заголовочном файле <system_error>

      std::system_error - это тип исключения, которое вызывается различными функциями стандартной библиотеки (как правило, функции, которые взаимодействуют с операционной системой, например, конструктор std::thread ), при этом исключение имеет соответствующий std::error_code .

      std::ios_base::failure

      Исключение определено в заголовочном файле <ios>

      Отвечает за исключения, которые выбрасываются при ошибках функций ввода вывода.

      std::bad_typeid

      Исключение определено в заголовочном файле <typeinfo>

      Исключение этого типа возникает, когда оператор typeid применяется к нулевому указателю полиморфного типа.

      std::bad_cast

      Исключение определено в заголовочном файле <typeinfo>

      Данное исключение возникает в том случае, когда производится попытка каста объекта в тот тип объекта, который не входит с ним отношения наследования.

      std::bad_weak_ptr

      Исключение определено в заголовочном файле <memory>

      std::bad_weak_ptr – тип объекта, генерируемый в качестве исключения конструкторами std::shared_ptr , которые принимают std::weak_ptr в качестве аргумента, когда std::weak_ptr ссылается на уже удаленный объект.

      std::bad_function_call

      Исключение определено в заголовочном файле <functional>

      Данное исключение генерируется в том случае, если был вызван метод std::function::operator() объекта std::function , который не получил объекта функции, то есть ему был передан в качестве инициализатора nullptr, например, а объект функции так и не был передан.

      std::bad_alloc

      Исключение определено в заголовочном файле <new>

      Вызывается в том случае, когда не удаётся выделить память.

      std::bad_array_new_length

      Исключение определено в заголовочном файле <new>

      Исключение вызывается в следующих случаях:

      1. Массив имеет отрицательный размер
      2. Общий размер нового массива превысил максимальное значение, определяемое реализацией
      3. Количество элементов инициализации превышает предлагаемое количество инициализирующих элементов

      std::bad_exception

      Исключение определено в заголовочном файле <exception>

      std::bad_exception - это тип исключения в C++, которое выполняется в следующих ситуациях:

      Исключение указывает на состояние ошибки, возникающее при выполнении программы. Можно указать отладчику, какие исключения или наборы исключений должны вызывать прерывание и в какой момент нужно прервать выполнение (то есть приостановить отладчик). Когда отладчик прерывает работу, он показывает, где было создано исключение. Кроме того, можно добавлять или удалять исключения. После открытия решения в Visual Studio в разделе Отладка > Windows > Параметры исключений откройте окно Параметры исключений.

      • Создается исключение, которое не обрабатывается.
      • Отладчик настроен на прерывание выполнения до вызова обработчика.
      • Задан параметр Только мой код, и отладчик настроен на прерывание по любому исключению, не обрабатываемому в коде пользователя.

      В приложениях, написанных на Visual Basic, отладчик управляет всеми ошибками как исключениями, даже при использовании обработчиков ошибок типа On Error.

      Настройка отладчика для прерывания выполнения при создании исключения

      Отладчик может прервать выполнение приложения в точке возникновения исключения, чтобы вы могли проверить исключение еще до вызова обработчика.

      В окне Параметры исключений (Отладка > Windows > Параметры исключений) разверните узел для категории исключений, например Исключения среды CLR. Затем установите флажок для конкретного исключения в этой категории, например System.AccessViolationException. Можно также выбрать всю категорию исключений.

      Отмечено AccessViolationException

      Для поиска конкретных исключений можно воспользоваться окном Поиск на панели инструментов Параметры исключений или применить функцию поиска для фильтрации определенных пространств имен (например, System.IO).

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

      Если исключение AccessViolationException отмечено в окне Параметры исключений, при выполнении этого кода в режиме отладчика произойдет останов на строке throw . После этого выполнение можно продолжить. В консоли должны отображаться обе строки.

      Но в ней не отображается строка here .

      Далее приводится метод Main() консольного приложения:

      Если исключение AccessViolationException отмечено в окне Параметры исключений, при выполнении этого кода в режиме отладчика произойдет останов на строке throw в методах ThrowHandledException() и ThrowUnhandledException() .

      Чтобы восстановить параметры исключений до значений по умолчанию, выберите Восстановить для списка параметры по умолчанию:

      Восстановить параметры исключения до значений по умолчанию

      Настройка отладчика для возобновления выполнения при возникновении не обработанных пользователем исключений

      В окне Параметры исключений откройте контекстное меню, щелкнув правой кнопкой мыши метку столбца, а затем выберите Показать столбцы > Дополнительные действия. (Если параметр Только мой код отключен, данная команда не отображается.) Отобразится третий столбец с именем Дополнительные действия.

      Столбец "Дополнительные действия"

      Для исключения, у которого отображается Продолжить, если не обрабатывается в пользовательском коде в этом столбце, отладчик продолжает работу, если это исключение не обрабатывается в пользовательском коде, но обрабатывается в другом месте.

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

      Параметр Продолжить, если не обрабатывается в пользовательском коде

      Прервать для необрабатываемых пользователем исключений

      Добавление и удаление исключений

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

      В окне Параметры исключений выберите одну из категории исключений (например, Среда CLR).

      Введите имя исключения (например, System.UriTemplateMatchException).

      Введите имя исключения

      Исключение будет добавлено в список (в алфавитном порядке) и будет автоматически выбрано.

      Чтобы добавить исключение в категории "Исключения доступа к памяти GPU", "Исключения среды выполнения JavaScript" или "Исключения Win32", необходимо включить код ошибки, а также описание.

      Проверьте правильность написания! В окне Параметры исключений не проверяется существование добавленного исключения. Поэтому при вводе Sytem.UriTemplateMatchException появится запись для этого исключения (а не для System.UriTemplateMatchException).

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

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

      добавление общего исключения

      Добавление условий в исключение

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

      Добавление условий в исключение поддерживается, начиная с Visual Studio 2017.

      Чтобы добавить условные исключения, выполните следующие действия.

      Условия для исключения

      Чтобы добавить дополнительное условие к исключению, выберите Добавить условие. Отобразятся строки дополнительные условий.

      Дополнительные условия для исключения

      Для каждой строки условия введите имя модуля и измените список операторов сравнения на Равно или Не равно. Можно указать подстановочные знаки ( \* ) в имени, чтобы выбрать более одного модуля.

      Если необходимо удалить условие, выберите X в конце строки условия.

      При использовании блока try. catch..finally вначале выполняются все инструкции в блоке try . Если в этом блоке не возникло исключений, то после его выполнения начинает выполняться блок finally . И затем конструкция try..catch..finally завершает свою работу.

      Если же в блоке try вдруг возникает исключение, то обычный порядок выполнения останавливается, и среда CLR начинает искать блок catch , который может обработать данное исключение. Если нужный блок catch найден, то он выполняется, и после его завершения выполняется блок finally.

      Если нужный блок catch не найден, то при возникновении исключения программа аварийно завершает свое выполнение.

      Рассмотрим следующий пример:

      В данном случае происходит деление числа на 0, что приведет к генерации исключения. И при запуске приложения в режиме отладки мы увидим в Visual Studio окошко, которое информирует об исключении:

      В этом окошке мы видим, что возникло исключение, которое представляет тип System.DivideByZeroException , то есть попытка деления на ноль. С помощью пункта View Details можно посмотреть более детальную информацию об исключении.

      И в этом случае единственное, что нам остается, это завершить выполнение программы.

      Чтобы избежать подобного аварийного завершения программы, следует использовать для обработки исключений конструкцию try. catch. finally . Так, перепишем пример следующим образом:

      В данном случае у нас опять же возникнет исключение в блоке try, так как мы пытаемся разделить на ноль. И дойдя до строки

      выполнение программы остановится. CLR найдет блок catch и передаст управление этому блоку.

      После блока catch будет выполняться блок finally.

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

      Следует отметить, что в этой конструкции обязателен блок try . При наличии блока catch мы можем опустить блок finally:

      И, наоборот, при наличии блока finally мы можем опустить блок catch и не обрабатывать исключение:

      Обработка исключений и условные конструкции

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

      Если пользователь введет не число, а строку, какие-то другие символы, то программа выпадет в ошибку. С одной стороны, здесь как раз та ситуация, когда можно применить блок try..catch , чтобы обработать возможную ошибку. Однако гораздо оптимальнее было бы проверить допустимость преобразования:

      Метод Int32.TryParse() возвращает true , если преобразование можно осуществить, и false - если нельзя. При допустимости преобразования переменная x будет содержать введенное число. Так, не используя try. catch можно обработать возможную исключительную ситуацию.

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

      Ни одна серьезная программа не может обойтись без собственных обработчиков исключений, так как во время выполнения программы рано или поздно могут случиться разные «неожиданности». Например, пользователь введет неверные данные и компьютеру придется выполнять деление на ноль. Или исчезнет куда-нибудь файл, необходимый для работы программы. Конечно, ничего страшного не произойдет, так как операционная система обрабатывает подобные неприятности. Но программа при этом завершится аварийно, а это уже грозит не только потерей несохраненных данных, но и серией нецензурных выражений в адрес программиста со стороны пользователя. А это как раз тот редкий случай, когда пользователь абсолютно прав. Поэтому любой мало-мальски грамотный программист должен предусмотреть возможное появление ошибок в ходе выполнения программы и принять соответствующие меры.

      Эта статья посвящена обработке исключений. В качестве примеров используются языковые конструкции Visual C++ 6.5.

      1. Фреймовая обработка исключений

      1.1. Исключения и их обработчики

      Исключение – это событие, которое произошло во время выполнения программы, в результате совершения которого дальнейшее нормальное выполнение программы становится невозможным. Обычно такие события происходят из-за ошибок в программе или неправильных действий пользователя (впрочем, хороший программист не может рассчитывать на то, что пользователь всегда будет действовать правильно). После возникновения исключения требуется привести программу в рабочее состояние или выполнить её аварийное завершение с освобождением всех ресурсов, которые использовались программой.

      Для выполнения описанных выше действий в операционных системах Windows предназначен механизм структурной обработки исключений (structured exception handling, SHE). Работает это так. В программе выделяется блок программного кода, где может произойти исключение. Этот блок кода называется фреймом, а сам код называется охраняемым кодом. После фрейма вставляется программный блок, где обрабатывается исключение. Этот блок называется обработчиком исключения. Когда исключение будет обработано, управление передается первой инструкции, которая следует за обработчиком исключения.

      • EXCEPTION_EXECUTE_HANDLER – управление передается обработчику исключений;
      • EXCEPTION_CONTINUE_SEARCH – система продолжает поиск обработчика исключения;
      • EXCEPTION_CONTINUE_EXECUTION – система передает управление в точку прерывания программы.

      Переменные, объявленные внутри фрейма или блока обработки исключения, являются локальными и видны только внутри соответствующего блока, как это принято в С++. Пример обработки исключения:

      1.2. Как получить код исключения

      • EXCEPTION_ACCESS_VIOLATION – попытка чтения или записи в виртуальную память без соответствующих прав доступа;
      • EXCEPTION_BREAKPOINT – встретилась точка останова;
      • EXCEPTION_DATATYPE_MISALIGNMENT – доступ к данным, адрес которых не выровнен по границе слова или двойного слова;
      • EXCEPTION_SINGLE_STEP – механизм трассировки программы сообщает, что выполнена одна инструкция;
      • EXCEPTION_ARRAY_BIUNDS_EXCEEDED – выход за пределы массива, если аппаратное обеспечение поддерживает такую проверку;
      • EXCEPTION_FLT_DENORMAL_OPERAND – один из операндов с плавающей точкой является ненормализованным;
      • EXCEPTION_FLT_DIVIDE_BY_ZERO – попытка деления на ноль в операции с плавающей точкой;
      • EXCEPTION_FLT_INEXACT_RESULT – результат операции с плавающей точкой не может быть точно представлен десятичной дробью;
      • EXCEPTION_FLT_INVALID_OPERATION – ошибка в операции с плавающей точкой, для которой не предусмотрены другие коды исключения;
      • EXCEPTION_FLT_OVERFLOW – при выполнении операции с плавающей точкой произошло переполнение;
      • EXCEPTION_FLT_STACK_CHECK – переполнение или выход за нижнюю границу стека при выполнении операции с плавающей точкой;
      • EXCEPTION_FLT_UNDERFLOW – результат операции с плавающей точкой является числом, которое меньше минимально возможного числа с плавающей точкой;
      • EXCEPTION_INT_DIVIDE_BY_ZERO – попытка деления на ноль при операции с целыми числами;
      • EXCEPTION_INT_OVERFLOW – при выполнении операции с целыми числами произошло переполнение;
      • EXCEPTION_PRIV_INSTRUCTION – попытка выполнения привилегированной инструкции процессора, которая недопустима в текущем режиме процессора;
      • EXCEPTION_NONCONTINUABLE_EXCEPTION – попытка возобновления исполнения программы после исключения, которое запрещает выполнять такое действие.

      1.3. Функции фильтра

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

      Пример программы, в которой используется функция фильтра для принятия решения о дальнейшей обработке исключения, приведён ниже. Здесь функция фильтра (ff) возвращает одно из двух значений EXCEPTION_CONTINUE_EXECUTION или EXCEPTION_EXECUTE_HANDLER. Первое значение возвращается в том случае, если исключение генерируется системой при целочисленном делении на ноль, а второе – в остальных случаях. При попытке деления на ноль происходит исключение и в качестве выражения-фильтра применяется результат выполнения функции ff. Эта функция проверяет, чем было вызвано исключение, и если это деление на ноль, то ошибка исправляется (а = 10). Затем функция возвращает значение EXCEPTION_CONTINUE_EXECUTION, то есть программа продолжает свою работу, но уже с исправленным значением переменной a. Если же это исправление не сделать, то программа войдет в бесконечный цикл.

      1.4. Необработанные исключения

      • EXCEPTION_CONTINUE_SEARCH – передать управление отладчику приложения;
      • EXCEPTION_EXECUTE_HANDLER – передать управление обработчику исключений.
      • EXCEPTION_EXECUTE_HANDLER – выполнение программы прекращается;
      • EXCEPTION_CONTINUE_EXECUTION – возобновить исполнение программы с точки исключения;
      • EXCEPTION_CONTINUE_SEARCH – выполняется системная функция UnhandledExceptionFilter.

      1.5. Обработка исключений при операциях с плавающей точкой

      По умолчанию система отключает все исключения с плавающей точкой. Поэтому если при выполнении операции с плавающей точкой было получено число, которое не входит в диапазон представления чисел с плавающей точкой, то в результате система вернет NAN или INFINITY в случае слишком малого или слишком большого числа соответственно. Чтобы включить режим генерации исключений с плавающей точкой нужно изменить состояние слова, управляющего обработкой операций с плавающей точкой. Это можно сделать при помощи функции _controlfp, которая имеет следующий прототип: Прототип определен в заголовочном файле float.h. Эта функция возвращает старое слово, управляющее обработкой исключений. Параметр new задает новое управляющее слово, а параметр mask должен принимать значение _MCW_EM. Если значение этого параметра равно 0, то функция возвращает старое управляющее слово.

      • _EM_INVALID – исключение EXCEPTION_FLT_INVALID_OPERATION;
      • _EM_DENORMAL – исключение EXCEPTION_FLT_DENORMAL_OPERAND;
      • _EM_ZERODIVIDE – исключение EXCEPTION_FLT_DIVIDE_BY_ZERO;
      • _EM_OVERFLOW – исключение EXCEPTION_FLT_OVERFLOW;
      • _EM_UNDERFLOW – исключение EXCEPTION_FLT_UNDERFLOW;
      • _EM_INEXACT – исключение EXCEPTION_FLT_INEXACT_RESULT.

      Ниже приведен пример программы, которая обрабатывает исключение с плавающей точкой при делении на ноль. Все это прекрасно работает в консольных приложениях, а вот добиться нормальной работы обработчика исключений с плавающей точкой в MFC-приложениях мне так и не удалось. Можно, конечно, заменить системный обработчик исключений, как это описано в п.1.4, но тогда программа будет завершаться при возникновении исключения (хотя и не аварийно, то есть без надоевшего всем вопроса Windows XP об отправке отчета в компанию Microsoft). В общем, пришлось мне для обработки исключений с плавающей точкой воспользоваться еще одним способом. Этот способ более трудоемок, зато работает. Хотя во многих случаях проще проверять значения с помощью оператора if, не прибегая к «хитромудрым» способам обработки исключений в Visual C++.

      1.6. Использование блоков try и catch

      2. Финальная обработка исключений

      2.1. Финальные блоки фрейма

      В операционных системах Windows существует еще один способ обработки исключений. При этом способе код, где возможно возникновение исключения, также заключается в блок __try. Но теперь за этим блоком следует блок __finally. В таком случае блок __finally выполняется всегда – независимо от того, произошло исключение или нет. Такой способ обработки исключений называется финальная обработка исключений. Структурно финальная обработка выглядит следующим образом: Финальная обработка исключений используется для того, чтобы при любом исходе исполнения блока __try освободить ресурсы (память, файлы и т.п.), которые были захвачены внутри этого блока.

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

      2.2. Проверка завершения фрейма

      • Нормальное завершение блока.
      • Выход из блока при помощи управляющей инструкции __leave.
      • Выход из блока при помощи одной из управляющих инструкций return, break, continue или goto.
      • Передача управления обработчику исключения.

      Чтобы определить, как завершился блок __try, используется функция AbnormalTermination, которая имеет следующий прототип: В случае если блок __try завершился ненормально, эта функция возвращает ненулевое значение, иначе – значение FALSE. Используя эту функцию, ресурсы, захваченные в блоке __try, можно освобождать в зависимости от ситуации. Пример:


      Я каждый день пишу код на сишарпе, и натыкаюсь на одну проблему: я трачу кучу времени на то, чтобы решить, как быть, если что-то идёт не по плану.

      Эти размышления меня измучили, и я систематизировал свои знания и идеи по обработке исключительных случаев.

      Возьму простой пример. Допустим у нас есть сервис, который отдаёт нам модель юзера по Id.

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

      Давайте посмотрим, какие у нас есть варианты.

      Тут все просто. Метод ищет пользователя, если, не находит — выплевывает исключение.


      Пользоваться таким методом можно вот так:


      Плюсы подхода очевидны.

      • Никаких гарантий. Вообще никаких. Человек который использует твой сервис, может даже и не подумать о том, что тут что-то надо обрабатывать. Или обработает не все возможные. Если их не обработают на месте, а обработают выше, в том коде, который на такие исключения не рассчитан — могут возникнуть достаточно большие проблемы.
      • Тебе придется скрупулёзно писать и обновлять документацию такого метода, и компилятор тебе не будет гарантировать, что ты описал всё, что выплевываешь.
      • Очень плохо подходит для случаев, когда вызывающей стороне не нужно знать, почему произошла проблема.
      • В доменном коде тебе постоянно нужно будет добавлять свои типы исключений, они раздувают кодовую базу, часто дублируются и требуют поддержки.
      • Когда метод потенциально выплевывает пять шесть типов исключений, код превращается в нечитаемое говно — и код метода, и код использования.
      • В сишарпе принято использовать интерфейсы. Если есть какой-то сервис, и есть код, который его использует, то мы в этом коде работаем с интерфейсом сервиса. Так вот выкидывается исключение или нет — интерфейс это не определяет НИКАК. Т.е. если кто-то написал класс, который имплементит такой-то интерфейс, и внутри этого класса он выкидывает исключение, и он даже внес это в доку класса — я при использовании этого кода об этом НЕ УЗНАЮ.

      Ещё один распространненый способ разруливать это — try pattern.

      Идея завязана на out параметры в сишарпе. Выглядит вот так:


      Если все норм, мы возвращаем true, и присваиваем out переменной user найденное значение. Если не норм, отдаём false, а out переменную заполняем дефолтным значением (в случае с классом это будет null).

      Использовать такой метод следует так:


      У подхода много плюсов:

      • Он идиоматичен. Такая конвенция знакома всем шарпистам, она используется в родных коллекциях, все знаю, как с этим работать.
      • Способ надежен. Просто проигнорировать возможную проблему не получится. Ведь метод возвращает bool, и пользователь кода вынужден будет обратить внимание на твою задумку.
      • Код использования выглядит достаточно лаконично и понятно.
      • Отлично подходит для ситуаций, когда причина возможной неудачи очевидна — Мы не тащим очевидную информацию в стиле «not found».
      • Не нужно создавать дополнительные файлы исключений, сама реализация очень проста синтаксически — метод не перегружается лишним кодом и докой.
      • Мы явным образом снимаем с себя ответственность за обработку неудачи, и передаем её вызывающему коду.

      Очень похожий способ — SomeOrDefault.

      Тоже распространенный для дотнета подход, когда мы отдаем найденное значение, а иначе null.


      А использовать вот так:


      Наивысшая надежность — Maybe монада.

      Идея простая. Есть отдельная сборка, в ней лежит абстрактный класс Maybe, у него два наследника, Success и Failure. Отдельная сборка и интёрнал конструктор нужны, чтобы гарантировать — наследников всегда будет только два.

      Тогда метод будет выглядеть вот так:


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

      Использовать код можно например так:


      Здесь не так много плюсов, но они очень увесистые.

      • Гарантии, надежность. Есть только один способ обойти нашу защиту — взять, и руками скастить результат к Success. Но это я не знаю, кем надо быть.
      • Мы легко можем протаскивать необходимую информацию об ошибке через класс Failure. Здесь много чего можно наинженерить, но главное — возможности для этого есть. Правда это будет уже не Maybe, а Result монада, но какая разница.
      • Вербозность. По коду, который отдаёт Maybe сразу понятно, что все может пойти не по плану.
      • Сам тип Maybe может быть один, универсальный на весь проект.
      • Подход отлично масштабируется на асинхронный код.
      • Легко разделять ответственность — мы можем сделать методы, которые готовы работать с Maybe, и метода, которые не готовы. Так мы построим приложение, в котором четко, на уровне типов разделена ответственность по обработке ошибок.

      Да, у меня в статье Maybe представлена исключительно как концепт. У неё есть отличные реализации в виде библиотек. В случае, если нужно передать информацию об ошибке, используется монада Either/Result. Для которой так же существуют сторонние решения.

      Способов борбы с исключениями несколько, не очень понятно, когда и какой использовать.

      Если у меня нет nullable, и кейс асинхронный — значит try pattern и someOrDefault мне не пойдет, и тогда я бы тоже взял Maybe.

      Соответственно, если хотим передать данные об ошибке, тогда лучше использовать Result монаду.

      Exception хорошо подходит для случаев:

      • Когда у тебя есть модуль, в нем произошла ошибка, и это значит что с этим модулем больше работать нельзя (например сторонний сервис упал). Выплевываем исключение, ловим его где то сверху, уведомляем все заинтересованные части системы, что сервис сдох.
      • Когда приложение не может продолжать свою работу. Например у нас десктопный софт, который является тонким клиентом, а сеть пропала. Бахаем ексепшн, ловим, говорим «извините», закрываемся.
      • Когда понятия не имеешь, что делать в случае ошибки, да и на проект тебе насрать — тогда бы я тоже взял Exception.

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

      Это поднимает куда более серьезную проблему. Ладно, подход с исключениями технически несовершенен, но даже если и был бы, есть штука, куда более несовершенная. Программисты. Человеческий фактор. Я вот пришел сюда, такой умный, начал учить как обрабатывать ошибки, а потом заглянул в код своих проектов, и везде вижу одно и то же- мой класс, как разработчика, недостаточно высок, я постоянно не понимаю, как разруливать исключительные ситуации. Я их игнорирую, логгирую, и прячу. Кроме тех мест, где они кому-то уже навредили, и меня заставили именно там все продумать. И никакие технические возможности языка не заставят меня продумывать все.

      Но. Они заставят продумывать чуть больше, может быть, на 5%, может на 1, может на 10. И это вообще единственный способ хоть как то уменьшать влияние человеческого фактора. Поэтому, я не вижу причин, чтобы отказываться от тех же монад или гарантированно обрабатываемых исключений.

      Я привел четыре концептуальных подхода к работе с ошибками, но на деле их намного больше. Например приходит в голову подход в Go — отдавать из функций кортеж (результат*ошибка). Как по мне- очень спорный способ, но я открыт к дискуссии. Делитесь мыслями в комментариях, какие ещё у нас есть варианты, и в чем их преимущество.

      Код примеров лежит здесь.

      На правах рекламы

      Подыскиваете виртуальный сервер для отладки проектов, сервер для разработки и размещения? Вы точно наш клиент :) Посуточная тарификация серверов самых различных конфигураций, антиDDoS.

      Читайте также: