До настоящего момента мы делали вид, что в наших программах ошибок не бывает. Но даже в самой
надежной программе иногда возникают непредвиденные ситуации — например, такие,
как потеря сетевого соединения или отсутствие бумаги в принтере. Конечно, программист
в этом не виноват, но винить пользователя в разрыве связи тоже было бы несправедливо.
По крайней мере, в таких ситуациях программа не должна завершаться аварийно.
Она должна:
Впрочем,
выполнить эти требования бывает непросто. Сетевой код обычно никак не связан
с объектами, состояние которых требуется сохранить. Соответственно, программисту
нужны средства для передачи управления и для оповещения других объектов о происходящем,
чтобы они могли принять нужные меры.
Непредвиденные
ситуации возникают и в хорошо написанных программах, но в плохо написанных программах
они возникают чаще. Например, если данные не проверяются перед использованием,
может возникнуть ошибка деления на 0 или переполнение контейнера, не рассчитанного
на такой объем данных. Вы как программист обязаны проследить за тем, чтобы этого
не случилось. Какую бы форму обработки ошибок вы ни избрали, она все равно не
заменит проверки данных перед использованием!
Так или иначе,
хороший программист знает, что наш мир не идеален, а непредвиденные ситуации
встречаются чаще, чем хотелось бы. В этой главе мы вернемся с небес на землю.
Итак, в отличие
от прежних версий VB в VB .NET поддерживается механизм структурной обработки
исключений (или просто обработки исключений). В этой главе вы не только
познакомитесь с синтаксисом обработки исключений в приложениях VB .NET, но и
узнаете, какими преимуществами обладает этот механизм. В частности, при обработке
исключений отпадает необходимость даже в более или ме-
нее оправданном
применении GoTo, описанном в главе 3. Однако ничто хорошее не дается бесплатно,
поэтому вы также должны помнить о некоторых тонкостях, связанных со структурной
обработкой исключений.
Читатели,
привыкшие к синтаксису прежних версий VB, могут продолжать использовать старую
конструкцию On Error. Но в новых программах это выглядит довольно глупо. Давно
пора отказаться от архаичного способа обработки ошибок, появившегося на заре
развития вычислительной техники! (Одновременное использование двух способов
в одной процедуре не разрешается.)
Проверка
ошибок и обработка исключений
Традиционный
механизм обработки ошибок, использовавшийся в прежних версиях 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, по
крайней мере, утомительно. Хотя в некоторых ситуациях возвращаемое значение
приходится проверять независимо от выбранной схемы обработки ошибок, не стоит
превращать это в постоянную практику. Также следует учитывать фактор эффективности:
структурная обработка исключений быстрее программируется, отнимает меньше времени
при сопровождении, а нередко и выполняется быстрее!
Подготовка
к структурной обработке исключений
Прежде чем
переходить к примерам, демонстрирующим обработку исключений на практике, необходимо
познакомиться с некоторыми обстоятельствами. Во-первых, при структурной обработке
исключений в программу включается дополнительная ветвь, которая автоматически
выполняется при возникновении каких-либо
аварийных ситуаций. Кроме того, при обработке исключений 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 обычно считается проявлением плохого стиля программирования.
Следующим
шагом должен быть перехват и последующий анализ исключения. Для этого команда
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, чтобы продолжить
его обработку.
Наличие
нескольких секций 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
Выше уже говорилось
о том, что метод 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")
Мы создали
новый класс исключений, производный от IOExcepti on, потому что потенциальная
проблема явно относилась к категории ввода-вывода. Допустим, ситуация имеет
более общий характер и для базового класса не существует других очевидных кандидатов,
кроме класса Exception. Впрочем, это не совсем верно — лучший выбор существует
всегда. Мы настоятельно рекомендуем выбирать в качестве базового не сам класс
Exceptlon, а производный от него класс AppllcationException.
Дело в том,
что .NET Framework различает исключения, возникшие в результате проблем исполнительной
среды (например, нехватки памяти или дискового пространства) и проблем, обусловленных
работой вашего приложения. Именно исключения второй категории должны быть производными
от AppllcationExcepti on, поэтому именно этот класс следует выбирать базовым
при определении обобщенных исключений в программе.
Учтите,
что класс IOException, как и многие стандартные исключения, является произ-водным
от Exception, а не от ApplicationException.
Исполнительная
среда помогает сделать следующий шаг. Иерархия исключений расходится на две
ветви, показанные на рис. 7.1.
Рис.
7.1. Две основные ветви иерархии исключений
Классы Exceptlon,
AppllcationExcepti on и SystemExcepti on обладают одинаковой функциональностью.
Существование трех классов вместо одного — не более чем удобная абстракция,
благодаря которой становится проще понять исключения, возникающие в ваших программах.
Исключения
как замена для goto
Обработка
исключений в сочетании с определением собственных классов исключений позволяет
полностью отказаться от использования 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.
При использовании
блоков 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 выполняется до передачи исключений внешнему.коду и до возвра-щения
из функции.
Рекомендации
по использованию исключений
Исключения
выглядят эффектно, и новички часто склонны злоупотреблять ими. В самом деле,
стоит ли тратить время на анализ пользовательского ввода, когда можно просто
инициировать исключение? Не поддавайтесь соблазну. При неправильном использовании
обработка исключений существенно замедляет работу программы. Ниже приведены
некоторые рекомендации по использованию исключений в программе.