Содержание
Исключения, их обработка, и некоторые другие моменты, связанные с ошибками в приложении на C#.
Исключения (Exceptions) и инструкция try
Инструкция try отмечает блок кода как объект для обработки ошибок или очистки. После блока try обязательно должен идти либо блок catch, либо блок finally, либо они оба. Блок catch выполняется, когда внутри блока try возникает ошибка. Блок finally выполняется после того, как прекращает выполнять блок try (или, если присутствует, блок catch), независимо от того, выполнился ли он до конца или был прерван ошибкой, что позволяет выполнить так называемый код очистки.
Блок catch имеет доступ к объекту исключения (Exception), который содержит информацию об ошибке. Блок catch позволяет обработать исключительную ситуацию и как-либо скорректировать ошибку или выбросить новое исключение. Повторное выбрасывание исключения в блоке catch обычно применяется с целью логирования ошибок или чтобы выбросить новое, более специфическое исключение.
Блок finally добавляет в программу прогнозируемость, позволяя выполнить определенный код при любых обстоятельствах. Это может быть полезно для выполнения операций очистки, например, закрытия сетевого подключения и т.д.
В целом конструкция try выглядит следующим образом:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | try {     ... // в пределах этого блока может быть выброшено исключение } catch (ExceptionA ex) {     ... // обработчик исключений типа ExceptionA } catch (ExceptionB ex) {     ... // обработчик исключений типа ExceptionB } finally {     ... // код очистки } | 
 Например, следующий код выбросит ошибку DivideByZeroException (поскольку делить на ноль нельзя) и наша программа завершить досрочно:
| 1 2 | int x = 3, y = 0; Console.WriteLine (x / y); | 
 Чтобы этого избежать можно использовать конструкцию try:
| 1 2 3 4 5 6 7 8 9 10 | 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:
| 1 2 3 4 5 6 7 | try {     DoSomething(); } catch (IndexOutOfRangeException ex) { ... } catch (FormatException ex) { ... } catch (OverflowException ex) { ... } | 
Каждая оговорка способна обработать только то исключение, которое точно совпадает с ее типом. Для одного выброшенного исключения может быть выполнена только одна оговорка catch. Обрабатываются блоки catch в том порядке, в котором они идут в коде. В этой связи более специфические исключения должны перехватываться раньше чем более общие.
Исключение может быть перехвачено и без указания переменной, если не нужен доступ к ее членам:
| 1 2 | catch (StackOverflowException) // без переменной { ... } | 
Более того, в оговорке catch можно опустить и переменную и тип исключения — такая оговрка будет перехватывать все исключения:
| 1 | catch { ... } | 
Блок finally
Блок finally выполняется всегда, независимо от того выброшено исключение или нет. Блок finally обычно содержит код очистки.
Блок finally выполняется в следующих случаях:
- после завершения блока catch
- если выполнение блока tryпрервано jump-инструкциями:return,gotoи т.д.
- после выполнения блока tryполностью, если исключений так и не было выброшено
Блок finally делает программу более прогнозируемой. Например, в следующем примере открываемый файл в итоге всегда будет закрыт, независимо от того, завершиться ли блок try без ошибок, или будет прерван выброшенным исключением, или сработает инструкция return если файл окажется пустым:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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:
| 1 2 3 4 | using (StreamReader reader = File.OpenText ("file.txt")) {     ... } | 
Что эквивалентно следующей конструкции:
| 1 2 3 4 5 6 7 8 9 | StreamReader reader = File.OpenText ("file.txt"); try {     ... } finally {     if (reader != null) ((IDisposable)reader).Dispose(); } | 
Выбрасывание исключений
Исключение может быть выброшено автоматически во время выполнения программы либо явно в коде программы с помощью ключевого слова throw:
| 1 2 3 4 5 6 | static void Display (string name) {     if (name == null)     throw new ArgumentNullException ("name");     Console.WriteLine (name); } | 
 Также исключение может быть выброшено повторно внутри блока catch:
| 1 2 3 4 5 6 7 | try { ... } catch (Exception ex) {     // логирование ошибки     ...     throw; // повторное выбрасывание того же самого исключения } | 
Такой подход позволяет заносить ошибки в лог без их дальнейшего поглощения. Также это позволяет уклониться от обработки неожиданных исключений.
Если throw заменить на throw ex, то пример по прежнему будет работать, но свойство исключения StackTrace не будет отражать исходную ошибку.
Другой распространенный сценарий использования повторного выбрасывания исключения — повторное выбрасывание более специфического и конкретного типа исключения, чем было перехвачено ранее:
| 1 2 3 4 5 6 7 8 | 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).
Директивы препроцессора
Директивы препроцессора снабжают компилятор дополнительной информацией об областях кода. Самые распространенные директивы препроцессора — условные директивы, позволяющие включить или исключить области кода из компиляции.
| 1 2 3 4 5 6 7 8 9 10 11 | #define DEBUG class MyClass {     int x;     void Foo()     {         # if DEBUG         Console.WriteLine ("Testing: x = {0}", x);         # endif     } } | 
 В этом классе инструкции в методе Foo скомпилируются если определен символ DEBUG, а если его удалить — инструкции не скомпилируются. Символы препроцессора могут быть определены в исходном коде (как в примере), а могут быть переданы компилятору в командной строке с помощью параметра /define:symbol.
С директивами #if и #elif можно использовать операторы ||, && и ! с несколькими символами:
| 1 | #if TESTMODE && !DEBUG | 
 Директивы #error и #warning предотвращают некорректное использование условных директив, заставляя компилятор генерировать предупреждение или ошибку при передаче неверного набора символов.
Директивы препроцессора схожи с условными конструкциями и статическими переменными, однако дают возможности, недоступные для последних:
- условное включение атрибута
- изменение типа, объявляемого для переменной
- переключение между разными пространствами имен или псевдонимами типа в директиве using:123456using TestType =#if V2MyCompany.Widgets.GadgetV2;#elseMyCompany.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.
| 1 2 3 4 5 6 7 | 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 указывает компилятору на необходимость игнорировать все обращения к определенному классу или методу, если заданный символ не был определен:
| 1 2 3 4 5 | [Conditional ("LOGGINGMODE")] static void LogStatus (string msg) {     ... } | 
Это равносильно тому, что каждый вызов метода будет окружен условными директивами:
| 1 2 3 | #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. По умолчанию они отправляют сообщения в окно вывода отладчика:
| 1 2 3 4 | 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.
| 1 2 3 4 5 6 7 8 9 10 11 | // Удалить стандартный прослушиватель, очистив коллекцию прослушивателей: 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 для записи дополнительных данных:
| 1 2 3 4 5 6 7 8 9 | 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=2007-03-08T05: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:
| 1 | Debug.Fail ("File data.txt does not exist!"); | 
 Метод Assert вызывает Fail если аргумент типа bool равен false. Это называется созданием утверждения и указывает на ошибку, если оно нарушено. Можно также создать необязательное сообщение об ошибке:
| 1 2 3 | Debug.Assert (File.Exists ("data.txt"), "File data.txt does not exist!"); var result = ... Debug.Assert (result != null); | 
 Методы Write, Fail и Assert также могут принимать категорию в виде строки ,которая может быть использована при обработке вывода.

