try-catch (Справочник по C#)
Блок try содержит защищенный код, который может вызвать исключение. Этот блок выполняется, пока не возникнет исключение или пока он не будет успешно завершен. Например, следующая попытка приведения объекта null вызывает исключение NullReferenceException:
Хотя предложение catch может использоваться без аргументов для перехвата любого типа исключения, такое использование не рекомендуется. В целом вы должны перехватывать только те исключения, после которых вы знаете, как выполнить восстановление. Поэтому всегда следует указывать аргумент объекта, производный от System.Exception, например:
Использование аргументов catch представляет один из способов фильтрации исключений, которые требуется обработать. Вы также можете использовать фильтр исключений, который дополнительно проверяет исключение, чтобы решить, следует ли его обрабатывать. Если фильтр исключений возвращает значение false, поиск обработчика продолжается.
Фильтры исключений предпочтительнее перехвата и повторного вызова (объясняется ниже), поскольку фильтры оставляют стек в целости и сохранности. Если последующий обработчик разгружает стек, вы можете увидеть, откуда изначально произошло исключение, а не только последнее место, в котором оно было повторно вызвано. Обычно выражения фильтра исключений используются для ведения журнала. Вы можете создать фильтр, который всегда возвращает значение false, а также записывает выходной результат в журнал, чтобы регистрировать исключения в журнале по мере их поступления без необходимости их обработки и повторного вызова.
Вы можете перехватывать одно исключение и вызывать другое исключение. При этом следует указать перехватываемое исключение как внутреннее, как показано в следующем примере.
Вы также можете повторно вызывать исключение при выполнении указанного условия, как показано в следующем примере.
Дополнительные сведения о перехвате исключений см. в разделе try-catch-finally.
Исключения в асинхронных методах
Когда управление достигает await в асинхронном методе, выполнение метода приостанавливается до завершения выполнения ожидающей задачи. После завершения задачи выполнение в методе может быть возобновлено. Дополнительные сведения см. в разделе Асинхронное программирование с использованием ключевых слов async и await.
Задача может быть в состоянии сбоя, если в ожидаемом асинхронном методе произошло несколько исключений. Например, задача может быть результатом вызова метода Task.WhenAll. При ожидании такой задачи перехватывается только одно из исключений и невозможно предсказать, какое исключение будет перехвачено. См. пример в разделе Пример Task.WhenAll.
Пример
Пример двух блоков catch
В следующем примере используются два блока catch и перехватывается наиболее конкретное исключение, поступившее первым.
Пример асинхронного метода
Пример Task.WhenAll
В следующем примере демонстрируется обработка исключений, когда несколько задач могут привести к нескольким исключениям. Блок try ожидает задачу, которая возвращается вызовом метода Task.WhenAll. Эта задача завершается после завершения трех задач, к которым применяется WhenAll.
Каждая из трех задач вызывает исключение. Блок catch выполняет итерацию по исключениям, которые обнаруживаются в свойстве Exception.InnerExceptions задачи, возвращенной методом Task.WhenAll.
Спецификация языка C#
Дополнительные сведения см. в разделе Оператор try в документации Спецификация C# 6.0.
Обработка исключений
Конструкция try..catch..finally
Если нужный блок catch не найден, то при возникновении исключения программа аварийно завершает свое выполнение.
Рассмотрим следующий пример:
В данном случае происходит деление числа на 0, что приведет к генерации исключения. И при запуске приложения в режиме отладки мы увидим в Visual Studio окошко, которое информирует об исключении:
И в этом случае единственное, что нам остается, это завершить выполнение программы.
В данном случае у нас опять же возникнет исключение в блоке try, так как мы пытаемся разделить на ноль. И дойдя до строки
выполнение программы остановится. CLR найдет блок catch и передаст управление этому блоку.
После блока catch будет выполняться блок finally.
Таким образом, программа по-прежнему не будет выполнять деление на ноль и соответственно не будет выводить результат этого деления, но теперь она не будет аварийно завершаться, а исключение будет обрабатываться в блоке catch.
И, наоборот, при наличии блока finally мы можем опустить блок catch и не обрабатывать исключение:
Однако, хотя с точки зрения синтаксиса C# такая конструкция вполне корректна, тем не менее, поскольку CLR не сможет найти нужный блок catch, то исключение не будет обработано, и программа аварийно завершится.
Обработка исключений и условные конструкции
Ряд исключительных ситуаций может быть предвиден разработчиком. Например, пусть программа предусматривает ввод числа и вывод его квадрата:
С точки зрения производительности использование блоков try..catch более накладно, чем применение условных конструкций. Поэтому по возможности вместо try..catch лучше использовать условные конструкции на проверку исключительных ситуаций.
Безопасная работа с исключениями в C#
Точный класс. Используйте наименее общий класс для отлавливания исключений, иначе это может приводить к труднообнаруживаемым ошибкам. Рассмотрим следующий код:
Содержательная обработка. Всегда обрабатывайте исключения содержательно. Код вида
скрывает проблемы, не позволяя обнаружить их при отладке или выполнении.
Результат функции. Не используйте исключения для возврата результата работы функции и не используйте специальные коды возврата для обработки ошибок. Каждому — свое. Результат функции нужно возращать, а ошибки, которые нельзя пропускать, — обрабатывать с помощью исключений.
Исходное место. Сохраняйте информацию об исходном месте возникновения исключения. Например,
В случае вызова throw e; информация о месте генерации исключения будет подменена новой строкой, поскольку создание нового исключения очистит стэк вызовов. Вместо этого нужно сделать вызов throw; который просто перевыбросит исходное исключение.
Добавление смысла. Меняйте исходное исключение, только чтобы добавить смысл, необходимый для пользователя. Например, в случае подключения к базе данных, пользователю может быть безразлична причина сбоя подключения (ошибка сокета), достаточно информации о самом сбое.
Стандартные конструкторы. Реализуйте стандартные конструкторы, иначе правильная обработка исключений может быть затруднена. Так для исключения NewException эти конструкторы таковы:
Под катом расшифровка доклада Евгения (epeshk) Пешкова с нашей конференции DotNext 2018 Piter, где он рассказал про эти и другие особенности исключений.
Привет! Меня зовут Евгений. Я работаю в компании СКБ Контур и занимаюсь разработкой системы хостинга и деплоя приложений под Windows. Суть в том, что у нас есть много продуктовых команд, которые пишут собственные сервисы и хостят их у нас. Мы предоставляем им легкое и простое решение разнообразных инфраструктурных задач. Например, проследить за потреблением системных ресурсов или докинуть реплик к сервису.
Иногда получается, что приложения, которые хостятся в нашей системе, разваливаются. Мы видели очень много способов, как приложение может упасть в рантайме. Один из таких способов — это выкинуть какой-нибудь неожиданный и фееричный exception.
Access Violation
Это исключение случается при некорректных операциях с памятью. Например, если приложение пробует обратиться к области памяти, к которой у него нет доступа. Исключение низкоуровневое, и обычно, если оно случилось, предстоит очень долгая отладка.
Попробуем получить это исключение, используя C#. Для этого запишем байт 42 по адресу 1000 (будем считать, что 1000 — это достаточно случайный адрес и у нашего приложения, скорее всего, доступа к нему нет).
WriteByte делает как раз то, что нам нужно: записывает байт по заданному адресу. Мы ожидаем, что этот вызов выбросит AccessViolationException. Этот код действительно выбросит это исключение, его удастся обработать и приложение продолжит работать. Теперь немного изменим код:
Если вместо WriteByte использовать метод Copy и скопировать байт 42 по адресу 1000, то, используя try-catch, AccessViolation поймать не получится. При этом на консоль будет выведено сообщение о том, что приложение завершено из-за необработанного AccessViolationException.
Получается, что у нас есть две строчки кода, при этом первая крашит все приложение с AccessViolation, а вторая выбрасывает обрабатываемое исключение того же типа. Чтобы понять, почему так происходит, мы посмотрим на то, как устроены эти методы изнутри.
Начнем с метода Copy.
Теперь поймём, почему удалось обработать AccessViolation при использовании метода WriteByte. Посмотрим на код этого метода:
Этот метод реализован полностью в managed-коде. Здесь используется C#-pointer, чтобы писать данные по нужному адресу, а также перехватывается NullReferenceException. Если перехватили NRE — выбрасывается AccessViolationException. Так нужно из-за спецификации. При этом все исключения, выброшенные конструкцией throw — обрабатываемые. Соответственно, если при выполнении кода внутри WriteByte произойдёт NullReferenceException — мы сможем поймать AccessViolation. Мог ли произойти NRE, в нашем случае, при обращении не к нулевому адресу, а к адресу 1000?
Перепишем код с использованием C# pointers напрямую, и увидим, что при обращении к ненулевому адресу действительно выбрасывается NullReferenceException:
Давайте разберемся, почему NullReference выбрасывается не только при обращении по нулевому адресу. Представьте, что вы обращаетесь к полю объекта ссылочного типа, и ссылка на этот объект нулевая:
В этой ситуации мы ожидаем получить NullReferenceException. Обращение к полю объекта происходит по смещению относительно адреса этого объекта. Получится, что мы обратимся к адресу, достаточно близкому к нулю (вспомним, что ссылка на наш исходный объект — нулевая). С таким поведением рантайма мы получим ожидаемое исключение без дополнительной проверки адреса самого объекта.
Но что же происходит, если мы обращаемся к полю объекта, а сам этот объект занимает больше, чем 64 КБ?
Можем ли мы в этом случае получить AccessViolation? Проведем эксперимент. Создадим очень большой объект и будем обращаться к его полям. Одно поле – в начале объекта, второе – в конце:
Оба метода выбросят NullReferenceException. Никакого AccessViolationException не произойдет.
Посмотрим на инструкции, которые будут сгенерированы для этих методов. Во втором случае JIT-компилятор добавил дополнительную инструкцию cmp, которая обращается к адресу самого объекта, тем самым вызывая AccessViolation с нулевым адресом, который будет преобразован рантаймом в NullReferenceException.
Стоит отметить, что для этого эксперимента недостаточно использовать в качестве большого объекта массив. Почему? Оставим этот вопрос читателю, пишите идеи в комментариях 🙂
Подведем краткий итог экспериментов с AccessViolation.
AccessViolationException ведёт себя по-разному в зависимости от того, где исключение произошло (в managed-коде или в нативном). Кроме того, если исключение произошло в managed-коде, то будет проверяться адрес объекта.
В нашем продакшене была вот такая ситуация:
В этот хелпер мы передавали action из нашего приложения. Так получилось, что он падал с AccessViolation. В результате наше приложение постоянно логгировало AccessViolation, вместо того, чтобы упасть, т.к. код в библиотеке под 3.5 мог его поймать. Нужно обратить внимание, что перехватываемость зависит не от версии рантайма, на котором запущено приложение, а от TargetFramework, под который было собрано приложение, и его зависимости.
Подводим итог. Обработка AccessVilolation зависит от того, где он произошел — в нативном или управляемом коде — а также от TargetFramework и настроек рантайма.
Thread Abort
Иногда в коде нужно остановить выполнение одного из потоков. Для этого можно использовать метод thread.Abort();
При вызове метода Abort в останавливаемом потоке выбрасывается исключение ThreadAbortException. Разберём его особенности. Например, такой код:
Абсолютно эквивалентен такому:
Если всё-таки нужно обработать ThreadAbort и выполнить еще какие-то действия в останавливаемом потоке, то можно использовать метод Thread.ResetAbort(); Он прекращает процесс остановки потока и исключение перестаёт прокидываться выше по стеку. Важно понимать, что метод thread.Abort() сам по себе ничего не гарантирует — код в останавливаемом потоке может препятствовать остановке.
Еще одна особенность thread.Abort() заключается в том, что он не сможет прервать код в том случае, если он находится в блоках catch и finally.
Внутри кода фреймворка часто можно встретить методы, у которых блок try пустой, а вся логика находится внутри finally. Это делается как раз с той целью, чтобы этот код не могла быть прерван ThreadAbortException.
Также вызов метода thread.Abort() дожидается выброса ThreadAbortException. Объединим эти два факта и получим, что метод thread.Abort() может заблокировать вызывающий поток.
В реальности с этим можно столкнуться при использовании конструкции using. Она разворачивается в try/finally, внутри finally вызывается метод Dispose. Он может быть сколь угодно сложным, содержать вызовы обработчики событий, использовать блокировки. И если thread.Abort был вызван во время выполнения Dispose — thread.Abort() будет его ждать. Так мы получаем блокировку почти на пустом месте.
OUT OF MEMORY
Это исключение можно получить, если памяти на машине оказалось меньше, чем требуется. Или когда мы уперлись в ограничения 32-битного процесса. Но получить его можно, даже если на компьютере много свободной памяти, а процесс — 64-битный.
Код выше выкинет OutOfMemory. Все дело в том, что в дотнете по умолчанию не разрешены объекты более 2 ГБ. Это можно исправить настройкой gcAllowVeryLargeObjects в App.config. В этом случае массив размером 4 ГБ создастся.
А теперь попробуем создать массив ещё больше.
Это реализация метода string.Concat. Если длина строки-результата будет больше, чем int.MaxValue, то сразу выбрасывается OutOfMemoryException.
Перейдем к ситуации, когда OutOfMemory возникает по делу, когда реально заканчивается память.
Сначала мы ограничиваем память нашего процесса в 64 мБ. Далее внутри цикла выделяем новые массивы байтов, сохраняем их в какой-то лист, чтобы GC их не собирал, и пытаемся поймать OutOfMemory.
В этом случае может произойти все что угодно:
В виртуальной памяти страницы могут быть не только отображены в физическую память, но и могут быть зарезервированными (reserved). Если страница зарезервирована, то приложение отметило, что собирается её использовать. Если страница уже отображена в реальную память или своп, то она называется «закоммиченной» (committed). Стек использует такую возможность разделять память на зарезервированную и закомиченную. Выглядит это примерно так:
Получается, что мы вызываем метод WriteLine, который занимает какое-то место на стеке. Так получается, что уже вся закоммиченная память закончилась, значит операционная система в этот момент должна взять еще одну зарезервированную страницу стека и отобразить ее в реальную физическую память, которая уже заполнена массивами байтов. Это и приводит к исключению StackOverflow.
Следующий код позволит на старте потока закоммитить всю память под стек сразу.
Кроме того, можно использовать настройку рантайма disableCommitThreadStack. Её нужно отключить, чтобы стек потока коммитился заранее. Стоит отметить, что поведение по умолчанию описанное в документации и наблюдаемое в реальности — различно.
Stack Overflow
Разберёмся подробнее со StackOverflowException. Посмотрим на два примера кода. В одном из них мы запускаем бесконечную рекурсию, которая приводит к переполнению стека, во втором мы просто выбрасываем это исключение с помощью throw.
Так как все исключения, выброшенные с помощью throw, обрабатываемы, то во втором случае мы поймаем исключение. А с первым случаем все интереснее. Обратимся к MSDN:
«You cannot catch stack overflow exceptions, because the exception-handling code may require the stack.»
MSDN
Здесь сказано, что мы не сможем перехватить StackOverflowException, так как сам перехват может потребовать дополнительного места в стеке, который уже закончился.
Чтобы как-нибудь защититься от этого исключения, можно поступить следующим образом. Во-первых, можно ограничить глубину рекурсии. Во-вторых, можно использовать методы класса RuntimeHelpers:
В C# 7.2 появилась возможность использовать Span и stackallock вместе без использования unsafe-кода. Возможно, благодаря этому stackalloc станет использоваться в коде чаще и будет полезно иметь способ защититься от StackOverflow при его использовании, выбирая, где именно выделить память. В качестве такого способа предложены метод, проверяющий возможность аллокации на стеке и конструкция trystackalloc.
Вернёмся к документации по StackOverflow на MSDN
Instead, when a stack overflow occurs in a normal application, the Common Language Runtime (CLR) terminates the process.»
MSDN
Если есть «normal» application, которые падают при StackOverflow, значит есть и не-«normal» application, которые не падают? Для того, чтобы ответить на этот вопрос придется спуститься на уровень ниже с уровня управляемого приложения на уровень CLR.
Приложение, которое хостит CLR может переопределить поведение при переполнении стека так, чтобы вместо завершения всего процесса выгружался Application Domain, в потоке котором это переполнение произошло. Таким образом, мы можем превратить StackOverflowException в AppDomainUnloadedException.
Следующий код конфигурирует CLR-host так, чтобы при StackOverflow выгружался AppDomain (C++):
Хороший ли это способ спастись от StackOverflow? Наверно, не очень. Во-первых, нам пришлось написать код на C++, чего делать не хотелось бы. Во-вторых, мы должны поменять свой C#-код так, чтобы та функция, которая может выбросить StackOverflowException выполнялась в отдельном AppDomain’е и в отдельном потоке. Наш код сразу превратится вот в такую лапшу:
Ради того, чтобы вызвать метод InfiniteRecursion, мы написали кучу строк. В-третьих, мы начали использовать AppDomain. А это почти гарантирует кучу новых проблем. В том числе, с исключениями. Рассмотри пример:
Так как наше исключение не помечено как сериализуемое, то наш код упадет с исключением SerializationException. И чтобы исправить эту проблему, нам недостаточно пометить наше исключение атрибутом Serializable, еще потребуется реализовать дополнительный конструктор для сериализации.
Это все получается не очень красиво, поэтому идём дальше — на уровень операционной системы и хаков, которые не стоит использовать в продакшене.
SEH/VEH
Обратите внимание, что если между Managed и CLR летали Managed-exceptions, то между CLR и Windows летают SEH-exceptions.
SEH – Structured Exception Handling
На самом деле, конструкция throw тоже работает через SEH.
Здесь стоит отметить, что код у CLR-exception всегда один и тот же, поэтому какой бы тип исключения мы не выбрасывали, оно всегда будет обрабатываемым.
VEH — это векторная обработка исключений, расширение SEH, но работающее на уровне процесса, а не на уровне одного потока. Если SEH по семантике схож с try-catch, то VEH по семантике схож с обработчиком прерываний. Мы просто задаем свой обработчик и можем получать информацию обо всех исключениях, которые происходят в нашем процессе. Интересная возможность VEH — это то, что он позволяет изменить SEH-исключение до того, как оно попадет в обработчик.
С VEH можно взаимодействовать через WinApi:
В Context находится информация о состоянии всех регистров процессора в момент исключения. Нас же будет интересовать EXCEPTION_RECORD и поле ExceptionCode в нем. Мы можем подменить его на собственный код исключения, о котором CLR вообще ничего не знает. Векторный обработчик выглядит так:
Теперь сделаем обёртку, устанавливающую векторный обработчик в виде метода HandleSO, который принимает в себя делегат, который потенциально может упасть со StackOverflowException (для наглядности в коде нет обработки ошибок функций WinApi и удаления векторного обработчика).
Внутри него также используется метод SetThreadStackGuarantee. Этот метод резервирует место на стеке под обработку StackOverflow.
Таким образом мы можем пережить вызов метода с бесконечной рекурсией. Наш поток продолжит работать как ни в чем не бывало, как будто никакого переполнения не происходило.
Но, что произойдет, если вызвать HandleSO дважды в одном потоке?
А произойдёт AccessViolationException. Вернемся к устройству стека.
Операционная система умеет детектировать переполнение стека. В самом верху стека лежит специальная страница, помеченная флагом Guard page. При первом обращении к этой странице произойдет другое исключение – STATUS_GUARD_PAGE_VIOLATION, а флаг Guard page со страницы снимается. Если просто перехватить это переполнение, то этой страницы на стеке больше не будет – при следующем переполнении операционная система не сможет этого понять и stack-pointer выйдет за границы памяти, выделенной под стек. Как итог — произойдет AccessViolationException. Значит нужно восстанавливать флаги страниц после обработки StackOverflow – cамый простой способ это сделать – использовать метод _resetstkoflw из библиотеки рантайма C (msvcrt.dll).
Изучив практические вопросы, подведем общие итоги:
Ссылки
22-23 ноября Евгений выступит на DotNext 2018 Moscow с докладом «Системные метрики: собираем подводные камни». А еще в Москву приедут Джеффри Рихтер, Грег Янг, Павел Йосифович и другие не менее интересные спикеры. Темы докладов можно посмотреть здесь, а купить билеты — здесь. Присоединяйтесь!
Что такое исключения в C# и как работать с блоками try, catch и finally
Содержание статьи:
Вступление
Часто во время выполнения программ возникают ошибки, которые не связаны с плохим написанием кода. Например, программе нужно найти пользователя, который отсутствует в базе данных, или подключенный файл оказался поврежденным.
Некоторые из ошибок происходят случайным образом и их невозможно предвидеть. Их называют исключениями.
Try, catch, finally и throw: основные команды в С#
Exception: типы исключений в C#
В таблице ниже перечислим несколько свойств.
| Свойство | Функция |
| InnerException | Выдача экземпляра класса, вызвавшего исключение |
| Source | Указание на имя объекта, который вызвал исключение |
| Message | Текст ошибки |
| HelpLink | Ссылка на файл справки, связанный с исключениями. |
Если нужно обработать только определенные ошибки, используют конкретный тип исключений.
Давайте рассмотрим несколько наиболее популярных.
| Тип исключения | Значение |
| ArgumenOutOfRangeException | Значение аргумента не соответствует допустимому диапазону |
| IndexOutOfRangeException | Выход за диапазон массива или допустимых значений |
| StackOverflowException | Переполнение стека |
| OutOfMemoryException | Недостаточно памяти для выполнения программы |
| NullReferenceException | Обращение к неопределенному объекту |
Пример c try и catch в C# (example)
Вот пример обработки исключения типа IndexOutOfRangeException с операторами try и catch в C#:
Если менее конкретные исключения будут записаны перед более конкретными, то компилятор может выдать ошибку из-за невозможности достигнуть следующего блока.
Пример многоразового использования блока catch в C#:
Алгоритм выполнения операторов try, catch и finally в C#
Теперь давайте посмотрим поближе на пример использования try и catch в C#:
Ошибки в приложениях с визуальным интерфейсом
Все вышеуказанные правила работают в консольных приложениях. Если в программы есть интерфейс, то при возникновении ошибки увидим соответствующее окно:
Пример ошибки в Microsoft Visual Studio
В таком случае разработчик может попробовать продолжить выполнение программы, прервать его или изменить настройки и обработать исключение.
Генерирование исключительных ситуаций
Например, у нас есть платформа для разработки ASP.NET. Платформа должна разрешать пользователю иметь только один сеанс в системе и выдавать сообщение об ошибке, если он вошел с другого окна браузера.
Фильтры и условные конструкции
В заключение
Реализовать обработку исключений в C# можно разными способами. Некоторые распространенные ошибки возможно уловить с помощью стандартных типов исключений. Нужно понимать свойства ошибок, чтобы учиться с ними работать. Также важно учитывать последовательность исключений.
Помните, что в случае, если C# не имеет функционала для предотвращения такого исключения, его можно создать самостоятельно.
Видео: обработка исключений в языке программирования C#






