Анализ исключений
Следующим шагом должен быть перехват и последующий анализ исключения. Для этого команда Catch приводится к следующему виду:
Catch excep As Exception
(имя может быть любым, поскольку упоминание в заголовке секции Catch считается объявлением переменной). Теперь объект исключения, на который ссылается ехсер, автоматически заполняется данными. Например, в следующей секции Catch используется встроенный метод ToString объекта исключения ехсер:
Catch ехсер As Exception
Console.WriteLine(excep)
Результат выглядит примерно так:
System.IndexOutOfRangeException:
An exception of type_ System.IndexOutOfRangeException
was thrown, at Exception_l.Exception!.Main() in
C:\Documents and_ Settings\x20\My DocumentsWisual Studio
Projects\ConsoleApplication!4\Exception.vb:1ine 6
Из описания видно, что ошибка произошла в строке 6 при обращении к элементу массива. Конечно, если вы не хотите пугать пользователя, выводить эту информацию в окончательной версии программы не рекомендуется, но в процессе отладки она очень полезна.
При знакомстве с этим примером возникает логичный вопрос. Допустим, пользователь ввел имя файла, но метод ProcessFile не может его обработать. Что тогда? Можно ли отличить одно исключение от другого? Как вы вскоре увидите, небольшое усложнение секции Catch позволяет различать исключения по категориям. Более того, в секции Catch можно даже заново инициировать перехваченное исключение командой Throw, чтобы продолжить его обработку.
Две основные ветви иерархии исключений
Обработка исключений в сочетании с определением собственных классов исключений позволяет полностью отказаться от использования GoTo. Например, в главе 3 был приведен пример оправданного применения GoTo для прерывания вложенных циклов, когда ошибка происходит во внутреннем цикле. Программист VB .NET в подобной ситуации просто заключает весь цикл в блок Try-Catch, как показано ниже:
Sub Main()
Dim getData As String
Dim i, j As Integer
Dim e As System.I0.I0Exception
Try
For i = 1 To 10
For j = 1 To 100 Console.WriteC'Type the data, hit the Enter key between " & _
"ZZZ to end: ") getData _
Console.ReadLine() If getData = "ZZZ" Then
e New System.I0.I0Exception("Data entry ended " & _
"at user request") Throw e Else
' Обработка данных
End If
Next j
Next i
Catch
Console.WriteLinete.Message)
Console. Readline()
End Try
End Sub
В приведенном выше фрагменте выделенные строки нельзя объединить конструкцией следующего вида:
Dim e As New System.IO.IOException("Data entry ended at user request")
Вследствие правил видимости VB .NET объект исключения окажется недоступным в секции Catch.
Иерархия исключений
Мы создали новый класс исключений, производный от IOExcepti on, потому что потенциальная проблема явно относилась к категории ввода-вывода. Допустим, ситуация имеет более общий характер и для базового класса не существует других очевидных кандидатов, кроме класса Exception. Впрочем, это не совсем верно — лучший выбор существует всегда. Мы настоятельно рекомендуем выбирать в качестве базового не сам класс Exceptlon, а производный от него класс AppllcationException.
Дело в том, что .NET Framework различает исключения, возникшие в результате проблем исполнительной среды (например, нехватки памяти или дискового пространства) и проблем, обусловленных работой вашего приложения. Именно исключения второй категории должны быть производными от AppllcationExcepti on, поэтому именно этот класс следует выбирать базовым при определении обобщенных исключений в программе.
Учтите, что класс IOException, как и многие стандартные исключения, является произ-водным от Exception, а не от ApplicationException.
Исполнительная среда помогает сделать следующий шаг. Иерархия исключений расходится на две ветви, показанные на Рисунок 7.1.
Инициирование исключений
Выше уже говорилось о том, что метод ProcessFilе просто передает исключение в процедуру Sub Main, из которой он был вызван. В процедуре Sub Mai n команда вызова тоже заключена в блок Try-Catch, поэтому исключение будет обработано. С другой стороны, такое решение выглядит немного наивно, а если написанные вами классы будут использоваться другими программистами, оно становится попросту опасным. Но даже если дело как-нибудь обойдется, пользователи вашего кода вряд ли будут довольны тем, что вы без разбора передаете исключения, не пытаясь их обработать.
Лучше попытаться по возможности «прибрать» за собой, а затем воспользоваться ключевым словом Throw, чтобы передать объект исключения вызывающей стороне. В главе 4 упоминалось о том, что в VB .NET не поддерживается детерминированное завершение. Следовательно, если вы создали объект с методом D1 spose, этот метод следует вызвать перед тем, как инициировать исключение. Сказанное относится и к открытию файлов, и к получению графического контекста. В следующем фрагменте представлена условная структура подобного кода:
Try
' Создание локального объекта с методом Dispose
' Код. который может инициировать исключения
Catch(e As Exception)
local Object.dispose()
Throw e;
End Try
Если вы не вызовете метод Dispose для своего локального объекта, то захваченные ресурсы так и не будут освобождены. Ведь ссылка на объект существует лишь в локальном коде; остальные части программы не обладают доступом к методу Dispose! С другой стороны, причина, по которой возникло исключение, остается в силе, поэтому о возникшей проблеме (например, о неудачной операции с файлом) нужно сообщить вызывающему коду. Для этого следует заново инициировать исключение командой Throw, как это сделано во второй выделенной строке.
Впрочем, если вы действительно хотите программировать «как положено», не ограничивайтесь простым перезапуском исключения. Постарайтесь сделать свой код как можно более информативным и включите в объект исключения дополнительную информацию. Для этого есть три возможности.
-
Добавьте в исключение содержательное сообщение и инициируйте его заново. Возможно, новая информация окажется полезной.
Инициируйте исключение одного из стандартных типов, производных от типа текущего исключения, чтобы оно лучше описывало ситуацию.
Создайте новый класс исключения, производный от типа текущего исключения, который будет описывать ситуацию лучше, чем любой из стандартных классов.
Решения расположены по возрастанию приоритета, и в идеальном случае следует всегда использовать пункт 3. На практике программисты при выборе руководствуются своей оценкой того, какую информацию об исключении необходимо передать для дальнейшей обработки.
Для примера представьте такую ситуацию: из источника данных читаются пары «ключ/значение», и для последнего ключа не находится парного значения. Программа предполагает, что значение ассоциируется с каждым ключом, поэтому при попытке чтения возникает неожиданно'е исключение ввода-вывода (чтение данных из файла описано в главе 9).
Теперь вы хотите сообщить о происходящем вызывающей стороне. Чтобы добавить в исключение строку, можно воспользоваться специальной версией конструктора класса Exception:
Public Sub New(ByVal message As String)
В следующем фрагменте в объект IOException добавляется новая строка с сообщением об отсутствии значения для последнего ключа, после чего исключение инициируется заново.
Dim excep As New IQException("Missing value for last key") Throw excep
Получив инициированное исключение, внешний код получает текст сообщения методом Message класса Exception и узнает о возникшей проблеме.
На практике в подобных ситуациях чаще возникает исключение класса EndOfStream-Exception, производного от IOException. Операции с потоками данных рассматриваются в главе 9.
Вторая ситуация реализуется элементарно благодаря главному правилу наследования: производный класс всегда может использоваться вместо базового класса. Вам лишь остается инициировать исключение производного класса, которое лучше подходит для данной ситуации.
Последний случай требует некоторой дополнительной работы, поскольку для этого потребуется определить класс, производный от существующего класса исключения. Предположим, вы хотите определить новый класс исключения, производный от System. 10. lOException. Новый класс отличается от старого лишь одним ReadOnly-свойством, возвращающим ключ, с которым не ассоциируется парное значение:
Public Class LastValueLostException Inherits System.I0.I0.Exception
Private mKey As String
Public Sub New(ByVal theKey As String)
MyBase.New("No value found for last key")
mKey = theKey
End Sub
Public Readonly Property LastKey() As String Get
Return mKey
End Get
End Property
End Class
Обратите внимание: имя созданного класса исключения завершается словом Exception. Это стандартное правило, которому мы настоятельно рекомендуем следовать. Получив исключение LastValueLostException, программист может воспользоваться свойством LastKey, значение которого передается в конструкторе нового класса исключения, и получить ключ, не ассоциируемый со значением. Следующая строка обеспечивает выдачу правильной информации методом Message базового класса Exception: MyBase.New("No value found for last key")
В этой строке вызывается конструктор базового класса (и в конечном счете конструктор предка Exception).
Возможно, вы заметили, что в классе LastValueLostException не переопределяются другие методы — такие, как метод ToString, унаследованный от Exception. В стандартных ситуациях объекты исключений всегда должны выводить стандартные сообщения.
Как использовать созданный класс в программе? Например, если последний ключ без парного значения был равен «oops», исключение будет инициироваться следующей командой:
Throw New LastValueLostException("oops")
Наличие нескольких секций Catch
Одной секции Try в VB .NET может соответствовать несколько секций Catch. Каждая секция перехватывает определенную категорию исключений, при этом для идентификации ошибок используются объекты классов, производных от базового класса Exception. Пример:
Sub Main()
Dim args(). argument As String Try
args = Environment.GetCormandLineArgs()
ProcessFile(argsd))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLine("ERROR - No file name supplied")
Catch ioProblem As System.10.I0Exception
Console.WriteLine("ERROR - can't process file named " & args(D)
Catch except As Exception
' Прочие исключения
End Try
Console.WriteLine("Press enter to end")
Console. ReadLine()
End Sub
В данном примере программа последовательно просматривает все секции Catch, пытаясь найти совпадение. Если пользователь не указал имя файла, совпадение будет найдено в первой секции. Вторая секция должна совпадать в том случае, если при вызове ProcessFile не удастся обработать файл (возможные причины рассматриваются далее). Если первые два случая не подошли, остальные исключения перехватываются последней секцией Catch.
Обнаружив подходящую секцию Catch, VB выполняет ее. Код других секций Catch при этом не выполняется.
Совпадение считается обнаруженным, если текущее исключение относится к типу, указанному в заголовке секции Catch, или производному от него. Например, класс FileNotFoundException является производным от I0Exception, поэтому следующий фрагмент неправилен:
Try
ProcessFile(args(1))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLinet"ERROR = No file name supplied")
Catch IOProblem As System.IO.l0Exception
Console. WriteLinet "ERROR = can't process file named " & args(D)
Catch fileNotFound As System.IO.FileNotFoundException
End Try
Специализированное исключение FileNotFoundException будет поглощено предыдущей секцией, перехватывающей исключение базового класса l0Exception.
Из сказанного следует, что размещать секции Catch после секции Catch e As Exception бесполезно. Указание типа Exception в первой секции Catch автоматически перекрывает все остальные секции (кстати говоря, секция Catch без явного указания типа исключения считается эквивалентной Catch e As Exception). Также следует учитывать, что пустая секция с условием Catch e As Exception напоминает очень опасную конструкцию On Error Resume из прежних версий VB.
Несмотря на все опасности, связанные с перехватом обобщенных исключений Catch e As Exception, эту проверку рекомендуется включать в последнюю секцию Catch любого блока Try — особенно на стадии разработки и тестирования, поскольку эта проверка помогает лучше изолировать ошибки. Если все остальные способы не помогают, попробуйте вывести содержимое стека на консоль или в файл методом StackTrace класса обобщенного исключения Exception. Пример:
Try
ProcessFile(argsd))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLine("ERROR - No file name supplied")
Catch fnf As System.I0.FileNotFoundException
Console.WriteLinet"ERROR - FILE NOT FOUND")
Catch ioProblem As System.I0.lOException
Console.WriteLine("ERROR - can't process file named " & args(1))
Catch e As Exception
Console.WriteLinet"Please inform the writer of this program " & _
"of this message")
Console.Writete.StackTrace)
End Try
Что произойдет, если возникшее исключение не подойдет ни к одной из секций Catch, а в конце блока Try-Catch отсутствует универсальная секция Catch e As Exception? В этом случае исключение передается в секцию Try верхнего уровня, заключающую код внутренней секции Try. Если подходящая секция Catch не будет найдена и во внешней секции Try, поиск продолжается в методе, от которого поступил вызов. Вероятно, именно это и произойдет при вызове метода ProcessFi I e из предыдущего примера — метод ProcessFi 1е передает все необработанные исключения (в форме объекта Exception) в процедуру Sub Main.
Если исключение не будет перехвачено ни одной секцией Try в методе, управление переходит в секцию Finally, а затем немедленно передается за пределы метода. Таким образом, обработку исключений можно рассматривать как невероятно мощную (и притом интеллектуальную) разновидность GoTo. Интеллектуальность заключается в автоматическом выполнении завершающего кода в секции Finally.
В общем случае, если исключение не было обработано программой вплоть до точки входа в приложение, .NET выводит сообщение с описанием исключения и содержимое стека с информацией обо всех вызванных методах на момент возникновения исключения.
В VB .NET секция Catch может дополняться условием When, расширяющим возможности ее применения. Синтаксис выглядит следующим образом:
Catch badnameException When theName - String.Empty
Подготовка к структурной обработке исключений
Прежде чем переходить к примерам, демонстрирующим обработку исключений на практике, необходимо познакомиться с некоторыми обстоятельствами. Во-первых, при структурной обработке исключений в программу включается дополнительная ветвь, которая автоматически выполняется при возникновении каких-либо аварийных ситуаций. Кроме того, при обработке исключений VB .NET автоматически создает объект, содержащий информацию об ошибке.
Когда в программе происходит исключение, встроенный механизм начинает искать обработчик, подходящий для данного объекта исключения (то есть для конкретной причины ошибки). Речь идет не о наборе GoTo, запутывающих логику программы, — обработка исключения больше напоминает запасную дорогу, идущую параллельно главной магистрали и связанную с ней несколькими переездами — настоящей мечте любого водителя, попавшего в пробку. Если в программе что-то пойдет не так, управление автоматически передается ветви, содержащей логику обработки исключений (если, конечно, вы ее запрограммировали). После этого исключение либо рассматривается одним из обработчиков, либо передается дальше по цепочке.
В VB .NET для обработки исключений существует синтаксическая конструкция, называемая блоком Try-Catch. Допустим, у нас имеется консольное приложение ProcessFile. Предполагается, что пользователь запускает его в режиме командной строки командой вида ProcessFile имя_файла
Имя файла передается в виде параметра. Как это обычно бывает, пользователи будут делать все, чтобы сбить бедную программу с толку. В частности, они могут:
ввести имя несуществующего файла;
ввести имя файла, недоступного для выполнения данной операции.
Программа должна быть написана так, чтобы учитывать все возможные ошибки со стороны пользователя. Ниже приведен пример простого блока Try-Catch, который может входить в приложение ProcessFile:
Module Exceptionl Sub Main()
Dim args() As String Try
args = Environment.GetCommandLineArgs()
ProcessFile(argsd))
Catch
Console.WriteLine("ERROR")
End Try
Console.WriteLine("Press enter to end")
Console. ReadLine()
End Sub
Sub ProcessFiletByVal fileName As String)
' Обработка файла
Console.WriteLine("Am processing " & fName)
End Sub
End Module
Секция Try блока Try-Catch содержит «правильный» код — в данном примере это вызов ProcessFile (вызов Environment.GetCommandLingArgs() заключен в секцию Try, потому что он тоже может инициировать исключение — например, если ваша программа работает на платформе, не поддерживающей передачи аргументов в командной строке).
Секция Catch в блоке Try-Catch необходима, потому что некоторые невнимательные пользователи не обращают внимания на указания. Если в приведенном фрагменте пользователь забывает ввести имя файла, программа пытается обратиться к имени файла, что приводит к исключению IndexOutOfRangeExceptl on, поскольку элемент с указанным индексом отсутствует в файле. При возникновении исключения управление передается в дополнительную ветвь, то есть в блок Catch, который в нашем примере просто выводит строку ERROR в консольном окне.
Из блока Try, как и из других управляющих конструкций VB .NET (таких, как циклы For и Do), можно выйти немедленно командой Exit Try. Впрочем, применение Exit Try обычно считается проявлением плохого стиля программирования.
Содержание
|
Вперед
|
Проверка ошибок и обработка исключений
Традиционный механизм обработки ошибок, использовавшийся в прежних версиях VB, а также в программировании СОМ и Windows, основан на проверке возвращаемого значения функции и выборе действий. Обычно для проверки возвращаемого значения в программе создается аналог конструкции Select Case, причем значения интерпретируются абсолютно произвольно. Например, в одном случае 0 означает успех, а в другом — неудачу. А в приведенном ниже фрагменте кода VB6 коды выглядят и вовсе странно:
Select Case Error-Number
Case 57
MsgBox "Your printer may be off-line."
Case 68
MsgBox "Is there a printer available?"
' Другие секции Case
Case Else
' Все остальные случаи
End Select
Подобные конструкции работают, но их трудно читать и еще труднее изменять в процессе сопровождения программы. Можно уверенно сказать, что эта схема таит в себе широкие возможности для ошибок программирования. Например, вы можете перепутать коды ошибок или забыть проверить некоторые из возвращаемых значений. Кроме того, писать один и тот же код проверки при каждом вызове функции Windows API, по крайней мере, утомительно. Хотя в некоторых ситуациях возвращаемое значение приходится проверять независимо от выбранной схемы обработки ошибок, не стоит превращать это в постоянную практику. Также следует учитывать фактор эффективности: структурная обработка исключений быстрее программируется, отнимает меньше времени при сопровождении, а нередко и выполняется быстрее!
Рекомендации по использованию исключений
Исключения выглядят эффектно, и новички часто склонны злоупотреблять ими. В самом деле, стоит ли тратить время на анализ пользовательского ввода, когда можно просто инициировать исключение? Не поддавайтесь соблазну. При неправильном использовании обработка исключений существенно замедляет работу программы. Ниже приведены некоторые рекомендации по использованию исключений в программе.
-
Исключение является признаком аварийной ситуации; не используйте исключения для простой передачи информации (мы видели программу, в которой при успешном завершении функции инициировалось исключение SUCCESS_EXCEPTION).
Не заменяйте тривиальные проверки обработкой исключений. Например, исключения не стоит применять для проверки достижения конца файла (EOF).
Избегайте раздробленной обработки исключений, при которой едва ли не каждая команда заключается в отдельный блок Try-Catch. Заключение всей операции в один блок Try-Catch обычно предпочтительнее использования нескольких блоков.
Не поглощайте исключения конструкциями вида Catch e As Excepti on с пустым блоком команд, если для этого нет достаточно веских причин. Такая конструкция эквивалентна бездумному применению On Error Resume в старых программах VB, и пользоваться ею нежелательно по тем же причинам. Если в программе произошло исключение, обработайте его или передайте для дальнейшей обработки.
Последнюю рекомендацию скорее можно назвать «правилом хорошего тона». Передавая исключение во внешний код для последующей обработки, добавьте в него новую информацию (или определите новый класс исключений), чтобы внешний код мог точно определить, что произошло и какие меры были приняты для того, чтобы исправить ситуацию.
Секция Finally
При использовании блоков Try-Catch нередко существует код, который должен выполняться как при нормальном завершении, так и при возникновении исключения. Например, в обоих случаях следует закрыть файлы, вызвать методы Dispose и т. д. Даже в простом примере, приведенном в начале главы, потребовалась команда ReadLine, чтобы консольное окно оставалось на экране до нажатия клавиши Enter.
Чтобы некоторый фрагмент выполнялся независимо от того, возникнет ли в программе исключение или нет, в блок Try-Catch включается секция Finally, выделенная в следующем примере жирным шрифтом:
Sub Main()
Dim args(). argument As String
args = Environment. GetCommandLineArgs()
Try
ProcessFile(argsd))
Catch
Console.WriteLine("ERROR")
Finally
Console.WriteLine("Press enter to end")
Console.ReadLine()
End Try
End Sub
Код секции Finally выполняется до передачи исключений внешнему.коду и до возвра-щения из функции .