Обработка ошибок с помощью исключений - Mini-Soft

Обработка ошибок с помощью исключений Одно из основных назначений .NET Common Language Runtime (CLR) — недопущение ошибок (что достигается такими средствами, как автоматическое управление памятью и ресурсами в управляемом коде) или хотя бы их обнаружение во время компиляции (благодаря строго типизированной системе). Однако некоторые ошибки можно обнаружить только в период выполнения, а значит, для всех языков, соответствующих спецификации Common Language Specification (CLS), должен быть предусмотрен единый метод реакции на ошибки. Эта глава посвящена системе обработки ошибок, реализованной в CLR, — обработке исключений. Сначала мы изучим общий механизм и основы синтаксиса обработки исключений. Вы увидите, как обработка исключений соотносится с наиболее распространенными на сегодняшний день методами обработки ошибок, и поймете преимущества обработки исключений над другими методиками. Затем мы углубимся в наиболее специфические вопросы обработки исключений в .NET, такие как применение класса Exception и производных от него собственных классов исключений. Последний раздел посвящен созданию приложений с обработкой исключений.   Исключения — это условия, при которых нормальный ход программы, т. е. последовательность вызовов методов в стеке вызовов, невозможен или нежелателен. Надо понимать разницу между исключением и ожидаемым событием (скажем, обнаружение конца файла). Если у вас есть метод, последовательно считывающий файл, вы понимаете, что в какой-то момент будет достигнут его конец. Такое событие по природе своей вряд ли является исключительным и, конечно, не должно прерывать выполнение программы. Если же вы пытаетесь прочитать файл, а ОС уведомляет вас о дисковой ошибке, это, разумеется, исключительная ситуация, и именно такая, что повлияет на нормальный ход работы вашего метода при его попытке продолжить считывание файла. Большинство исключений связано и с другой проблемой — контекстом. Рассмотрим пример. Допустим, вы пишете код с сильными связями — код, в котором один метод отвечает за одно свое действие. Он может выглядеть примерно так: while (Ifile.IsEOFO) String record = file.ReadRecordO; // Пытаемся заблокировать и открыть файл. } Если возникнет сбой в методе OpenFile, этот метод не сможет обработать ошибку. Дело в том, что он отвечает только за открытие файлов. Он не может определить, с чем связана проблема при открытии файла: с катастрофической ошибкой или просто недоразумением. Следовательно, OpenFile не может обработать ошибку, поскольку он, что называется, не находится в верном контексте. Собственно поэтому и существуют обработчики исключений: один метод обнаруживает наличие исключительной ситуации, но оказывается не в том контексте, чтобы реагировать на ошибку. Он сигнализирует исполняющей среде, что возникла ошибка. Исполняющая среда последовательно проходит по стеку вызовов, пока не находит метод, способный правильно работать с ошибкой. Понятно, что это приобретает особую остроту, когда метод, находящийся на пятом уровне вложенности, сталкивается с ошибкой, а обработать ее корректно может только метод первого уровня. Давайте рассмотрим синтаксис, применяемый при обработке исключений.   При обработке исключений используются всего четыре ключевых слова: try, catch, throw к finally. Способ применения ключевых слов прост и понятен. Когда метод не может выполнить свою задачу, т. е. когда он определяет исключительную ситуацию, то передает исключение вызывающему методу через ключевое слово throw. Вызывающий метод (если предположить, что он обладает достаточным контекстом для работы с исключением) получает это исключение посредством ключевого слова catch и решает, что предпринять. В следующих разделах мы рассмотрим семантику языка, регулирующую передачу и обнаружение исключений, а также несколько примеров.   Чтобы методу уведомить вызвавший его метод, что возникла ошибка, он использует ключевое слово throw. throw [выражение]; Мы рассмотрим разные способы передачи исключений чуть позже. Сейчас достаточно представлять себе, что при передаче исключения вам нужно передать объект типа System.Exception (или производный класс). Далее приведен пример метода, определившего, что произошла неисправимая ошибка и ему нужно передать исключение вызывающему методу. Обратите внимание, как создается новый экземпляр объекта System. Exceptions и передается вызывающему методу. // Обнаружена ошибка.   Если метод может передавать исключения, должна быть обратная сторона, которая находит это исключение. Ключевое слово catch определяет блок кода, который выполнится при возникновении исключения данного типа. Содержащийся в этом блоке код называется обработчиком исключения. Нужно понимать, что не каждый метод должен иметь дело с каждым переданным исключением прежде всего потому, что метод может не иметь контекста для адекватной обработки информации об ошибке. Как-никак суть обработки исключений в том, что ошибка должна обрабатываться кодом, имеющим достаточный контекст, чтобы корректно это проделать (см. ниже раздел "Обработка ошибок в правильном контексте"). Пока что будем рассматривать ситуации в которых метод пытается улавливать любые исключения, посланные ему вызванным методом. Для обнаружения исключений применяются ключевые слова try и catch. Для обнаружения исключения нужно ограничить код, который вы собираетесь выполнить, блоком try, а затем указать, какие типы исключений этот код может обрабатывать в блоке catch. Все операторы из блока try будут обрабатываться по порядку, если только один из вызванных методов не передаст исключение. Если это произойдет, управление будет передано на первую строку соответствующего блока catch. Под "соответствующим блоком" я подразумеваю блок, определенный для улавливания исключений данного типа. Вот пример метода (Foo), вызывающего и обнаруживающего исключения, посылаемые другим методом (Ваг): catch(System.Exception e) // Обработка ошибки. У вас может возникнуть вопрос: "Что будет, если Ваг передаст исключение, a Foo его не уловит?" (Это может иметь место, если обращение к Ваг не содержится в блоке try.) Результат зависит от структуры приложения. При передаче исключения управление передается по стеку вызовов наверх, пока не будет найден блок catch для исключения данного типа. Если метод с подходящим блоком catch не обнаруживается, приложение прерывается. Следовательно, если один метод вызывает другой — передающий исключение — структура приложения должна быть такова, чтобы метод в стеке вызовов смог обработать исключение.   Случается, что метод, уловивший исключение и сделавший все, что он может в своем контексте, затем повторно передает (rethrow) исключение выше. Это легко реализовать с помощью ключевого слова throw: rethrow.Foo(); } Console.WriteLine(e.Message); } } // Обработка ошибки, throw; > } throw new Exception("Передано от Rethrow.Bar"); } В этом примере Main вызывает Foo, который вызывает Bar. Bar передает исключение, которое улавливает Foo. После этого Foo неким образом обрабатывает исключение и повторно передает его наверх методу Main с помощью ключевого слова throw.   Освобождение ресурсов с помощью finally Один трудный вопрос с обработкой ошибок связан с тем, что код всегда выполняется независимо от того, уловлено ли исключение. Например, вы выделили ресурс, такой как физическое устройство или файл данных. Теперь предположим, что вы открыли этот ресурс и вызвали метод, передавший исключение. Независимо от того, может ли ваш метод продолжить работу с ресурсом, вам в любом случае нужно освободить или закрыть ресурс. Здесь-то и применяется ключевое слово finally: Console.WriteLine("try..."); > Console.WriteLine("catch..."); } Console.WriteLine("finally"); } } > Как видите, finally позволяет избежать двойного кодирования освобождения ресурса: в блоке catch и после блоков try/catch. Независимо от того, передано ли исключение, будет выполнен код в блоке finally.   Ознакомившись с основами передачи и улавливания исключений, давайте уделим пару минут сравнению различных подходов к обработке ошибок в разных языках программирования. Стандартным подходом к обработке ошибок всегда была передача кода ошибки вызывающему методу, который должен был декодировать возвращаемое значение и действовать соответствующим образом. Воз-вр^щаемое значение может быть простым, как, скажем, базовый тип С или C++, или это может быть указатель на более сложный объект, содержащий всю информацию, необходимую для оценки и понимания ошибки. Более развитые методы обработки ошибок включают соответствующую законченную подсистему. В этом случае вызванный метод указывает ошибку подсистеме, а затем возвращает код ошибки вызывающему методу. Вызывающий метод затем вызывает глобальную функцию, экспортируемую из подсистемы обработки ошибок, чтобы определить причину последней зарегистрированной подсистемой ошибки. Пример такого подхода — Microsoft Open Database Connectivity (ODBC) SDK. Однако независимо от конкретной семантики концепция остается той же: активизирующий метод так или иначе вызывает другой метод и анализирует возвращаемое значение, чтобы узнать, успешно ли завершился вызванный метод. Этот подход, хотя и был стандартом долгие годы, во многом очень устарел. В следующих разделах описываются случаи, в которых обработка исключений имеет громадные преимущества над применением кодов возврата.   При использовании кодов возврата вызываемый метод возвращает код ошибки, и причина ошибки обрабатывается вызывающим методом. Поскольку обработка происходит вне области видимости вызываемого метода, нет гарантии, что вызывающий метод проверит возвращенный код ошибки. Скажем, вы пишете класс CommaDelimitedFile, реализующий чтение и запись стандартных файлов с разделителями. Ваш класс, в частности, должен иметь методы для открытия и чтения данных из файла. При старом подходе уведомления об ошибках эти методы должны возвращать переменные, которые должны проверяться вызывающим методом, чтобы определить, успешно ли выполнился вызов. Если пользователь вашего класса вызвал метод CommaDelimitedFile.Open, а затем пытается вызвать метод CommaDelimitedFile.Read, не проверив, успешно ли завершился Open, это может привести к очень неприглядным результатам (и скорей всего это случится, когда вы демонстрируете свою программу очень важному заказчику). Но если метод Open этого класса передаст исключение, вызывающий метод будет поставлен перед фактом, что в методе Open произошел сбой. Дело в том, что при каждой передаче исключения управление передается наверх, пока исключение не улавливается. Такой код может выглядеть следующим образом: this.fileName = fileName; // и передаем исключение при возникновении ошибки, throw new Exception("Открыть файл не удалось"); } // Код для чтения файла, return false; // EOF } } Console.WriteLlne("Пытаемся открыть файл"); CommaDelimltedFlle file = new CommaDelimltedFileO; file.Open("c:\\test.csv"); Console.WrlteLine("Читаем файл"); while (file.Read(record) == true) { Console.Writeline(record); } Console.WriteLine("Чтение файла закончено"); } Console.WrlteLine(e.Message); } } } Здесь, если метод CommaDelimitedFile.Open или CommaDelimited-File.Read передаст исключение, вызывающий метод вынужден будет на него прореагировать. Если ни вызывающий, ни другие методы в текущем кодовом сегменте не уловят исключение данного типа, приложение прервется. Заметьте: обращение к методу Open находится в блоке и, следовательно, попытка недопустимого чтения (если Open передаст исключение) производиться не будет. Это объясняется тем, что управление будет передано из блока try, в котором производится обращение

Hosted by uCoz