Winforms правильная обработка исключений на c. Отладка и обработка исключительных ситуаций

Доброго времени суток! Сегодня мы поговорим о том, как обрабатывать ошибки в программах, написанных на языке C#.

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

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

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

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

Try { //Потенциально опасный блок кода }catch(тип_ошибок) { //Обработка ошибок } //Дальнейшие операторы программы

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

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

Давайте модифицируем код программы из предыдущего урока, добавим контроль и обработку ошибок, которые могут возникнуть при вводе пользователем некорректном данных.

//Потенциально опасный блок try { //Приглашение пользователю ввести первое число Console.Write("Введите первое число и нажмите клавишу Enter: "); //Получение первой строки string firstString = Console.ReadLine(); //Преобразование первой строки в число int firstArg = Convert.ToInt32(firstString); //Приглашение пользователю ввести второе число Console.Write("Введите первое число и нажмите клавишу Enter: "); //Получение второй строки string secondString = Console.ReadLine(); //Преобразование второй строки в число int secondArg = Convert.ToInt32(secondString); //Сложение двух переменных int result = firstArg + secondArg; //Вывод результата Console.WriteLine("Результат сложения введенных чисел: " + result.ToString()); } //Блок обработки ошибок, SystemException - самый общий тип ошибок catch (SystemException) { Console.WriteLine("Во время выполнения программы произошла ошибка, вероятно, были введены некорректные данные!"); }

Теперь, если пользователь введет некорректные данные вместо целого числа, то произошедшая ошибка будет обработана. Если вставить этот код в метод «Main» , собрать и запустить программу, то можно будет увидеть следующее поведение:

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

Последнее обновление: 23.10.2018

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

Try { } catch { } finally { }

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

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

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

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

Class Program { static void Main(string args) { int x = 5; int y = x / 0; Console.WriteLine($"Результат: {y}"); Console.WriteLine("Конец программы"); Console.Read(); } }

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

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

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

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

Class Program { static void Main(string args) { try { int x = 5; int y = x / 0; Console.WriteLine($"Результат: {y}"); } catch { Console.WriteLine("Возникло исключение!"); } finally { Console.WriteLine("Блок finally"); } Console.WriteLine("Конец программы"); Console.Read(); } }

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

Int y = x / 0;

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

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

Возникло исключение! Блок finally Конец программы

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

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

Try { int x = 5; int y = x / 0; Console.WriteLine($"Результат: {y}"); } catch { Console.WriteLine("Возникло исключение!"); }

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

Try { int x = 5; int y = x / 0; Console.WriteLine($"Результат: {y}"); } finally { Console.WriteLine("Блок finally"); }

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

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

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

Static void Main(string args) { Console.WriteLine("Введите число"); int x = Int32.Parse(Console.ReadLine()); x *= x; Console.WriteLine("Квадрат числа: " + x); Console.Read(); }

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

Static void Main(string args) { Console.WriteLine("Введите число"); int x; string input = Console.ReadLine(); if (Int32.TryParse(input, out x)) { x *= x; Console.WriteLine("Квадрат числа: " + x); } else { Console.WriteLine("Некорректный ввод"); } Console.Read(); }

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

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

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

Отладка и инструментальная среда Visual Studio .Net

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

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

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

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

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

Обработка исключений в языках C/C++

Для стиля программирования на языке C характерно описание методов класса как булевых функций, возвращающих true в случае нормального завершения метода и false - при возникновении исключительной ситуации . Вызов метода встраивался в If -оператор, обрабатывающий ошибку в случае неуспеха завершения метода:

bool MyMethod(...){...} if !MyMethod(){// обработка ошибки} {//нормальное выполнение}

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

Поэтому в C/C++ применяется схема try/catch блоков, суть которой в следующем. Участок программы, в котором может возникнуть исключительная ситуация , оформляется в виде охраняемого try-блока. Если при его выполнении возникает исключительная ситуация , то происходит прерывание выполнения try-блока c классификацией исключения. Это исключение начинает обрабатывать один из catch-блоков, соответствующий типу исключения . В C/C++ применяются две такие схемы. Одна из них - схема с возобновлением - соответствует так называемым структурным, или С-исключениям. Вторая схема - без возобновления - соответствует С++ исключениям. В первой схеме обработчик исключения - catch-блок - возвращает управление в некоторую точку try-блока. Во второй схеме управление не возвращается в try-блок.

С некоторыми синтаксическими отличиями схема с возобновлением применяется в языках VB/VBA.

Схема обработки исключений в C#

Язык C# наследовал схему исключений языка С++, внеся в нее свои коррективы. Рассмотрим схему подробнее и начнем с синтаксиса конструкции try-catch-finally :

try {...} catch (T1 e1) {...} ... catch(Tk ek) {...} finally {...}

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

Выбрасывание исключений. Создание объектов Exception

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

Синтаксически оператор throw имеет вид:

throw[выражение]

Выражение throw задает объект класса, являющегося наследником класса Exception . Обычно это выражение new , создающее новый объект. Если оно отсутствует, то повторно выбрасывается текущее исключение. Если исключение выбрасывается операционной системой, то она сама классифицирует исключение, создает объект соответствующего класса и автоматически заполняет его поля.

В рассматриваемой нами модели исключения являются объектами, класс которых представляет собой наследника класса Exception . Этот класс и многочисленные его наследники является частью библиотеки FCL, хотя и разбросаны по разным пространствам имен. Каждый класс задает определенный тип исключения в соответствии с классификацией, принятой в Framework .Net. Вот лишь некоторые классы исключений из пространства имен System : ArgumentException , ArgumentOutOfRangeException , ArithmeticException , BadImageFormatException , DivideByZeroException , OverflowException . В пространстве имен System.IO собраны классы исключений , связанных с проблемами ввода-вывода: DirectoryNotFoundException , FileNotFoundException и многие другие. Имена всех классов исключений заканчиваются словом Exception . Разрешается создавать собственные классы исключений , наследуя их от класса Exception .

При выполнении оператора throw создается объект te , класс TE которого характеризует текущее исключение, а поля содержат информацию о возникшей исключительной ситуации . Выполнение оператора throw приводит к тому, что нормальный процесс вычислений на этом прекращается. Если это происходит в охраняемом try-блоке, то начинается этап "захвата" исключения одним из обработчиков исключений.

Захват исключения

Блок catch - обработчик исключения имеет следующий синтаксис:

catch (T e) {...}

Класс T , указанный в заголовке catch -блока, должен принадлежать классам исключений . Блок catch с формальным аргументом e класса T потенциально способен захватить текущее исключение te класса TE , если и только если объект te совместим по присваиванию c объектом e. Другими словами, потенциальная способность захвата означает допустимость присваивания e = te , что возможно, когда класс TE является потомком класса T . Обработчик, класс T которого является классом Exception , является универсальным обработчиком , потенциально он способен захватить любое исключение, поскольку все они являются его потомками.

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

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

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

В .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 поддерживается стандартная методика для генерации и выявления ошибок в исполняющей среде, называемая структурированной обработкой исключений (SEH - structured exception handling) .

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

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

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

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

При обнаружении ошибки код осуществляет генерацию исключения (создание экземпляра класса исключения) и выдает его следующим образом:

Throw new IndexOutOfRangeException(“Вы ввели: ” + userInput);

Как только компилятор встречает оператор throw внутри блока try, он немедленно ищет соответствующий блок catch.

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

Оператор throw может находиться в любом методе, вызванном во время выполнения блока try, — оно не обязано располагаться в том же самом методе, в котором определен блок try

Обработчики исключений для производного класса (IndexOutOfRangeException) должны идти раньше, чем для базового (Exception) (!)

Если обработчик catch записан как: catch { … } то это значит, что он отвечает за любой код (за любое возникшее исключение), в том числе написанным не на C# или не управляемым.

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

While(true) // бесконечный цикл { Console.Write("Выберите пункт меню от 0 до 5 или для выхода: "); string userInput = Console.ReadLine(); if (userInput == "") {Console.WriteLine("Вы нажали значт выходим..."); break; } try { int x = int.Parse(userInput); switch (x) { case 0: Console.WriteLine("Выбран пункт 0"); break; case 1: Console.WriteLine("Выбран пункт 1"); break; case 2: Console.WriteLine("Выбран пункт 2"); break; case 3: Console.WriteLine("Выбран пункт 3"); break; case 4: Console.WriteLine("Выбран пункт 4"); break; case 5: Console.WriteLine("Выбран пункт 5"); break; default: throw new IndexOutOfRangeException(); break; } } catch (IndexOutOfRangeException e) { Console.WriteLine("Неверное значение. Выберити цифру от "); } catch (FormatException e) { Console.WriteLine("Ошибка преобразования, возможно вы ввели строку..."); } catch (Exception e) { Console.WriteLine("Еще какая-то ошибка"); } catch {} // обработчик неуправляемого кода и кода на другом языке }

Свойства и методы System.Exception

if (ErrorCondition == true) { Exception myException = new ClassMyException("Help!"); myException.Source = "Название приложения"; myException.HelpLink = "myHelpFile.txt"; throw myException; }

В коде не нужно генерировать исключения от общего класса System.Exception – он не дает представления о природе ошибочного состояния (!). В иерархии существует два важных класса:

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

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

IOException (пространство имен System.IO) связаны с чтением и записью данных в файл.

StackOverflowException возникает тогда, когда участок памяти отведенный под стек, заполняется до отказа. Переполнение стека может возникнуть например в том случае, когда метод начинает рекурсивно вызвать самого себя. Обычной причиной появления EndOfStreamException является попытка чтения за границами файла. Переполнение OverflowException возникает, например при попытке привести int, содержащий -40 к типу uint в контексте checked.

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

— с целью изменения типа сгенерированного исключения (использование свойства InnerException, которое содержит ссылку на любое сгенерированное исключение);

— обработка разных исключений в разных участках кода.

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

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

Class LandLineSpyFoundException: ApplicationException { public LandLineSpyFoundException(string spyName) : base("шпийон"+spyName) { } public LandLineSpyFoundException(string spyName, Exception innerException) : base(spyName, innerException) { } }



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: