Обработка ошибок в си шарп

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

Конструкция try..catch..finally

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

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

try
{
	
}
catch
{
	
}
finally
{
	
}

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

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

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

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

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

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

Исключения в C#

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

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

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

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

В данном случае у нас опять же возникнет исключение в блоке 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, то исключение не будет обработано, и программа аварийно завершится.

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

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

Square("12"); // Квадрат числа 12: 144
Square("ab"); // !Исключение

void Square(string data)
{
    int x = int.Parse(data);
    Console.WriteLine($"Квадрат числа {x}: {x * x}");
}

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

Square("12"); // Квадрат числа 12: 144
Square("ab"); // Некорректный ввод

void Square(string data)
{
    if (int.TryParse(data, out var x))
    {
        Console.WriteLine($"Квадрат числа {x}: {x * x}");
    }
    else
    {
        Console.WriteLine("Некорректный ввод");
    }
}

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

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

Содержание

  • Исключения (Exceptions) и инструкция try
  • Оговорка catch
  • Блок finally
  • Инструкция using
  • Выбрасывание исключений
  • Основные свойства System.Exception
  • Основные типы исключений
  • Директивы препроцессора
    • Pragma Warning
    • Атрибут Conditional
  • Классы Debug и Trace
    • TraceListener
    • Fail и Assert

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

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

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

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

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

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

try

{

  ... // в пределах этого блока может быть выброшено исключение

}

catch (ExceptionA ex)

{

  ... // обработчик исключений типа ExceptionA

}

catch (ExceptionB ex)

{

  ... // обработчик исключений типа ExceptionB

}

finally

{

  ... // код очистки

}

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

int x = 3, y = 0;

Console.WriteLine (x / y);

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

try

{

  int x = 3, y = 0;

  Console.WriteLine (x / y);

}

catch (DivideByZeroException ex)

{

  Console.Write («y cannot be zero. «);

}

// выполнение программы продолжится отсюда

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

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

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

Оговорка catch

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

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

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

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

try

{

  DoSomething();

}

catch (IndexOutOfRangeException ex) { ... }

catch (FormatException ex) { ... }

catch (OverflowException ex) { ... }

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

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

catch (StackOverflowException) // без переменной

{ ... }

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

Блок finally

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

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

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

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

static void ReadFile()

{

  StreamReader reader = null;

  try

  {

      reader = File.OpenText («file.txt»);

      if (reader.EndOfStream) return;

      Console.WriteLine (reader.ReadToEnd());

  }

  finally

  {

      if (reader != null) reader.Dispose();

  }

}

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

Инструкция using

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

using (StreamReader reader = File.OpenText («file.txt»))

{

  ...

}

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

StreamReader reader = File.OpenText («file.txt»);

try

{

  ...

}

finally

{

  if (reader != null) ((IDisposable)reader).Dispose();

}

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

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

static void Display (string name)

{

  if (name == null)

  throw new ArgumentNullException («name»);

  Console.WriteLine (name);

}

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

try { ... }

catch (Exception ex)

{

  // логирование ошибки

  ...

  throw; // повторное выбрасывание того же самого исключения

}

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

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

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

try

{

  ... // парсинг даты рождения из xml-данных

}

catch (FormatException ex)

{

  throw new XmlException («Неправильная дата рождения», ex);

}

В таких случаях необходимо передать исходное исключение в качестве первого параметра конструктора нового исключения, ссылка на объект исходного исключения позже будет доступна через свойство 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).

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

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

#define DEBUG

class MyClass

{

  int x;

  void Foo()

  {

      # if DEBUG

      Console.WriteLine («Testing: x = {0}», x);

      # endif

  }

}

В этом классе инструкции в методе Foo скомпилируются если определен символ DEBUG, а если его удалить — инструкции не скомпилируются. Символы препроцессора могут быть определены в исходном коде (как в примере), а могут быть переданы компилятору в командной строке с помощью параметра /define:symbol.

С директивами #if и #elif можно использовать операторы ||, && и ! с несколькими символами:

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

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

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

    using TestType =

      #if V2

          MyCompany.Widgets.GadgetV2;

      #else

          MyCompany.Widgets.Gadget;

      #endif

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

Полный список директив препроцессора:

  • #define symbol — определяет символ
  • #undef symbol — удаляет символ
  • #if symbol [оператор symbol2]... — условная компиляция; допустимые операторы ==, !=, && и ||
  • #else — выполняет код после #endif
  • #elif symbol [оператор symbol2] — объединяет #else и #if
  • #endif — конец условных директив
  • #warning text — текст предупреждения, которое появится в выдаче компилятора
  • #error text — текст ошибки, которая появится в выдаче компилятора
  • #line [число["файл"] | hidden]число указывает номер строки в исходном коде; файл — имя файла, которое появится в выдаче компилятора; hidden — дает указание дебагеру пропустить код от этой точки до следующей директивы #line
  • #region name — отмечает начало области
  • #endregion — отмечает конец области
  • #pragma warning

Pragma Warning

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

public class Foo

{

  static void Main() { }

  #pragma warning disable 414

  static string Message = «Hello»;

  #pragma warning restore 414

}

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

Если не указывать номер директива #pragma warning отменит или восстановит вывод всех предупреждений.

Если скомпилировать программу с параметром /warnaserror, то все не отмененные директивой #pragma warning предупреждения будут расцениваться компилятором как ошибки.

Атрибут Conditional

Атрибут Conditional указывает компилятору на необходимость игнорировать все обращения к определенному классу или методу, если заданный символ не был определен:

[Conditional («LOGGINGMODE»)]

static void LogStatus (string msg)

{

  ...

}

Это равносильно тому, что каждый вызов метода будет окружен условными директивами:

#if LOGGINGMODE

LogStatus («Message Headers: « + GetMsgHeaders());

#endif

Классы Debug и Trace

Статические классы Debug и Trace предлагают базовые возможности логирования. Оба класса схожи, отличие заключается в их назанчении. Класс Debug предназначен для отладочных сборок, класс Trace — для отладочных и финальных. В связи с этим все методы класса Debug определены с атрибутом [Conditional("DEBUG")], а методы класса Trace — с атрибутом [Conditional("TRACE")]. Это значит, что все обращения к Debug и Trace будут подавляться компилятором, пока не определен символ DEBUG или TRACE.

Класс Debug и Trace определяют методы Write, WriteLine и WriteIf. По умолчанию они отправляют сообщения в окно вывода отладчика:

Debug.Write («Data»);

Debug.WriteLine (23 * 34);

int x = 5, y = 3;

Debug.WriteIf (x > y, «x is greater than y»);

Класс Trace также содержит методы TraceInformation, TraceWarning и TraceError. Их действия зависят от зарегистрированных прослушивателей.

TraceListener

Классы Debug и Trace имеют свойство Listeners, которое представляет собой статическую коллекцию экземпляров TraceListener. Они отвечают за обработку данных, возвращаемых методами Write, Fail и Trace.

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

  • при подключении к отладчику (например, Visual Studio) сообщения записываются в окно вывода отладчика, во всех остальных случаях сообщения игнорируются
  • при вызове метода Fail отображается диалоговое окно, запрашивающее у пользователя дальнейшие действия: продолжить, прервать или повторить отладку (независимо от того, подключен ли отладчик)

Это поведение можно изменить или дополнить, удалив (на обязательно) стандартный прослушиватель и/или добавив один или более собственных прослушивателей.

Прослушиваетли трассировки можно написать с нуля (создав производный класс от TraceListener) или воспользоваться готовыми классами:

  • TextWriterTraceListener записывает в Stream или TextWriter или добавляет в файл; имеет четыре подкласса: ConsoleTraceListener, DelimitedListTraceListener, XmlWriterTraceListener и EventSchemaTraceListener
  • EventLogTraceListener записывает в журнал событий Windows
  • EventProviderTraceListener записывает в систему трассировки событий Windows (Event Tracing for Windows — ETW)
  • WebPageTraceListener выводит на веб-страницу ASP.NET

Ни один из этих прослушивателе не отображает диалоговое окно при вызове Fail, это делает только DefaultTraceListener.

// Удалить стандартный прослушиватель, очистив коллекцию прослушивателей:

Trace.Listeners.Clear();

// Добавить средство записи в файл trace.txt:

Trace.Listeners.Add (new TextWriterTraceListener («trace.txt»));

// Добавит средство записи в консоль:

System.IO.TextWriter tw = Console.Out;

Trace.Listeners.Add (new TextWriterTraceListener (tw));

// Добавить средство записи в журнал событий Windows:

if (!EventLog.SourceExists («DemoApp»))

  EventLog.CreateEventSource («DemoApp», «Application»);

Trace.Listeners.Add (new EventLogTraceListener («DemoApp»));

В случае журнала событий Windows сообщения, отправляемые с помощью Write, Fail или Assert, записываются как сведения, а сообщения методов TraceWarning и TraceError записываются как предупреждения или ошибки.

Каждый экземпляр TraceListener имеет свойство Filter и TraceFilter, с помощью которых можно управлять, будет ли сообщение записано в этот прослушиватель. Для этого необходимо создать экземпляр классов EventTypeFilter или SourceFilter (производных от TraceFilter) или создать свой класс, наследующий от TraceFilter и переопределить в нем метод ShouldTrace.

В TraceListener также определены свойства IndentLevel и IndentSize для управления отступами и свойство TraceOutputOptions для записи дополнительных данных:

TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out);

tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack;

// Это применяется при использовании метода Trace:

Trace.TraceWarning («Orange alert»);

DiagTest.vshost.exe Warning: 0 : Orange alert

DateTime=20070308T05:57:13.6250000Z

Callstack= at System.Environment.GetStackTrace(Exception e, Boolean

needFileInfo)

at System.Environment.get_StackTrace() at ...

Прослушиватели, которые записывают данные в поток, кэшируются. По этой причине данные не появляются в потоке немедленно, а также поток перед завершением приложения должен быть закрыт, или хотя бы сброшен, чтоб не потерять данные в кэше. Для этой цели классы Trace и Debug содержат статические методы Close и Flush, которые вызывают Close и Flush во всех прослушивателях (а они в свою очередь закрывают или сбрасывают все потоки). Метод Close вызывает метод Flush, закрывает файловые дескрипторы и предотвращает дальнейшую запись.

Классы Trace и Debug также определяют свойство AutoFlush, которое если равно true вызывает Flush после каждого сообщения.

Fail и Assert

Классы Debug и Trace содержат методы Fail и Assert.

Метод Fail отправляет сообщения каждому TraceListener:

Debug.Fail («File data.txt does not exist!»);

Метод Assert вызывает Fail если аргумент типа bool равен false. Это называется созданием утверждения и указывает на ошибку, если оно нарушено. Можно также создать необязательное сообщение об ошибке:

Debug.Assert (File.Exists («data.txt»), «File data.txt does not exist!»);

var result = ...

Debug.Assert (result != null);

Методы Write, Fail и Assert также могут принимать категорию в виде строки ,которая может быть использована при обработке вывода.

Время на прочтение
6 мин

Количество просмотров 21K

В рамках скорого старта курса «C# Developer. Professional» подготовили для вас перевод материала.

Приглашаем также всех желающих на бесплатный демо-урок «DI-контейнеры для C#». На этом занятии мы:

1) Разберемся с тем, что такое принцип DI и зачем он нужен;
2) Научимся применять DI без использования контейнеров;
3) Рассмотрим два популярных DI-контейнеры для C#: Windsor и Autofac, разберем их плюсы и минусы;
4) Научимся регистрировать зависимости, управлять их жизненным циклом, применять инъекцию зависимостей.


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

Не генерируйте исключения повторно

Я натыкаюсь на это снова и снова. Люди оказываются сбиты с толку тем, что исходный стек трейс «волшебным образом» исчезает при обработке ошибок. Чаще всего это вызвано повторной генерацией исключений. Давайте посмотрим на пример, в котором у нас есть вложенные try/catch:

try
{
    try
    {
        // Вызов какого-либо кода, который может сгенерировать исключение SpecificException

    }
    catch (SpecificException specificException)
    {
        log.LogError(specificException, "Specific error");
    }
    

    // Вызов какого-либо кода
}
catch (Exception exception)
{
    log.LogError(exception, "General erro");
}

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

catch (SpecificException specificException)
{
    // ...
    throw specificException;
}

Или так:

catch (SpecificException specificException)
{
    // ...
    throw;
}

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

Декорируйте исключения

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

Информацию в словарь Data вносится посредством добавьте пар ключ/значение:

var exception = new Exception("En error happened");
exception.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
throw exception;

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

Вы также можете декорировать исключения, сгенерированные сторонним кодом. Добавьте try/catch:

try
{
    service.SomeCall();
}
catch (Exception e)
{
    e.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
    throw;
}

Код перехватывает любые исключения, генерируемые методом SomeCall, и добавляет в них имя пользователя. Посредством добавления ключевого слова throw в блок catch исходное исключение пробрасывается дальше по стеку.

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

Вероятнее всего, у вас есть где-то код, похожий на этот:

try
{
    File.WriteAllText(path, contents);
}
catch (Exception e)
{
    logger.Error(e);
}

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

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

try
{
    File.WriteAllText(path, contents);
}
catch (ArgumentException ae)
{
    Message.Show("Invalid path");
}
catch (DirectoryNotFoundException dnfe)
{
    Message.Show("Directory not found");
}
catch (Exception e)
{
    var supportId = Guid.NewGuid();
    e.Data.Add("Support id", supportId);
    logger.Error(e);
    Message.Show($"Please contact support with id: {supportId}");
}

Перехватывая ArgumentException и DirectoryNotFoundException перед перехватом общего Exception, я могу показать пользователю специализированное сообщение. В этих сценариях я не регистрирую исключение, поскольку пользователь может быстро исправить ошибки. В случае Exception я генерирую support id, регистрирую ошибку (используя декораторы, как показано в предыдущем разделе) и показываю сообщение пользователю.

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

Старайтесь избегать исключений

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

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

Address a = null;
var city = a.City;

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

Если вы хотите разрешить city с нулевым значением, вы можете избежать исключения, используя null-condition оператор:

Address a = null;
var city = a?.City;

Добавляя ? при доступе к a C# автоматически обрабатывает сценарий, в котором адрес равен null. В этом случае переменной city будет присвоено значение null.

Другой распространенный пример исключений — это анализ чисел или логических значений. В следующем примере будет сгенерировано FormatException:

var i = int.Parse("invalid");

Строка invalid не может быть распаршена в виде целого числа. Чтобы не оборачивать это в try/catch, int предоставляет интересный метод, который вы, вероятно, уже использовали 1000 раз:

if (int.TryParse("invalid", out int i))
{
}

В случае, если invalid может быть распаршена как int, TryParse возвращает true и помещает распаршенное значение в переменную i. Еще одно исключение удалось избежать.

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

Забавно вспоминать, как я был Java-программистом (когда .NET находился в стадии бета-тестирования). Мы создавали собственные пользовательские исключения для всего чего угодно. Возможно, это происходило из-за более явной реализации исключений в Java, но я не вижу этого в .NET и C#. Создавая пользовательское исключение, у вас гораздо больше возможностей для перехвата определенных исключений, как уже было показано. Вы можете декорировать свое исключение пользовательскими переменными, не беспокоясь о том, поддерживает ли ваш логгер словарь Data:

public class MyVerySpecializedException : Exception
{
    public MyVerySpecializedException() : base() {}
    public MyVerySpecializedException(string message) : base(message) {}
    public MyVerySpecializedException(string message, Exception inner) : base(message, inner) {}
    
    public int Status { get; set; }
}

Класс MyVerySpecializedException (возможно, это не то имя класса, которое вы должны использовать в качестве примера :D) реализует три конструктора, которые должен иметь каждый класс исключения. Кроме того, я добавил свойство Status в качестве примера дополнительных данных. Это позволит нам написать такой код:

try
{
    service.SomeCall();
}
catch (MyVerySpecializedException e) when (e.Status == 500)
{
    // Do something specific for Status 500
}
catch (MyVerySpecializedException ex)
{
    // Do something general
}

Используя ключевое слово when, я могу перехватить MyVerySpecializedException, когда значение свойства Status равно 500. Все остальные сценарии попадут в общий catch MyVerySpecializedException.

Логируйте исключения

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

try
{
    service.SomeCall();
}
catch
{
    // Игнорируется
}

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

Существует несколько отличных фреймворков для ведения логов, таких как NLog и Serilog. Если вы веб-разработчик ASP.NET (Core), запись неперехваченных исключений может выполняться автоматически с помощью elmah.io или одного из других доступных инструментов.


Узнать подробнее о курсе «C# Developer. Professional».

Смотреть вебинар «DI-контейнеры для C#».

ГЛАВА 13. Обработка исключительных ситуаций

Исключительная ситуация, или просто исключение,
происходит во время выполнения. Используя под­
систему обработки исключительных ситуаций в С#,
можно обрабатывать структурированным и контроли­
руемым образом ошибки, возникающие при выполнении
программы. Главное преимущество обработки исключи­
тельных ситуаций заключается в том, что она позволяет ав­
томатизировать получение большей части кода, который
раньше приходилось вводить в любую крупную програм­
му вручную для обработки ошибок. Так, если программа
написана на языке программирования без обработки ис­
ключительных ситуаций, то при неудачном выполнении
методов приходится возвращать коды ошибок, которые не­
обходимо проверять вручную при каждом вызове метода.
Это не только трудоемкий, но и чреватый ошибками про­
цесс. Обработка исключительных ситуаций рационализи­
рует весь процесс обработки ошибок, позволяя определить
в программе блок кода, называемый обработчиком исклю­
чений и выполняющийся автоматически, когда возникает
ошибка. Эго избавляет от необходимости проверять вруч­
ную, насколько удачно или неудачно завершилась конкрет­
ная операция либо вызов метода. Если возникнет ошибка,
она будет обработана соответствующим образом обработ­
чиком ошибок.

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

Класс System.Exception

В C# исключения представлены в виде классов. Все классы исключений должны
быть производными от встроенного в C# класса Exception, являющегося частью про­
странства имен System. Следовательно, все исключения являются подклассами класса
Exception.

К числу самых важных подклассов Exception относится класс SystemException.
Именно от этого класса являются производными все исключения, генерируемые испол­
няющей системой C# (т.е. системой CLR). Класс SystemException ничего не добавляет
к классу Exception, а просто определяет вершину иерархии стандартных исключений.

В среде .NET Framework определено несколько встроенных исключений, являю­
щихся производными от класса SystemException. Например, при попытке выпол­
нить деление на нуль генерируется исключение DivideByZeroException. Как будет
показано далее в этой главе, в C# можно создавать собственные классы исключений,
производные от класса Exception.

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

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

Операторы программы, которые требуется контролировать на появление исключе­
ний, заключаются в блок try. Если внутри блока try возникает исключительная ситуа­
ция, генерируется исключение. Это исключение может быть перехвачено и обработано
каким-нибудь рациональным способом в коде программы с помощью оператора, обо­
значаемого ключевым словом catch. Исключения, возникающие на уровне системы,
генерируются исполняющей системой автоматически. А для генерирования исключе­
ний вручную служит ключевое слово throw. Любой код, который должен быть непре­
менно выполнен после выхода из блока try, помещается в блок finally.

Применение пары ключевых слов try и catch

Основу обработки исключительных ситуаций в C# составляет пара ключевых слов
try и catch. Эти ключевые слова действуют совместно и не могут быть использованы
порознь. Ниже приведена общая форма определения блоков try/catch для обработ­
ки исключительных ситуаций:

try {
    // Блок кода, проверяемый на наличие ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepTypel.
}
catch (ExcepType2 exOb) {
    // Обработчик исключения типа ExcepType2.
}

где ЕхсерТуре — это тип возникающей исключительной ситуации. Когда исключение
генерируется оператором try, оно перехватывается составляющим ему пару опера­
тором catch, который затем обрабатывает это исключение. В зависимости от типа
исключения выполняется и соответствующий оператор catch. Так, если типы гене­
рируемого исключения и того, что указывается в операторе catch, совпадают, то вы­
полняется именно этот оператор, а все остальные пропускаются. Когда исключение
перехватывается, переменная исключения exOb получает свое значение.

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

Следует, однако, иметь в виду, что если исключение не генерируется, то блок опера­
тора try завершается как обычно, и все его операторы catch пропускаются. Выполне­
ние программы возобновляется с первого оператора, следующего после завершающе­
го оператора catch. Таким образом, оператор catch выполняется лишь в том случае,
если генерируется исключение.

Простой пример обработки исключительной ситуации

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

// Продемонстрировать обработку исключительной ситуации.
using System;

class ExcDemol {
    static void Main() {
        int[] nums = new int[4];

        try {
            Console.WriteLine("До генерирования исключения.");

            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[(0)]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

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

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

В данном примере массив nums типа int состоит из четырех элементов. Но в цикле
for предпринимается попытка проиндексировать этот массив от 0 до 9, что и приво­
дит к появлению исключения IndexOutOfRangeException, когда происходит обра­
щение к элементу массива по индексу 4.

Несмотря на всю свою краткость, приведенный выше пример наглядно демон­
стрирует ряд основных моментов процесса обработки исключительных ситуаций.
Во-первых, код, который требуется контролировать на наличие ошибок, содержится в
блоке try. Во-вторых, когда возникает исключительная ситуация (в данном случае —
при попытке проиндексировать массив nums за его границами в цикле for), в блоке
try генерируется исключение, которое затем перехватывается в блоке catch. В этот
момент выполнение кода в блоке try завершается и управление передается блоку
catch. Это означает, что оператор catch не вызывается специально, а выполнение
кода переходит к нему автоматически. Следовательно, оператор, содержащий метод
WriteLine() и следующий непосредственно за циклом for, где происходит выход
индекса за границы массива, вообще не выполняется. А в задачу обработчика исклю­
чений входит исправление ошибки, приведшей к исключительной ситуации, чтобы
продолжить выполнение программы в нормальном режиме.

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

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

for(int i=0; i < 10; i++) {

на строку

for(int i=0; i < nums.Length; i++) {

Теперь индексирование массива не выходит за его границы в цикле for. Следова­
тельно, никакого исключения не генерируется и блок catch не выполняется.

Второй пример обработки исключительной ситуации

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

В качестве еще одного примера рассмотрим следующую программу, где блок try
помещается в методе Main(). Из этого блока вызывается метод GenException(), в ко­
тором и генерируется исключение IndexOutOfRangeException. Это исключение не
перехватывается методом GenException(). Но поскольку метод GenException() вы­
зывается из блока try в методе Main(), то исключение перехватывается в блоке catch,
связанном непосредственно с этим блоком try.

/* Исключение может быть сгенерировано одним методом
и перехвачено другим. */
using System;

class ExcTest {
    // Сгенерировать исключение.
    public static void GenException() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы
        массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums [{0}] : {1}", i, nums[i]);
        }

        Console.WriteLine("He подлежит выводу");
    }
}

class ExcDemo2 {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

Выполнение этой программы дает такой же результат, как и в предыдущем
примере.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

Как пояснялось выше, метод GenException() вызывается из блока try, и поэтому
генерируемое им исключение перехватывается не в нем, а в блоке catch внутри мето­
да Main(). А если бы исключение перехватывалось в методе GenException(), оно не
было бы вообще передано обратно методу Main().

Последствия неперехвата исключений

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

// Предоставить исполняющей системе C# возможность самой обрабатывать ошибки.
using System;

class NotHandled {
    static void Main() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
        }
    }
}

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

Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива.
    в NotHandled.Main() в <имя_файла>:строка 16

Это сообщение уведомляет об обнаружении в методе NotHandled.Main() необра­
ботанного исключения типа System.IndexOutOfRangeException, которое связано
с выходом индекса за границы массива.

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

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

// Не сработает!
using System;

class ExcTypeMismatch {
    static void Main() {
        int[] nums = new int[4];
        try {
            Console.WriteLine("До генерирования исключения.");
            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        /* Если перехват рассчитан на исключение DivideByZeroException,
        то перехватить ошибку нарушения границ массива не удастся. */
        catch (DivideByZeroException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

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

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива
    в ExcTypeMismatch.Main() в <имя_файла>:строка 18

Как следует из приведенного выше результата, в блоке catch, реагирующем
на исключение DivideByZeroException, не удалось перехватить исключение
IndexOutOfRangeException.

Обработка исключительных ситуаций — “изящный” способ устранения программных ошибок

Одно из главных преимуществ обработки исключительных ситуаций заключается
в том, что она позволяет вовремя отреагировать на ошибку в программе и затем про­
должить ее выполнение. В качестве примера рассмотрим еще одну программу, в кото­
рой элементы одного массива делятся на элементы другого. Если при этом происходит
деление на нуль, то генерируется исключение DivideByZeroException. Обработка
подобной исключительной ситуации заключается в том, что программа уведомляет
об ошибке и затем продолжает свое выполнение. Таким образом, попытка деления
на нуль не приведет к аварийному завершению программы из-за ошибки при ее вы­
полнении. Вместо этого ошибка обрабатывается «изящно», не прерывая выполнение
программы.

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

class ExcDemo3 {
    static void Main() {
        int[] numer = { 4, 8, 16, 32, 64, 128 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                // Перехватить исключение.
                Console.WriteLine("Делить на нуль нельзя!");
            }
        }
    }
}

Ниже приведен результат выполнения этой программы.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16

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

Применение нескольких операторов catch

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

// Использовать несколько операторов catch.
using System;

class ExcDemo4 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
            }
        }
    }
}

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

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Подходящий элемент не найден.

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

Вообще говоря, операторы catch выполняются по порядку их следования в про­
грамме. Но при этом выполняется только один блок catch, в котором тип исклю­
чения совпадает с типом генерируемого исключения. А все остальные блоки catch
пропускаются.

Перехват всех исключений

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

catch {
    // обработка исключений
}

С помощью такой формы создается «универсальный» обработчик всех исключе­
ний, перехватываемых в программе.

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

// Использовать "универсальный" обработчик исключений.
using System;

class ExcDemo5 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 );

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch { // "Универсальный" перехват.
                Console.WriteLine("Возникла некоторая исключительная ситуация.");
            }
        }
    }
}

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

4/2 равно 2
Возникла некоторая исключительная ситуация.
16/4 равно 4
32/4 равно 8
Возникла некоторая исключительная ситуация.
128 / 8 равно 16
Возникла некоторая исключительная ситуация.
Возникла некоторая исключительная ситуация.

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

ПРИМЕЧАНИЕ
В подавляющем большинстве случаев «универсальный» обработчик исключений (не при­
меняется. Как правило, исключения, которые могут быть сгенерированы в коде, обрабаты­
ваются по отдельности. Неправильное использование “универсального” обработчика может
привести к тому, что ошибки, перехватывавшиеся при тестировании программы, маскируют­
ся. Кроме того, организовать надлежащую обработку всех исключительных ситуаций в одном
обработчике не так-то просто. Иными словами, “универсальный» обработчик исключений
может оказаться пригодным лишь в особых случаях, например в инструментальном средстве
анализа кода во время выполнения.

Вложение блоков try

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

// Использовать вложенный блок try.
using System;

class NestTrys {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = ( 2, 0, 4, 4, 0, 8 );

        try { // внешний блок try
            for(int i=0; i < numer.Length; i++) {
                try { // вложенный блок try
                    Console.WriteLine(numer[i] + " / " +
                                    denom[i] + " равно " +
                                    numer[i]/denom[i]);
                }
                catch (DivideByZeroException) {
                    Console.WriteLine("Делить на нуль нельзя!");
                }
            }
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Подходящий элемент не найден.");
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

Выполнение этой программы приводит к следующему результату.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Неисправимая ошибка - программа прервана.

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

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

Генерирование исключений вручную

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

где в качестве exceptOb должен быть обозначен объект класса исключений, произво­
дного от класса Exception.

Ниже приведен пример программы, в которой демонстрируется применение опе­
ратора throw для генерирования исключения DivideByZeroException.

// Сгенерировать исключение вручную.
using System;

class ThrowDemo {
    static void Main() {
        try {
            Console.WriteLine("До генерирования исключения.");
            throw new DivideByZeroException();
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Исключение перехвачено.");
        }
        Console.WriteLine("После пары операторов try/catch.");
    }
}

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

До генерирования исключения.
Исключение перехвачено.
После пары операторов try/catch.

Обратите внимание на то, что исключение DivideByZeroException было сге­
нерировано с использованием ключевого слова new в операторе throw. Не следует
забывать, что в данном случае генерируется конкретный объект, а следовательно, он
должен быть создан перед генерированием исключения. Это означает, что сгенериро­
вать исключение только по его типу нельзя. В данном примере для создания объекта
DivideByZeroException был автоматически вызван конструктор, используемый по
умолчанию, хотя для генерирования исключений доступны и другие конструкторы.

Повторное генерирование исключений

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

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

В приведенном ниже примере программы демонстрируется повтор­
ное генерирование исключения. В данном случае генерируется исключение
IndexOutOfRangeException.

// Сгенерировать исключение повторно.
using System;

class Rethrow {
    public static void GenException() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
                throw; // сгенерировать исключение повторно
            }
        }
    }
}

class RethrowDemo {
    static void Main() {
        try {
            Rethrow.GenException();
        }
        catch(IndexOutOfRangeException) {
            // перехватить исключение повторно
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

В этом примере программы ошибки из-за деления на нуль обрабатываются локаль­
но в методе GenException(), но ошибка выхода за границы массива генерируется
повторно. В данном случае исключение IndexOutOfRangeException обрабатывается
в методе Main().

Использование блока finally

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

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

try {
    // Блок кода, предназначенный для обработки ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepType1.
}
catch (ExcepType2 ехОb) {
    // Обработчик исключения типа ЕхсерТуре2.
}
finally {
    // Код завершения обработки исключений.
}

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

Ниже приведен пример применения блока finally.

// Использовать блок finally.
using System;

class UseFinally {
    public static void GenException(int what) {
        int t;
        int[] nums = new int[2];

        Console.WriteLine("Получить " + what);
        try {
            switch(what) {
                case 0:
                    t = 10 / what; // сгенерировать ошибку из-за деления на нуль
                    break;
                case 1:
                    nums[4] = 4; // сгенерировать ошибку индексирования массива
                    break;
                case 2:
                    return; // возврат из блока try
            }
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Делить на нуль нельзя!");
            return; // возврат из блока catch
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Совпадающий элемент не найден.");
        }
        finally {
            Console.WriteLine("После выхода из блока try.");
        }
    }
}

class FinallyDemo {
    static void Main() {
        for(int i=0; i < 3; i++) {
            UseFinally.GenException(i);
            Console.WriteLine();
        }
    }
}

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

Получить 0
Делить на нуль нельзя
После выхода из блока try.

Получить 1
Совпадающий элемент не найден.
После выхода из блока try.

Получить 2
После выхода из блока try.

Как следует из приведенного выше результата, блок finally выполняется независи­
мо от причины выхода из блока try.

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

Подробное рассмотрение класса Exception

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

В классе Exception определяется ряд свойств. К числу самых интересных отно­
сятся три свойства: Message, StackTrace и TargetSite. Все эти свойства доступны
только для чтения. Свойство Message содержит символьную строку, описывающую
характер ошибки; свойство StackTrace — строку с вызовами стека, приведшими к ис­
ключительной ситуации, а свойство TargetSite получает объект, обозначающий ме­
тод, сгенерировавший исключение.

Кроме того, в классе Exception определяется ряд методов. Чаще всего приходится
пользоваться методом ToString(), возвращающим символьную строку с описанием
исключения. Этот метод автоматически вызывается, например, при отображении ис­
ключения с помощью метода WriteLine().

Применение всех трех упомянутых выше свойств и метода из класса Exception
демонстрируется в приведенном ниже примере программы.

// Использовать члены класса Exception.
using System;
class ExcTest {
    public static void GenException() {
        int[] nums = new int[4];
        Console.WriteLine("До генерирования исключения.");
        // Сгенерировать исключение в связи с выходом за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: (1)", i, nums[i]);
        }
        Console.WriteLine("He подлежит выводу");
    }
}

class UseExcept {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException exc) {
            Console.WriteLine("Стандартное сообщение таково: ");
            Console.WriteLine(exc); // вызвать метод ToString()
            Console.WriteLine("Свойство StackTrace: " + exc.StackTrace);
            Console.WriteLine("Свойство Message: " + exc.Message);
            Console.WriteLine("Свойство TargetSite: " + exc.TargetSite);
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

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

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Стандартное сообщение таково: System.IndexOutOfRangeException: Индекс находился
вне границ массива.
    в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main() в <имя_файла>:строка 29
Свойство StackTrace:в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main()в <имя_файла>:строка 29
Свойство Message: Индекс находился вне границ массива.
Свойство TargetSite: Void genException()
После блока перехвата исключения.

В классе Exception определяются четыре следующих конструктора.

public Exception()
public Exception(string сообщение)
public Exception(string сообщение, Exception внутреннее_исключение)
protected Exception(System.Runtime.Serialization.SerializationInfo информация,
    System.Runtime.Serialization.StreamingContext контекст)

Первый конструктор используется по умолчанию. Во втором конструкторе ука­
зывается строка сообщение, связанная со свойством Message, которое имеет отно­
шение к генерируемому исключению. В третьем конструкторе указывается так на­
зываемое внутреннее исключение. Этот конструктор используется в том случае, когда
одно исключение порождает другое, причем внутреннее_исключение обозначает
первое исключение, которое будет пустым, если внутреннее исключение отсутствует.
(Если внутреннее исключение присутствует, то оно может быть получено из свойства
InnerException, определяемого в классе Exception.) И последний конструктор об­
рабатывает исключения, происходящие дистанционно, и поэтому требует десериали­
зации.

Следует также заметить, что в четвертом конструкторе класса Exception типы
SerializationInfo и StreamingContext относятся к пространству имен System.
Runtime.Serialization.

Наиболее часто используемые исключения

В пространстве имен System определено несколько стандартных, встроенных ис­
ключений. Все эти исключения являются производными от класса SystemException,
поскольку они генерируются системой CLR при появлении ошибки во время выпол­
нения. В табл. 13.1 перечислены некоторые наиболее часто используемые стандартные
исключения.

Таблица 13.1. Наиболее часто используемые исключения, определенные в пространстве имен System

Исключение Значение
ArrayTypeMismatchException Тип сохраняемого значения несовместим с типом массива
DivideByZeroException Попытка деления на нуль
IndexOutOfRangeException Индекс оказался за границами массива
InvalidCastException Неверно выполнено динамическое приведение типов
OutOfMemoryException Недостаточно свободной памяти для дальнейшего выполнения программы. Это исключение может быть, например, сгенерировано, если для создания объекта с помощью оператора new не хватает памяти
OverflowException Произошло арифметическое переполнение
NullReferenceException Попытка использовать пустую ссылку, т.е. ссылку, которая не указывает ни на один из объектов

Большинство исключений, приведенных в табл. 13.1, не требует особых пояснений,
кроме исключения NullReferenceException. Это исключение генерируется при по­
пытке использовать пустую ссылку на несуществующий объект, например, при вы­
зове метода по пустой ссылке. Пустой называется такая ссылка, которая не указывает
ни на один из объектов. Для того чтобы создать такую ссылку, достаточно, например,
присвоить явным образом пустое значение переменной ссылочного типа, используя
ключевое слово null. Пустые ссылки могут также появляться и другими, менее оче­
видными путями. Ниже приведен пример программы, демонстрирующий обработку
исключения NullReferenceException.

// Продемонстрировать обработку исключения NullReferenceException.
using System;

class X {
    int x;

    public X(int a) {
        x = a;
    }

    public int Add(X o) {
        return x + o.x;
    }
}

// Продемонстрировать генерирование и обработку
// исключения NullReferenceException.
class NREDemo {
    static void Main() {
        X p = new X(10);
        X q = null; // присвоить явным образом пустое значение переменной q
        int val;
        try {
            val = p.Add(q); // эта операция приведет к исключительной ситуации
        } catch (NullReferenceException) {
            Console.WriteLine("Исключение NullReferenceException!");
            Console.WriteLine("Исправление ошибки...n");
            // А теперь исправить ошибку.
            q = new X(9);
            val = p.Add(q);
        }
        Console.WriteLine("Значение val равно {0}", val);
    }
}

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

Исключение NullReferenceException!
Исправление ошибки...

Значение val равно 19

В приведенном выше примере программы создается класс X, в котором определя­
ются член х и метод Add(), складывающий значение члена х в вызывающем объекте
со значением члена х в объекте, передаваемом этому методу в качестве параметра. Оба
объекта класса X создаются в методе Main(). Первый из них (переменная р) инициа­
лизируется, а второй (переменная q) — нет. Вместо этого переменной q присваивается
пустое значение. Затем вызывается метод р.Add() с переменной q в качестве аргумен­
та. Но поскольку переменная q не ссылается ни на один из объектов, то при попытке
получить значение члена q.х генерируется исключение NullReferenceException.

Получение производных классов исключений

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

ПРИМЕЧАНИЕ
В прошлом специальные исключения создавались как производные от класса
Application.Exception, поскольку эта иерархия классов была первоначально зарезер­
вирована для исключений прикладного характера. Но теперь корпорация Microsoft не реко­
мендует этого делать, а вместо этого получать исключения, производные от класса Exception.
Именно по этой причине данный подход и рассматривается в настоящей книге.

Создаваемые пользователем классы будут автоматически получать свойства и мето­
ды, определенные в классе Exception и доступные для них. Разумеется, любой из этих
членов класса Exception можно переопределить в создаваемых классах исключений.
Когда создается собственный класс исключений, то, как правило, желательно, что­
бы в нем поддерживались все конструкторы, определенные в классе Exception. В про­
стых специальных классах исключений этого нетрудно добиться, поскольку для этого
достаточно передать подходящие аргументы соответствующему конструктору класса
Exception, используя ключевое слово base. Но формально нужно предоставить толь­
ко те конструкторы, которые фактически используются в программе.

Рассмотрим пример программы, в которой используется исключение специального
типа. Напомним, что в конце главы 10 был разработан класс RangeArray, поддержи­
вающий одномерные массивы, в которых начальный и конечный индексы определяют­
ся пользователем. Так, например, вполне допустимым считается массив, индексируе­
мый в пределах от -5 до 27. Если же индекс выходил за границы массива, то для обра­
ботки этой ошибки в классе RangeArray была определена специальная переменная.
Такая переменная устанавливалась и проверялась после каждой операции обращения
к массиву в коде, использовавшем класс RangeArray. Безусловно, такой подход к об­
работке ошибок «неуклюж» и чреват дополнительными ошибками. В приведенном
ниже улучшенном варианте класса RangeArray обработка ошибок нарушения границ
массива выполняется более изящным и надежным способом с помощью специально
генерируемого исключения.

// Использовать специальное исключение для обработки
// ошибок при обращении к массиву класса RangeArray.
using System;

// Создать исключение для класса RangeArray.
class RangeArrayException : Exception {
/* Реализовать все конструкторы класса Exception. Такие конструкторы просто
реализуют конструктор базового класса. А поскольку класс исключения
RangeArrayException ничего не добавляет к классу Exception, то никаких
дополнительных действий не требуется. */
public RangeArrayException() : base)) { }

public RangeArrayException(string str) : base(str) { }

public RangeArrayException(
string str, Exception inner) : base (str, inner) { }

protected RangeArrayException(
System.Runtime.Serialization.SerializationInfo si,
System.Runtime.Serialization.StreamingContext sc) :
base(si, sc) { }

// Переопределить метод ToString() для класса исключения RangeArrayException.
public override string ToString() {
    return Message;
}

}

// Улучшенный вариант класса RangeArray.
class RangeArray {
// Закрытые данные.
int[] a; // ссылка на базовый массив
int lowerBound; // наименьший индекс
int upperBound; // наибольший индекс

// Автоматически реализуемое и доступное только для чтения свойство Length.
public int Length { get; private set; }
// Построить массив по заданному размеру

public RangeArray(int low, int high) {
    high++;
    if(high <= low) {
        throw new RangeArrayException("Нижний индекс не меньше верхнего.");
    }
    а = new int[high - low];
    Length = high - low;
    lowerBound = low;
    upperBound = --high;
}

// Это индексатор для класса RangeArray.
public int this[int index] {
    // Это аксессор get.
    get {
        if(ok(index)) {
            return a[index - lowerBound];
        } else {
            throw new RangeArrayException("Ошибка нарушения границ.");
        }
    }
    // Это аксессор set.
    set {
        if(ok(index)) {
            a[index - lowerBound] = value;
        }
        else throw new RangeArrayException("Ошибка нарушения границ.");
    }
}

// Возвратить логическое значение true, если
// индекс находится в установленных границах.
private bool ok(int index) {
    if(index >= lowerBound S index <= upperBound) return true;
    return false;
}

}

// Продемонстрировать применение массива с произвольно
// задаваемыми пределами индексирования.
class RangeArrayDemo {
static void Main() {
try {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);

        // Использовать объект ra в качестве массива.
        Console.WriteLine("Длина массива ra: " + ra.Length);
        for(int i = -5; i <= 5; i++)
            ra[i] = i;

        Console.Write("Содержимое массива ra: ");
        for(int i = -5; i <= 5; i++)
            Console.Write(ra[i] + " ");

        Console.WriteLine("n");

        // Использовать объект ra2 в качестве массива.
        Console.WriteLine("Длина массива ra2: " + ra2.Length);
        for(int i = 1; i <= 10; i++)
            ra2[i] = i;

        Console.Write("Длина массива ra2: ");
        for(int i = 1; i <= 10; i++)
            Console.Write(ra2[i] + " ");

        Console.WriteLine("n");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // А теперь продемонстрировать обработку некоторых ошибок.
    Console.WriteLine("Сгенерировать ошибки нарушения границ.");

    // Использовать неверно заданный конструктор.
    try {
        RangeArray ra3 = new RangeArray(100, -10); // Ошибка!
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // Использовать неверно заданный индекс.
    try {
        RangeArray ra3 = new RangeArray(-2, 2);

        for(int i = -2; i <= 2; i++)
            ra3[i] = i;

        Console.Write("Содержимое массива ra3: ");
        for(int i = -2; i <= 10; i++) // сгенерировать ошибку нарушения границ
            Console.Write(ra3[i] + " ");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }
}

}

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

Длина массива ra: 11
Содержимое массива ra: -5 -4 -3 -2 -1 0 1 2 3 4 5

Длина массива ra2: 10
Содержимое массива ra2: 1 2 3 4 5 6 7 8 9 10

Сгенерировать ошибки нарушения границ.
Нижний индекс не меньше верхнего.
Содержимое массива ra3: -2 -1 0 1 2 Ошибка нарушения границ.

Когда возникает ошибка нарушения границ массива класса RangeArray, генери­
руется объект типа RangeArrayException. В классе RangeArray это может произой­
ти в трех следующих местах: в аксессоре get индексатора, в аксессоре set индексатора
и в конструкторе класса RangeArray. Для перехвата этих исключений подразумева­
ется, что объекты типа RangeArray должны быть сконструированы и доступны из
блока try, что и продемонстрировано в приведенной выше программе. Используя
специальное исключение для сообщения об ошибках, класс RangeArray теперь дей­
ствует как один из встроенных в C# типов данных, и поэтому он может быть полностью
интегрирован в механизм обработки ошибок, обнаруживаемых в программе.

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

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

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

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

В приведенном ниже примере программы создаются два класса исключений:
ExceptA и ExceptB. Класс ExceptA является производным от класса Exception,
а класс ExceptB — производным от класса ExceptA. Затем в программе генерируются
исключения каждого типа. Ради краткости в классах специальных исключений предо­
ставляется только один конструктор, принимающий символьную строку, описываю­
щую исключение. Но при разработке программ коммерческого назначения в классах
специальных исключений обычно требуется предоставлять все четыре конструктора,
определяемых в классе Exception.

// Исключения производных классов должны появляться до
// исключений базового класса.
using System;

// Создать класс исключения.
class ExceptA : Exception {
public ExceptA(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

// Создать класс исключения, производный от класса ExceptA.
class ExceptB : ExceptA {
public ExceptB(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

class OrderMatters {
static void Main() {
for(int x = 0; x < 3; x++) {
try {
if(x==0) throw new ExceptA(«Перехват исключения типа ExceptA»);
else if(x==1) throw new ExceptB(«Перехват исключения типа
ExceptB»);
else throw new Exception));
}
catch (ExceptB exc) {
Console.WriteLine(exc);
}
catch (ExceptA exc) {
Console.WriteLine(exc);
}
catch (Exception exc) {
Console.WriteLine(exc);
}
}
}
}

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

Перехват исключения типа ExceptA.
Перехват исключения типа ExceptB.
System.Exception: Выдано исключение типа «System.Exception».
в OrderMatters.Main() в <имя_файла>:строка 36

Обратите внимание на порядок следования операторов catch. Именно в таком по­
рядке они и должны выполняться. Класс ExceptB является производным от класса
ExceptA, поэтому исключение типа ExceptB должно перехватываться до исключения
типа ExceptA. Аналогично, исключение типа Exception (т.е. базового класса для всех
исключений) должно перехватываться последним. Для того чтобы убедиться в этом,
измените порядок следования операторов catch. В итоге это приведет к ошибке во
время компиляции.

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

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

byte a, b, result;
а = 127;
b = 127;
result = (byte)(а * b);

В этом коде произведение значений переменных а и b превышает диапазон пред­
ставления чисел для типа byte. Следовательно, результат вычисления данного вы­
ражения приводит к переполнению для типа данных, сохраняемого в переменной
result.

В C# допускается указывать, будет ли в коде сгенерировано исключение при пере­
полнении, с помощью ключевых слов checked и unchecked. Так, если требуется ука­
зать, что выражение будет проверяться на переполнение, следует использовать клю­
чевое слово checked, а если требуется проигнорировать переполнение — ключевое
слово unchecked. В последнем случае результат усекается, чтобы не выйти за пределы
диапазона представления чисел для целевого типа выражения.

У ключевого слова checked имеются две общие формы. В одной форме проверя­
ется конкретное выражение, и поэтому она называется операторной. А в другой форме
проверяется блок операторов, и поэтому она называется блочной. Ниже приведены обе
формы:

checked (выражение)

checked {
// проверяемые операторы
}

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

У ключевого слова unchecked также имеются две общие формы. В первой, опера­
торной форме переполнение игнорируется при вычислении конкретного выражения.
А во второй, блочной форме оно игнорируется при выполнении блока операторов:

unchecked (выражение)

unchecked {
// операторы, для которых переполнение игнорируется
}

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

Ниже приведен пример программы, в котором демонстрируется применение клю­
чевых слов checked и unchecked.

// Продемонстрировать применение ключевых слов checked и unchecked.
using System;

class CheckedDemo {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);
result = checked((byte)(a * b)); // эта операция приводит к
// исключительной ситуации
Console.WriteLine(«Проверенный на переполнение результат: » +
result); //не подлежит выполнению
}
catch (OverflowException exc) {
Console.WriteLine(exc);
}
}
}

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

Непроверенный на переполнение результат: 1
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файла>:строка 20

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

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

// Продемонстрировать применение ключевых слов checked
// и unchecked в блоке операторов.
using System;

class CheckedBlocks {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
unchecked {
a = 127;
b = 127;
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);

            а = 125;
            b = 5;
            result = unchecked((byte)(a * b));
            Console.WriteLine("Непроверенный на переполнение результат: " +
                            result);
        }

        checked {
            a = 2;
            b = 7;
            result = checked((byte)(a * b)); // верно
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result);

            а = 127;
            b = 127;
            result = checked((byte)(a * b)); // эта операция приводит к
            // исключительной ситуации
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result); // не подлежит выполнению
        }
    }
    catch (OverflowException exc) {
        Console.WriteLine(exc);
    }
}

}

Результат выполнения этой программы приведен ниже.

Непроверенный на переполнение результат: 1
Непроверенный на переполнение результат: 113
Проверенный на переполнение результат: 14
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файма>:строка 41

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

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

Содержание

  • Пример исключения в C#
  • Блок try…catch…finally
    • Перехват и обработка исключений в блоке catch
  • Логические операции и обработка исключений в C#
  • Итого

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

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

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

Console.WriteLine("Введите любое целое число и нажмите Enter");
int i = int.Parse(Console.ReadLine());
double x = 5;
double y = x / i;
Console.WriteLine($"{x}/{i}={y}");

Теперь запустим программу и введем  число 0. В итоге, в Visual Studio мы увидим ошибку:

Мы получили исключение типа System.DivideByZeroException (деление на ноль) и наше приложение аварийно завершило свою работу. Кроме этого, в таком простом, казалось бы, приложении имеется ещё одна уязвимость — пользователь может ввести совсем не то, что от него требуется и вместо числа введет, например, строку. В этом случае мы, опять же, получим в Visual Studio исключение:

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

Блок try…catch…finally

Для обработки исключений в C# используется специальная конструкция — блок try...catch...finally. Перепишем наше приложение следующим образом:

Console.WriteLine("Введите любое целое число и нажмите Enter");
try
{
    int i = int.Parse(Console.ReadLine());
    int x = 5;
    double y = x / i;
    Console.WriteLine($"{x}/{i}={y}");
}
catch
{
    Console.WriteLine("Неправильный ввод значения");
}
finally
{
    Console.WriteLine("Выполнили блок finally");
}
_ = Console.ReadLine();

Теперь запустим программу и снова введем значение 0. В результате, программа не завершит работу аварийно, а выведет в консоль сообщение. Вот вывод консоли:

Введите любое целое число и нажмите Enter

0

Неправильный ввод значения

Выполнили блок finally

Приложение так же, как и в предыдущем примере, дошло до строки

double y = x / i;

однако, вместо аварийной остановки на строке с ошибкой, программа перешла в блок catch и вывела сообщение «Неправильный ввод значения». После того, как выполнен блок catch, программа переходит в блок finally, выполняет все операции в нем и завершает работу.

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

//БЕЗ БЛОКА FINALLY. Программа не завершается аварийно
try
{
    int i = int.Parse(Console.ReadLine());
    int x = 5;
    double y = x / i;
    Console.WriteLine($"{x}/{i}={y}");
}
catch
{
    Console.WriteLine("Неправильный ввод значения");
}

или

//БЕЗ БЛОКА CATCH. Программа аварийно завершит работу
try
{
    int i = int.Parse(Console.ReadLine());
    int x = 5;
    double y = x / i;
    Console.WriteLine($"{x}/{i}={y}");
}
finally
{
    Console.WriteLine("Выполнили блок finally");
}

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

Перехват и обработка исключений в блоке catch

В примере с блоком catch выше всё, что мы сделали — это вывели одно сообщение о том, что пользователь ввел неверное значение. При этом, при разработке реальных приложений часто необходимо не только сообщить пользователю об исключении, но и постараться направить его на «путь истинный». Например, в нашем тестовом приложении пользователь, как мы определили может:

  1. ввести 0 (исключение System.DivideByZeroException)
  2. ввести вместо целого числа строку (исключение System.FormatException)
  3. ввести вместо целого числа число с плавающей запятой (исключениеSystem.FormatException)
  4. ввести число, превышающее максимальное значение int (исключение System.OverflowException)

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

try
{
    i = int.Parse(Console.ReadLine());
    
    double y = x / i;
    Console.WriteLine($"{x}/{i}={y}");
}
catch (System.DivideByZeroException e)
{
    Console.WriteLine($"Деление на ноль! Исключение {e}");
}
catch (System.FormatException e)
{
    Console.WriteLine($"Введено не целое число! Исключение {e}");
}
catch (System.OverflowException e)
{
    Console.WriteLine($"Введите число в диапазоне от {int.MinValue} до {int.MaxValue}, исключая ноль. Исключение {e}");
}

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

Следует также отметить, что далеко не всегда удается на этапе разработки предугадать абсолютна все типы исключений. Что, например, произойдет, если мы уберем из нашего кода блок, обрабатывающий System.OverflowException? Правильно, мы снова нарвемся на аварийное завершение работы программы, так как компилятор пройдет по всем блокам catch и не сможет соотнести тип исключение с именем.  Чтобы такого не произошло, можно также предусмотреть при обработке исключений общий блок catch в котором будет обрабатываться всё, что не попало в другие блоки. Например, мы можем сделать обработку двух типов исключений, а третий — обработаем в общем блоке:

catch (System.OverflowException e)
{
    Console.WriteLine($"Введите число в диапазоне от {int.MinValue} до {int.MaxValue}, исключая ноль. Исключение {e}");
}


catch (System.DivideByZeroException e)
{
    Console.WriteLine($"Деление на ноль! Исключение {e}");
}
//общий блок catch
catch
{
    Console.WriteLine("Неизвестная ошибка. Перезапустите программу");
}

Необходимо отметить, что важен не только факт наличия, но и порядок написания блоков catch. Универсальный блок catch должен находиться в самом низу кода. Об этом, кстати, Visual Studio сообщает. Если вы перенесете общий блок catch и поставите его, например, над блоком, обрабатывающим исключение DivideByZeroException, то Visual Studio выдаст ошибку:

Ошибка CS1017 Конструкции catch не могут использоваться после универсальной конструкции catch оператора try

Логические операции и обработка исключений в C#

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

if (int.TryParse(Console.ReadLine(), out i))
{
    y = x / i;
    Console.WriteLine($"{x}/{i}={y}");
}    
else
{
    Console.WriteLine("Вы ввели не число!");
}

Здесь метод int.TryParse() пробует преобразовать строку в целое число и, если преобразование прошло успешно, то возвращает true. Таким образом, мы избежали использования конструкции try...catch, которая, кстати, с точки зрения производительности более накладна, чем обычный условный оператор if.

Итого

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

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Понравилась статья? Поделить с друзьями:
  • Обработка ошибки pdo в php
  • Обработка ошибок в сети frame relay
  • Обработка ошибки 404 в php
  • Обработка ошибок в русском языке
  • Обработка запроса провалилась ошибка на этапе первичной обработки документа