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


Содержание

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

Обработка исключений (exception handling) позволяет упорядочить обработку ошибок времени исполнения. Используя обработку исключений С++, программа может автоматически вызвать функцию-обработчик ошибок тогда, когда такая ошибка возникает. Принципиальным достоин­ством обработки исключений служит то, что она позволяет автоматизировать большую часть кода для обработки ошибок, для чего раньше требовалось ручное кодирование.

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

Обработка исключений в С++ использует три ключевых слова: try, catch и throw. Те инструкции программы, где ожидается возможность появления исключительных ситуаций, содержатся в бло­ке try. Если в блоке try возникает исключение, т. е. ошибка, то генерируется исключение. Исклю­чение перехватывается, используя catch, и обрабатывается. Ниже это общее описание будет рас­смотрено более подробно.

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

try <
// блок try
catch (тип1 аргумент) <
// блок catch
catch (тип2 аргумент) <
// блок catch
catch (типЗ аргумент) <
// блок catch
>
.
catch (типN аргумент) <
// блок catch
>

Размеры блока try могут изменяться в больших пределах. Например, блок try может содержать несколько инструкций какой-либо функции, либо же, напротив, включать в себя весь код функ­ции main(), так что вся программа будет охвачена обработкой исключений.

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

Общая форма записи инструкции throw имеет вид:

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

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

Ниже представлен пример, иллюстрирующий способ обработки исключений в С++:

// пример обработки простого исключения
#include
int main()
<
cout
int main()
<
cout
void Xtest(int test)
<
cout
// try/catch могут находиться в функции вне main()
void Xhandler(int test)
<
try <
if (test) throw test;
>
catch(int i) <
cout

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

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

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

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

Программные ошибки (bugs)

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

Пользовательские ошибки (user errors)

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

Исключения (exceptions)

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

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

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

Роль обработки исключений в .NET

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

Помимо приемов, изобретаемых самими разработчиками, в API-интерфейсе Windows определены сотни кодов ошибок с помощью #define и HRESULT, а также множество вариаций простых булевских значений (bool, BOOL, VARIANT BOOL и т.д.). Более того, многие разработчики СОМ-приложений на языке С++ (а также VB 6) явно или неявно применяют небольшой набор стандартных СОМ-интерфейсов (наподобие ISupportErrorlnfo. IErrorlnfo или ICreateErrorlnfо) для возврата СОМ-клиенту понятной информации об ошибках.

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

Прелесть этой методики состоит в том, что она позволяет разработчикам использовать в области обработки ошибок унифицированный подход, который является общим для всех языков, ориентированных на платформу .NET. Благодаря этому, программист на C# может обрабатывать ошибки почти таким же с синтаксической точки зрения образом, как и программист на VB и программист на С++, использующий C++/CLI.

Дополнительное преимущество состоит в том, что синтаксис, который требуется применять для генерации и перехвата исключений за пределами сборок и машин, тоже выглядит идентично. Например, при написании на C# службы Windows Communication Foundation (WCF) генерировать исключение SOAP для удаленного вызывающего кода можно с использованием тех же ключевых слов, которые применяются для генерации исключения внутри методов в одном и том же приложении.

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

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

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

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

член, способный генерировать (throw) в вызывающем коде экземпляр класса исключения при соответствующих обстоятельствах;

блок кода на вызывающей стороне, ответственный за обращение к члену, в котором может произойти исключение;

блок кода на вызывающей стороне, который будет обрабатывать (или перехватывать (catch)) исключение в случае его возникновения.

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

Исключением является проблема, возникающая во время выполнения программы. Исключение AC # — это ответ на исключительное обстоятельство, которое возникает во время работы программы, например попытка деления на ноль.

Исключения обеспечивают способ передачи контроля из одной части программы в другую. Обработка исключений C # построена на четырех ключевых словах: try , catch , finally и throw .

  • try — блок try идентифицирует блок кода, для которого активируются определенные исключения. За ним следует один или несколько блоков catch .
  • catch — программа выхватывает исключение с обработчиком исключений в месте в программе, где вы хотите справиться с этой проблемой. Ключевое слово catch указывает на улавливание исключения.
  • finally — блок finally используется для выполнения заданного набора операторов, независимо от того, выбрано или не выбрано исключение. Например, если вы открываете файл, он должен быть закрыт независимо от того, создано ли исключение или нет.
  • throw — программа выдает исключение, когда возникает проблема. Это делается с использованием ключевого слова throw .

Синтаксис

Предполагая, что блок вызывает исключение, метод выбирает исключение, используя комбинацию ключевых слов try и catch . Блок try / catch помещается вокруг кода, который может генерировать исключение. Код внутри блока try / catch называется защищенным кодом, а синтаксис использования try / catch выглядит следующим образом:

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

Классы исключений в C #

Исключения C # представлены классами. Исключительные классы в C # в основном прямо или косвенно получены из класса System.Exception . Некоторые классы исключений, полученные из класса System.Exception, представляют собой классы System.ApplicationException и System.SystemException .

Класс System.ApplicationException поддерживает исключения, создаваемые прикладными программами. Следовательно, исключения, определенные программистами, должны вытекать из этого класса.

Класс System.SystemException является базовым классом для всех предопределенных системных исключений.

В следующей таблице приведены некоторые предопределенные классы исключений, полученные из класса Sytem.SystemException :

Обрабатывает ошибки ввода-вывода.

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

Обрабатывает ошибки, возникающие, когда тип несовместим с типом массива.

Обрабатывает ошибки, возникающие при ссылке на нулевой объект.

Обрабатывает ошибки, возникающие при делении дивиденда на ноль.

Обрабатывает ошибки, возникающие при типизации.

System.OutOfMemoryException

Обрабатывает ошибки, возникающие из-за недостаточной свободной памяти.

System.StackOverflowException

Обрабатывает ошибки, возникающие при переполнении стека.

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

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

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

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Создание пользовательских исключений

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

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

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

В этой статье (перевод [1]) раскрываются следующие вопросы, касающиеся обработки исключений (Exception):

• Базовые понятия исключений C# (с примерами)
• Общие исключения .NET
• Как создать свои собственные пользовательские типы исключений
• Как найти скрытые исключения .NET
• Как лучше всего документировать и отслеживать исключения C#

[Что такое исключение?]

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

Приложения используют логику обработки исключений (exception handling) для явной поддержки кодом каких-то неординарных событий. Причины исключений могут быть самые разные — от печально известного NullReferenceException до таймаута обращения к базе данных.

Анатомия исключений C#. Исключения позволяют передать управление из одной части кода в другую часть. Когда срабатывает/выбрасывается исключение (exception thrown), текущий поток выполнения кода секции try прерывается, и запускается выполнение секции catch. Обработка исключений C# осуществляется следующими ключевыми словами: try, catch, finally и throw.

try — блок try инкапсулирует проверяемый на исключение регион кода. Если любая строка кода в этом блоке вызывает срабатывание исключения, то исключение будет обработано соответствующим блоком catch.
catch — когда происходит исключение, запускается блок кода catch. Это то место, где Вы можете обработать исключения и предпринять адекватные для него действия, например записать событие ошибки в лог, прервать работу программы, или может просто игнорировать исключение (когда блок catch пустой).
finally — блок finally позволяет Вам выполнить какой-то определенный код приложения, если исключение сработало, или если оно не сработало. Например, освобождение объекта из памяти, который должен быть освобожден. Часто блок finally опускается, когда обработка исключения подразумевает нормальное дальнейшее выполнение программы — потому блок finally может быть просто заменен обычным кодом, который следует за блоками try и catch.
throw — ключевое слово throw используется для реального создания нового исключения, в результате чего выполнение кода попадет в соответствующие блоки catch и finally.


Пример 1: базовый блок «try catch finally»

На языке C# ключевые слова try и catch используются для определения блока проверяемого кода (блок try catch). Блок try catch помещается вокруг кода, который потенциально может выбросить исключение. Если произошло исключение, то этот блок try catch обработает исключение, гарантируя тем самым, что приложение не выдаст ошибку необработанного исключения (unhandled exception) [5], ошибки пользователя, и эта ошибка не разрушит процесс выполнения приложения.

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

Ваш код обработки исключения на C# может применить несколько операторов catch, предназначенных для разных типов исключений. Это может быть очень полезным — в зависимости от того, что делает код. В предыдущем примере ArgumentNullException возникнет только тогда, когда переданный URL сайта окажется null. WebException происходит из-за широкого массива разных проблем. Перехват определенных типов исключений может помочь в их обработке.

Пример 2: фильтры исключений

Одна из новых функций C# версии 6 был ввод фильтров исключений (exception filters). Они дают Вам еще больше контроля над блоками catch и дальнейшей обработкой определенных исключений. Это помогает точно подстроить код под то, как Вы обрабатываете исключения, и как Вы хотите их обработать.

До C# 6 Вы должны были поймать все типы WebException и обработать их. Теперь Вы можете принять решение только обработать их в определенных сценариях и позволить другому пузырю сценариев до кода который названный этим методом. Вот измененный пример с фильтрами:

Илон Маск рекомендует:  Тег tr

[Общие исключения .NET]

Правильная обработка исключения критична для всего кода приложения. Имеется несколько часто используемых стандартных исключений (Common .NET Exceptions). Чаще всего стоит бояться исключения обращения по не инициализированной ссылке (null reference exception). Ниже приведен список общих ошибок, которые Вы будете наблюдать регулярно.

System.NullReferenceException — наиболее общее исключение, связанное с вызовом метода, когда его объект не инициирован.
System.IndexOutOfRangeException — произошла попытка доступа к не существующему элементу массива.
System.IO.IOException — используется в связи с файловыми операциями ввода/вывода (file I/O).
System.Net.WebException — обычно выбрасывается при любых ошибках, связанных с вызовами протокола HTTP.
System.Data.SqlClient.SqlException — различные типы исключений сервера SQL.
System.StackOverflowException — если метод рекурсивно вызывает сам себя, то Вы можете получить это исключение.
System.OutOfMemoryException — если приложение столкнулось с недостатком памяти.
System.InvalidCastException — если Вы попытались сделать приведение типа объекта (cast) к несовместимому типу.
System.InvalidOperationException — общее стандартное исключения в некоторых библиотеках.
System.ObjectDisposedException — попытка использовать объект, который уже освобожден.

[Как создать свои собственные пользовательские типы исключений]

Исключения C# определены как классы, точно так же, как и любой другой объект C#. Все исключения наследуются от базового класса System.Exception. Есть много общих исключений (common exceptions), которые Вы можете использовать в своем собственном коде. Обычно разработчики используют стандартный объект ApplicationException или Exception для выбрасывания пользовательских исключений (custom exceptions). Вы также можете создать свой собственный тип исключения.

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

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

Преимущества пользовательского типа исключений C#:

• Вызов кода может осуществлять пользовательскую обработку Custom Exception Type
• Возможность пользовательского мониторинга вокруг этого Custom Exception Type

Пример Custom Exception Type:

[Как найти скрытые исключения .NET]

Что такое First Chance Exceptions (исключения первого шанса)? Нормальная ситуация для большого количества исключений быть выброшенными, пойманными и затем проигнорированными. Внутренний код .NET Framework даже выбрасывает некоторые исключения, которые отбрасываются. Одна из функций C# это так называемые исключения первого шанса (first chance exceptions). Это позволяет Вам увидеть каждое выбрасываемое исключение .NET Exception по отдельности.

Код, наподобие приведенного ниже, очень часто встречается в приложениях. Этот код может выбросить (throw) тысячи исключений в минуту, и никто никогда про это бы не узнал. Этот код из приложения, который показывал серьезные проблемы производительности из-за плохой обработки исключений. Исключения произойдут, если reader равен null, columnName равен null, columnName не существует в результатах, значение столбца было null, или если value неправильная для DateTime. Настоящее минное поле ошибок.

Как в Visual Studio разрешить First Chance Exceptions. Когда запускаете приложение в отладчике Visual Studio, Вы можете установить Visual Studio останавливаться (break) в любой момент, когда выбрасывается C# Exception. Это может помочь найти исключения в своем коде, о которых Вы не знали, что они существуют.

Чтобы получить доступ к настройкам исключений, перейдите в меню Debug -> Windows -> Exception Settings (Отладка -> Исключения. это меню доступно при активной сессии отладчика). Под «Common Language Runtime Exceptions» Вы можете выбрать типы исключений, на которых отладчик должен автоматически поставить точку останова. Хорошей мыслью будет поставить здесь везде галочки. Как только код остановится на исключении, Вы можете указать ему игнорировать этот определенный тип исключений, если хотите.

Как просмотреть все исключения с префиксом. Бесплатный Stackify .NET profiler [6] также может показать все Ваши исключения. Подробнее см. статью [7]. Решение Stackify Retrace [8] (платное) для Ваших серверов также может собирать все исключения первого шанса через .NET profiler. Без каких-либо изменений в коде или конфигурации ом может автоматически собрать и показать все исключения.

Как в коде подписаться на First Chance Exceptions. Среда .NET Framework предоставляет способ подписаться на событие, чтобы запускать функцию обратного вызова (callback) в любой момент возникновения исключения. Вы можете использовать это, чтобы перехватить все исключения. Хорошей мыслью организовать потенциальную подписку на исключения, чтобы выводить информацию о них в окно отладки. Это дало бы некоторое отображение текущей ситуации в приложении без загромождения информацией Ваших лог-файлов. Обычно подписка делается один раз при старте приложения — в методе Main() консольного приложения или в коде запуска (startup) web-приложения ASP.NET.

[Как лучше всего документировать и отслеживать исключения C#]

Правильная обработка исключения критична для любого приложения. Ключевой компонент — создание библиотеки записи в лог исключений. Подробнее про это см. статью «C# Logging Best Practices» [9]. Лучше всего записывать исключения с использованием библиотек NLog, Serilog или log4net. Все эти три фреймворка дают Вам возможность записывать исключения в файл. Также они позволяют отправлять Ваши логи различным другим получателям — база данных, система лога Windows (Windows Event Viewer), email, или служба мониторинга ошибок (error monitoring service). Любое исключение в приложении должно быть записано, это критично для поиска проблем в Вашем коде.

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

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

Служба мониторинга ошибок — ключевой инструмент для любой команды разработки. Она позволяет централизованно собирать все Ваши исключения. Решение [8] предоставляет для этого следующие возможности:

• Централизованный лог исключений
• Просмотр и поиск всех исключений по всем серверам и приложениям
• Уникально идентифицировать каждое исключение
• Принимать оповещения на email о возникновении новых исключений или в случае слишком частого появления ошибок

C++ — Урок 011. Исключения

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

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

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

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

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

Оператор 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)
  • runtime_error
    • range_error
    • overflow_error
    • underflow_error
    • system_error (C++11)
      • ios_base::failure (начиная с C++11)
  • bad_typeid
  • bad_cast
  • bad_weak_ptr (C++11)
  • bad_function_call (C++11)
  • bad_alloc
    • bad_array_new_length (C++11)
  • bad_exception
  • ios_base::failure (до C++11)

std::logic_error

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

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

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

std::invalid_argument

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

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

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

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

std::domain_error


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

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

std::length_error

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

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

std::out_of_range

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

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

std::future_error

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

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

std::runtime_error

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

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

std::range_error

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

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

std::overflow_error

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

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

std::underflow_error

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

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

std::system_error

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

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

std::ios_base::failure

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

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

std::bad_typeid

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

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

std::bad_cast

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

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

std::bad_weak_ptr

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

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

std::bad_function_call

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

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

std::bad_alloc

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

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

std::bad_array_new_length

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

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

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

std::bad_exception

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

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

  1. Если нарушается динамическая спецификация исключений
  2. Если std::exception_ptr хранит копию пойманного исключения, и если конструктор копирования объекта исключения поймал current_exception, тогда генерируется исключение захваченных исключений.
Рекомендуем хостинг TIMEWEB

Рекомендуемые статьи по этой тематике

Обработка исключений для C#

View more Tutorials:

1- Что такое Exception?

Сначала давайте посмотрим на следующий иллюстративный пример:

В этом примере есть часть кода c ошибкой, которая получается из-за деления на 0. Деление на 0 вызывает исключение: DivideByZeroException

Мы будем модифицировать код примера выше:

2- Иерархия исключений

Exception

Основной класс всех исключений.

SystemException

Основной класс всех исключений генерированных во время запуска программы.

IndexOutOfRangeException

Выбрасывается во время запуска при доступе в элемент массива с неправильным индексом.

NullReferenceException

Выбрасывается во время запуска при ссылке на объект null.

AccessViolationException

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

InvalidOperationException

Выбрасывается методом при недействительном статусе.


ArgumentException

Основной класс всех исключений связанных с аргументом (Argument).

ArgumentNullException

Это подкласс ArgumentException, который выбрасывается методом, не позволяющим передачу аргументов null.

ArgumentOutOfRangeException

Это подкласс ArgumentException, который выбрасывается методом когда аргумент не соответствует рамке позволяющей передачу.

ExternalException

Основной класс для исключений или пришедших из внешней среды.

COMException

Класс расширенный из ExternalException, исключение упаковывает информацию COM.

SEHException

Класс расширенный из ExternalException, ловил все исключения из Win32.

Главное в .NET — Обработка исключение на C#

By Марк Михейлис | November 2015

Добро пожаловать в новую рубрику «Главное в .NET». В этой рубрике вы сможете следите за всем, что творится в мире Microsoft .NET Framework, будь то достижения в C# vNext (в настоящее время C# 7.0), усовершенствования во внутреннем устройстве .NET или события на фронте Roslyn и .NET Core (например, перевод MSBuild в статус ПО с открытым исходным кодом).

Я вел разработки с использованием .NET и писал об этой инфраструктуре с момента объявления о выпуске ее предварительной версии в 2000 г. В этой рубрике я буду писать не просто о новшествах, но и о том, как использовать соответствующие технологии с упором на рекомендации по их применению.

Я живу в Спокане (штат Вашингтон), где являюсь «главным корифеем» в консалтинговой компании сегмента «high-end», которая называется IntelliTect (IntelliTect.com). IntelliTect специализируется на решении трудных задач и великолепно справляется с ними. Я был Microsoft MVP (в настоящее время в области C#) в течении 20 лет и восемь из них занимал должность регионального директора Microsoft. Сегодня я открываю рубрику обсуждением обновленных правил обработки исключений.

В C# 6.0 включили два новых средства обработки исключений. Во-первых, появилась поддержка условий исключения (exception conditions) — возможность предоставлять выражение, которое отфильтровывает исключение, предотвращая его попадание в блок catch до раскрутки стека. Во-вторых, введена поддержка асинхронности в блоке catch — того, что было немыслимо в C# 5.0, хотя именно тогда в язык добавили асинхронность. Кроме того, в последних пяти версиях C# и соответствующих выпусках .NET Framework было много других изменений, которые в некоторых случаях достаточно значимы, чтобы потребовать правок в правилах кодирования на C#. В этом выпуске рубрики я дам обзор этих изменений и представлю обновленные правила кодирования, относящиеся к обработке исключений — их захвату.

Илон Маск рекомендует:  DaysInAYear - Функция Delphi

Захват исключений: обзор

Как хорошо понятно, генерация исключения определенного типа позволяет механизму захвата (catcher) использовать сам тип исключения для идентификации проблемы. Иначе говоря, не обязательно захватывать исключение и применять выражение switch к сообщению об исключении, чтобы определить, какое действие надо предпринять в связи с этим исключением. Вместо этого C# поддерживает множество блоков catch, каждый из которых нацелен на исключение конкретного типа, как показано на рис. 1.

Рис. 1. Захват исключений разных типов

Когда возникает исключение, управление передается первому блоку catch, способному обработать это исключение. Если с try сопоставлено более одного блока catch, близость совпадение определяется цепочкой наследования (предполагая отсутствие условия исключения C# 6.0), и обработка исключения передается первому catch в этой цепочке. Например, в коде на рис. 2 генерируется исключение типа System.Exception, и оно обрабатывается вторым блоком catch, так как System.InvalidOperationException в конечном счете наследует от System.Exception. Поскольку InvalidOperationException наиболее близко совпадает со сгенерированным исключением, оно будет захвачено блоком catch(InvalidOperationException. ), а не catch(Exception. ), если бы таковой блок был в этом коде.

Блоки catch должны появляться в порядке (вновь предполагая отсутствие условия исключения в C# 6.0) от наиболее специфичных до наиболее универсальных, чтобы избежать ошибки при компиляции. Например, добавление блока catch(Exception. ) до любого другого приведет к ошибке при компиляции, поскольку все предыдущие исключения наследуют от System.Exception в той или иной точке цепочки наследования. Также заметьте, что именованный параметр для блока catch не обязателен. По сути, заключительный catch, к сожалению, допускается даже без указания типа параметра.

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

Сценарий 1 Захваченное исключение не в полной мере идентифицирует проблему, из-за которой возникло это исключение. Например, при вызове System.Net.WebClient.DownloadString с допустимым URL исполняющая среда может сгенерировать System.Net.WebException, если нет сетевого соединения; то же самое исключение генерируется при указании несуществующего URL.

Сценарий 2 Захваченное исключение включает закрытые данные, которые не должны раскрываться выше по цепочке вызовов. Например, в очень ранней версии CLR v1 (фактически в версии pre-alpha) было исключение, сообщающее нечто вроде «Security exception: You do not have permission to determine the path of c:\temp\foo.txt» («Исключение защиты: у вас нет прав на определение пути c:\temp\foo.txt»).

Сценарий 3 Тип исключения слишком специфичен для обработки вызвавшим кодом. Например, исключение System.IO (вроде UnauthorizedAccessException, IOException, FileNotFoundException, DirectoryNotFoundException, PathTooLongException, NotSupportedException или SecurityException, ArgumentException) возникает на сервере при вызове веб-сервиса для поиска почтового кода ZIP.

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

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

Второй вариант при захвате исключения — определять, что вы фактически не в состоянии должным образом обработать его. В этом случае вы захотите повторно сгенерировать точно такое же исключение, отправив его следующему обработчику вверх по цепочке вызовов. Блок catch(InvalidOperationException. ) на рис. 1 как раз и демонстрирует этот вариант. В выражении throw не указана идентификация генерируемого им исключения (присутствует только ключевое слово throw), хотя экземпляр исключения (exception) появляется в области видимости этого блока catch и его можно было бы сгенерировать повторно. Генерация специфического исключения привела бы к обновлению всей информации стека для соответствия новой позиции throw. В итоге вся информация стека, указывающая место вызова, где возникло исходное исключение, была бы утрачена, что сильно затруднило бы диагностику проблемы. Так что, определив, что данный блок catch не может полноценно обработать исключение, это исключение следует генерировать повторно, используя пустое выражение throw.

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

Генерация существующих исключений без замены информации стека

В C# 5.0 был добавлен механизм, позволяющий генерировать ранее сгенерированное исключение, не теряя информации трассировки стека в исходном исключении. Это дает возможность повторно генерировать исключения, например, даже извне блока catch, а значит, без использования пустого выражения throw. Хотя потребность в этом возникает весьма редко, в некоторых случаях исключения обертываются или сохраняются до тех пор, пока поток выполнения программы не выйдет из блока catch. Так, многопоточный код мог бы обертывать исключение в AggregateException. .NET Framework 4.5 предоставляет класс System.Runtime.ExceptionServices.ExceptionDispatchInfo специально для этого случая, для чего используются его статический метод Capture и метод экземпляра Throw. Рис. 2 демонстрирует повторную генерацию исключения без сброса информации трассировки стека или применения пустого выражения throw.

Рис. 2. Использование ExceptionDispatchInfo для повторной генерации исключения

В случае метода ExeptionDispatchInfo.Throw компилятор не интерпретирует его как выражение возврата в том же стиле, в каком это могло бы быть сделано для обычного выражения throw. Например, если бы сигнатура метода возвращала значение, но на самом деле никакого значения по пути кода с ExceptionDispatchInfo.Throw не возвращалось бы, компилятор выдавал бы ошибку, указывающую на отсутствие возвращенного значения. Иногда разработчикам приходится следовать ExceptionDispatchInfo.Throw с выражением return, хотя такое выражение никогда не будет выполнено — вместо этого будет генерироваться исключение.

Захват исключений в C# 6.0

Универсальное правило обработки исключений — избегать захвата тех исключений, которые вы не можете полноценно обработать. Однако, поскольку выражения catch до появления C# 6.0 могли фильтровать исключения лишь по их типу, возможность проверки данных и контекста исключения до раскрутки стека в блоке catch требовали, чтобы этот блок был обработчиком до анализа исключения. К сожалению, после принятия решения о невозможности обработки исключения писать код, который позволяет другому блоку catch в том же контексте обработать исключение, было весьма затруднительно. А повторная генерация того же исключения вынуждает инициировать двухпроходный процесс обработки исключения, который сначала доставляет исключение выше по цепочке вызовов до тех пор, пока не найдется блок, способный его обработать, а затем раскручивает стек вызовов для каждого фрейма в стеке между исключением и позицией захвата.

Вместо того чтобы после генерации исключения раскручивать стек вызовов в блоке catch лишь для повторной генерации исключения из-за того, что при дальнейшем анализе стала понятной невозможность его полноценной обработки, очевидно, было бы предпочтительнее вообще не захватывать это исключение. Начиная с C# 6.0, в блоках catch доступно дополнительное условное выражение. Вместо подбора блока catch только по типу исключения в C# 6.0 введена поддержка проверки по условию. Выражение when позволяет вам предоставлять булево выражение, которое дополнительно фильтрует блок catch и определяет, что он способен обработать исключение, только если условие дает true. Блок System.Web.HttpException на рис. 1 демонстрирует это с помощью оператора сравнения на равенство (equality comparison operator).

Интересный результат условия исключения в том, что, когда указывается это условие, компилятор не принуждает к расстановке блоков catch в порядке цепочки наследования. Например, блок catch для типа System.ArgumentException с сопутствующим условием исключения теперь может появляться до более специфического блока catch для типа System.ArgumentNullException, хотя последний наследует от первого. Это важно, так как позволяет писать специфическое условие исключения, которое сопоставлено универсальному типу исключения, следующему за более специфическим типом исключения (с условием исключения или без него). Поведение в период выполнение остается согласованным с более ранними версиями C#: исключения захватываются первым совпадающим блоком catch. Просто теперь вопрос о том, можно ли считать какой-то блок catch совпадающим, решается на основе комбинации типа и условия исключения, а компилятор вводит порядок относительно только блоков catch, не имеющих условий исключения. Например, catch(System.Exception) с условием исключения может появляться до catch(System.ArgumentException) с условием исключения или без него.

Однако, как только появляется catch без условия исключения, более специфический блок catch [скажем, catch(System.ArgumentNullException)] может быть блокирован, даже если в нем есть условие исключения. Это оставляет программисту свободу в кодировании условий исключений, которые потенциально могут появляться не по порядку, — при этом более ранние условия исключения, захватывающие исключения, предназначенные для более поздних условий, что потенциально может даже сделать последних непреднамеренно недостижимыми. В конечном счете порядок ваших блоков catch аналогичен тому, как вы располагали бы выражения if-else. Когда условие удовлетворяется, все остальные блоки catch игнорируются. Однако в отличие от условий в выражениях if-else все блоки catch должны включать проверку типа исключения.

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

Пример оператора сравнения на рис. 1 тривиален — условие исключения не обязательно должно быть столь простым. Вы могли бы, например, вызывать какой-то метод для проверки условия. Единственное требование в том, что выражение должно быть предикатом — возвращать булево значение. Иначе говоря, вы можете фактически выполнять любой код из цепочки вызовов захвата исключения. Это открывает возможность полного предотвращения повторного захвата и генерации того же исключения; по сути, вы можете достаточно сузить контекст до захвата исключения и захватывать его только в том случае, если оно действительно будет обработано. Таким образом, правило избегать захвата исключений, которые вы не в состоянии полностью обработать, становится реальностью. Любая проверка условий, окружающая пустое выражение throw, вероятно, может служить признаком плохого кода (code smell), и этого следует избегать. Подумайте о добавлении условия исключения вместо использования пустого выражения throw, кроме случая сохранения измененяемого состояния (volatile state) перед завершением процесса.

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

Универсальный блок catch

C# требует, чтобы любой объект, генерируемый кодом, был производным от System.Exception. Однако это требование не универсально для всех языков. Так, C/C++ позволяет генерировать объект любого типа, в том числе управляемые исключения, не производные от System.Exception, или даже элементарные типы вроде int или string. Начиная с C# 2.0, все исключения — производные они от System.Exception или нет — будут передаваться в C#-сборки как производные от System.Exception. Результат заключается в том, что блоки catch для System.Exception будут захватывать все «достоверно обрабатываемые» исключения, не захваченные предшествующими блоками. Однако до C# 1.0, если исключение, не производное от System.Exception, генерировалось при вызове какого-то метода (находящего в сборке, написанной не на C#), то оно не захватывалось блоком catch(System.Exception). По этой причине C# также поддерживает универсальный блок catch (catch< >), который теперь ведет себя идентично блоку catch(System.Exception exception) с тем исключением, что в нем не указывается ни тип, ни имя переменной. Недостаток такого блока в том, что у вас нет экземпляра исключения, к которому вы могли бы обратиться, и нет способа узнать, какие действия следовало бы предпринять. Нельзя даже запротоколировать это исключение или распознать маловероятный случай, где такое исключение безопасно.

На практике вы должны избегать блока catch(System.Exception) и универсального блока catch (далее обобщенно называемых блоком catch для System.Exception), кроме как под предлогом «обработки» исключения простым его протоколированием перед завершением процесса. Следуя универсальному правилу захватывать только те исключения, которые вы можете обработать, было бы слишком самонадеянным писать код, для которого программист объявляет, что этот catch может обрабатывать любые возможные исключения. Во-первых, попытка перечислить всевозможные исключения (особенно в теле Main, где находится самое большое количество выполняемого кода и где контекст скорее всего минимален) — задача неподъемная, кроме как в простейших программах. Во-вторых, существует уйма исключений, которые могут генерироваться неожиданно для вас.

До C# 4.0 существовал третий набор исключений из-за поврежденного состояния, при которых программу, как правило, нельзя было восстановить хотя бы в принципе. Этот набор не столь значим, начиная с C# 4.0, но catch(System.Exception) (или универсальный блок catch) на самом деле не будет захватывать такие исключения. (С технической точки зрения, метод можно дополнить System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions, чтобы захватывать даже эти исключения, но вероятность того, что вы сумеете в достаточной мере обработать их, крайне низка. Подробнее на эту тему см. по ссылке bit.ly/1FgeCU6.)

Одна техническая деталь, которую стоит отметить в исключениях из-за поврежденного состояния, заключается в том, что при генерации в период выполнения они передаются только через блоки catch для System.Exception. Явно сгенерированное исключение из-за поврежденного состояния, такое как System.StackOverflowException или другое System.SystemException, будут фактически захвачены. Однако генерация такого исключения будет грубейшей ошибкой и на самом деле поддерживается только для обратной совместимости. Современные правила требуют, чтобы вы не генерировали какое-либо из этих исключений (в том числе System.StackOverflowException, System.SystemException, System.OutOfMemoryException, System.Runtime.InteropServices.COMException, System.Runtime.InteropServices.SEHException и System.ExecutionEngineException).

Подведем итог. Избегайте использования блока catch для System.Exception, если только он не должен обработать исключение с помощью какого-то кода очистки и запротоколировать факт этого исключения до повторной генерации или корректного завершения приложения. Например, если бы блок catch мог успешно сохранять любые изменяемые данные (на что в любом случае не стоит полагаться, так как они тоже могут быть повреждены) до закрытия приложения или повторной генерации исключения. При возникновении ситуации, в которой приложение следует завершить, потому что продолжать выполнение небезопасно, код должен вызывать метод System.Environment.FailFast. Используйте System.Exception и универсальные блоки catch только для протоколирования исключения перед завершением приложения.

Заключение

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

  • избегайте захвата исключений, которые вы не в состоянии полноценно обработать;
  • избегайте скрытия (отбрасывания) исключений, которые вы не полностью обрабатываете;
  • используйте throw для повторной генерации исключения, а не throw внутри блока catch;
  • указывайте в свойстве InnerException исключения-оболочки захваченное исключение, если только это не приводит к раскрытию конфиденциальных данных;
  • подумайте о применении выражения условия вместо повторной генерации исключения после захвата исключения, которое вы не можете обработать;
  • избегайте генерации исключений из условного выражения исключения;
  • будьте осторожны при повторной генерации других исключений;
  • используйте System.Exception и универсальные блоки catch только в редких случаях — при необходимости запротоколировать исключение перед завершением приложения;
  • избегайте отчета об исключении или его протоколирования ниже по стеку вызовов.
Илон Маск рекомендует:  Определение слова скриншот, как сделать снимок экрана

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

Заметьте, что многое из этого материала взято из следующего издания моей книги «Essential C# 6.0 (5th Edition)» (Addison-Wesley, 2015), которая доступна сейчас на itl.tc/EssentialCSharp.

Марк Михейлис*(Mark Michaelis) — учредитель IntelliTect, где является главным техническим архитектором и тренером. Почти два десятилетия был Microsoft MVP и региональным директором Microsoft с 2007 года. Работал в нескольких группах рецензирования проектов программного обеспечения Microsoft, в том числе C#, Microsoft Azure, SharePoint и Visual Studio ALM. Выступает на конференциях разработчиков, автор множества книг, последняя из которых — «Essential C# 6.0 (5th Edition)». С ним можно связаться в Facebook (facebook.com/Mark.Michaelis), через его блог (IntelliTect.com/Mark), в Twitter (@markmichaelis) или по электронной почте mark@IntelliTect.com.*

Выражаю благодарность за рецензирование статьи экспертам Кевину Босту (Kevin Bost), Джейсону Питерсону (Jason Peterson) и Мэдсу Торгерсону (Mads Torgerson).

C#: обработка ошибок

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

Исключения (Exceptions) и инструкция try

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

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

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

В целом конструкция try выглядит следующим образом:

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

Чтобы этого избежать можно использовать конструкцию try :

Обработка исключений довольно ресурсоёмкая операция, поэтому на практике для таких случаев как в примере ее лучше не использовать (лучше непосредственно перед делением проверить делить на равенство нулю).

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

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

Оговорка catch

В оговорке catch указывается какой тип исключения она должна перехватывать. Это может быть либо System.Exception , либо его производный класс. Перехватывая непосредственно System.Exception , мы перехватим все возможные ошибки. Это может быть полезно в нескольких случаях:

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

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

Можно обработать несколько типов исключений с помощью нескольких оговорок catch:

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


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

Более того, в оговорке catch можно опустить и переменную и тип исключения — такая оговрка будет перехватывать все исключения:

Блок finally

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

Блок finally выполняется в следующих случаях:

  • после завершения блока catch
  • если выполнение блока try прервано jump-инструкциями: return , goto и т.д.
  • после выполнения блока try полностью, если исключений так и не было выброшено

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

В пример для закрытия файла вызывается метод Dispose . Использование этого метода внутри блока finally является стандартной практикой. C# даже позволяет заменить всю конструкцию инструкцией using .

Инструкция using

Многие классы инкапсулируют неуправляемые ресурсы, такие как дескриптор файла, соединение с базой данных и т.д. Эти классы реализуют интерфейс System.IDisposable , который содержит единственный метод без параметров Dispose , освобождающий соответствующие машинные ресурсы. Инструкция using предусматривает удобный синтаксис вызова метода Dispose для объектов реализующих IDisposable внутри блока finally :

Что эквивалентно следующей конструкции:

Выбрасывание исключений

Исключение может быть выброшено автоматически во время выполнения программы либо явно в коде программы с помощью ключевого слова throw :

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

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

Если throw заменить на throw ex , то пример по прежнему будет работать, но свойство исключения StackTrace не будет отражать исходную ошибку.

Другой распространенный сценарий использования повторного выбрасывания исключения — повторное выбрасывание более специфического и конкретного типа исключения, чем было перехвачено ранее:

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

Основные свойства System.Exception

К наиболее важным свойствам класса System.Exception можно отнести:

  • StackTrace — строка, представляющая все методы, которые были вызваны, начиная с того, в котором было выброшено исключение, и заканчивая тем, в котором содержится блок catch , перехвативший исключение;
  • Message — строка с описанием ошибки;
  • InnerException — содержит ссылку на объект Exeption , который вызвал текущее исключение (например, при повторном выбрасывании исключения).

Основные типы исключений

Следующие типы исключений являются наиболее распространенными в среде CLR и .NET Framework. Их можно выбрасывать непосредственно или использовать как базовые классы для пользовательских типов исключений.

  • System.ArgumentException — выбрасывается при вызове функции с неправильным аргументом.
  • System.ArgumentNullException — производный от ArgumentException класс, выбрасывается если один из аргументов функции неожиданно равен null .
  • System.ArgumentOutOfRangeException — производный от ArgumentException класс, выбрасывается когда аргумент функции имеет слишком большое или слишком маленькое значение для данного типа (обычно касается числовых типов). Например, такое исключение будет выброшено если попытаться передать отрицательное число в функцию, которая ожидает только положительные числа.
  • System.InvalidOperationException — выбрасывается когда состояние объекта является неподходящим для нормального выполнения метода, например, при попытке прочесть не открытый файл.
  • System.NotSupportedException — выбрасывается, когда запрошенный функционал не поддерживается, например, если попытаться вызвать метод Add для коллекции доступной только для чтения (свойство коллекции IsReadOnly возвращает true ).
  • System.NotImplementedException — выбрасывается, когда запрошенный функционал еще не реализован.
  • System.ObjectDisposedException — выбрасывается при попытке вызвать метод объекта, который уже был уничтожен (disposed).

Директивы препроцессора

Директивы препроцессора снабжают компилятор дополнительной информацией об областях кода. Самые распространенные директивы препроцессора — условные директивы, позволяющие включить или исключить области кода из компиляции.

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

Обработка исключений (exception handling) позволяет упорядочить обработку ошибок времени исполнения. Используя обработку исключений С++, программа может автоматически вызвать функцию-обработчик ошибок тогда, когда такая ошибка возникает. Принципиальным достоин­ством обработки исключений служит то, что она позволяет автоматизировать большую часть кода для обработки ошибок, для чего раньше требовалось ручное кодирование.

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

Обработка исключений в С++ использует три ключевых слова: try, catch и throw. Те инструкции программы, где ожидается возможность появления исключительных ситуаций, содержатся в бло­ке try. Если в блоке try возникает исключение, т. е. ошибка, то генерируется исключение. Исклю­чение перехватывается, используя catch, и обрабатывается. Ниже это общее описание будет рас­смотрено более подробно.

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

try <
// блок try
catch (тип1 аргумент) <
// блок catch
catch (тип2 аргумент) <
// блок catch
catch (типЗ аргумент) <
// блок catch
>
.
catch (типN аргумент) <
// блок catch
>

Размеры блока try могут изменяться в больших пределах. Например, блок try может содержать несколько инструкций какой-либо функции, либо же, напротив, включать в себя весь код функ­ции main(), так что вся программа будет охвачена обработкой исключений.

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

Общая форма записи инструкции throw имеет вид:

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

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

Ниже представлен пример, иллюстрирующий способ обработки исключений в С++:

// пример обработки простого исключения
#include
int main()
<
cout
int main()
<
cout
void Xtest(int test)
<
cout
// try/catch могут находиться в функции вне main()
void Xhandler(int test)
<
try <
if (test) throw test;
>
catch(int i) <
cout

Обработка исключений в многопоточных приложениях

Рассказывает Пол Шарф, автор блога genericgamedev.com

В прошлом уроке мы использовали многопоточность для создания анимированных (или даже интерактивных) экранов загрузки, а также для уменьшения времени загрузки в целом.

И хотя мы рассмотрели множество вещей на примере Roche Fusion, кое-что мы не затронули вовсе — что, если что-то пойдет не так? Или, более техническим языком — что, если один из наших потоков вернет исключение?

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

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

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

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

Возможные решения

Существует несколько возможных способов обрабатывать исключения в потоках.

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

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

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

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

Используем то, что у нас уже есть

Ранее мы расписали архитектуру небольшого фреймворка, и я бы хотел представить вам решение, которое не приводит к вышеописанным проблемам.

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

actionQueue – это объект класса, разработанного нами в этом посте, который позволяет перенаправлять из всех потоков какие-либо действия на главный поток.

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

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

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

Сохраняем стеки вызовов

К счастью, у этой проблемы есть очень легкое решение.

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

Внесем поправку в написанный выше код:

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

Дополнительно

Конечно, если возможно, то лучше проектировать ваше приложение так, что возможность возникновения фатального исключения стремится к нулю.

Например, мы в Roche Fusion оборачиваем каждый скриптовый файл в try-catch . В случае, если что-то пойдет не так, в игровую консоль выводится предупреждение и загрузка продолжается.

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

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

Заключение

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

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

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL