Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne10.
Занимаясь программированием уже многие годы, я не только теоретически, но и на собственном опыте осознал, что программы, которые мы, программисты, разрабатываем, относятся к средствам повышенной опасности. В простых ситуациях ошибки в программах, могут стать источником разочарований и огорчений отдельного человека. В серьезных ситуациях ошибки чреваты катастрофой. На людей, создающих программы, возлагается ответственность, если хотите, то и моральная ответственность за надежность и правильность работы их творений. И хотя программа программе рознь не нужно думать, что сегодня можно состряпать что либо на авось, а завтра, когда наступит время серьезной программы, тогда и придет черед надежному программированию. Нет, программировать надежно нужно всегда.
Можно ли создать надежную программную систему? Вспоминая опыт собственной работы, могу сказать, что в наиболее ответственных случаях, когда речь шла об экспериментах, связанных с космосом, решение заключалось в том, что программа создавалась независимо двумя коллективами, начиная от разработки алгоритма, кончая системой тестов. Только после того, как обе системы правильно работали на всех предъявленных обеими сторонами тестах, программа принималась в эксплуатацию. Это были шестидесятые годы. Сегодня мы живем в другом мире, с другими возможностями. Вот цитата из письма, которое я, как бета-тестер Office 2000 получил от команды, занимающейся отладкой этой системы:
"...Another added feature to our beta program will be the
privilege tonominate otherbeta testers . Over our beta program we receive over 500,000 requests to participate on the Office beta program. This upcoming beta we are going to allow you, our top beta tester to add your co worker, friend orneighbor to our program".
Вряд ли здесь необходим точный перевод. Речь идет о том, что одной из привилегий лучших бета - тестеров будет возможность рекомендовать тестеров для участия в новой программе тестирования. И делается это потому, что возникла проблема отбора тестеров. Заметьте, поступило 500000 заявок на участие в тестировании программного продукта. Следует заметить, что отношение к тестерам и их работе самое серьезное. Могу сказать, что ни один из посланных мной отчетов не остался без внимания. Конечно, при такой коллективной и независимой отладке можно в гораздо большей степени надеяться на надежность программ.
Поговорим сейчас о том, что должен делать каждый из программистов, работающих в среде Office 2000, чтобы создать надежный продукт и уменьшить число возможных ошибок, не надеясь на постороннюю помощь. Мы рассмотрим три темы:
Ошибки неизбежно сопровождают всякую сколь-нибудь сложную программу. Это утверждение может вызвать раздражение у начинающего программиста, но человек более опытный посчитает его само собой разумеющимся и сосредоточится на средствах, предлагаемых программным окружением для борьбы с этим неизбежным злом. Самые неприятные, дорогостоящие ошибки это те, что допущены при определении основных задач и целей приложения, при проектировании структуры его управления и потоков передачи данных, а также ошибки, связанные с неверной реализацией алгоритмов. Часто они не проявляются непосредственно в виде сбоев в работе программы, а обнаруживаются после довольно длительного использования приложения и требуют для своего исправления существенных изменений в проекте и программе. Их корни могут иметь как объективную природу (сложность решаемых задач), так и субъективную (непонимание заказчиком того, что ему надо). Как бороться с такими ошибками? Выделим два известных подхода к этой проблеме. Первый из них состоит в повышении уровня языков и систем программирования, чтобы разработчик мог оперировать при создании системы понятиями предметной области, для которой она создается. Другой подход связан с идеей быстрого прототипирования, т. е. создания на ранней стадии разработки системы ее работающего прототипа, в ходе экспериментов с которым заказчик может уточнить свои требования. Оба эти подхода нашли отражение в Office 2000. Первый реализован в объектно-ориентированном подходе к построению как самой инструментальной системы, так и создаваемых в ней приложений. Развитие в VBA возможностей пользовательских модулей классов, в частности введение полиморфизма, - еще один шаг в направлении повышения уровня языка. Благодаря классам, можно вводить в программы объекты, представляющие те или иные понятия предметных областей. Реализации второго из указанных подходов существенно способствует визуальный стиль программирования, принятый во всех приложениях Office 2000. Создавая собственные меню и диалоговые окна, можно быстро спроектировать интерфейс системы и передать пользователю (тестерам) действующий ее прототип для оценки соответствия его требованиям. Затем завершить реализацию с учетом замечаний и уточнений, возникших у тестеров при работе с прототипом. Это позволит избежать многих ошибок, неизбежных при разработке системы без контактов с ее будущими пользователями.
Одним из факторов, влияющих на надежность программ, является сам язык программирования. Известно, что язык, в котором есть объявление переменных по умолчанию, разрешены преобразования данных по умолчанию в процессе вычислений, нет строгого контроля типов, - такой язык является ненадежным, в нем значительно легче создать ненадежную программу, содержащую трудно выявляемую ошибку. Язык VBA трудно причислить к надежным языкам, он скорее занимает по шкале надежности срединное положение. Во многом, это связано с историей его возникновения. Именно поэтому надо предпринимать ряд мер, благоприятствующих повышению надежности. О многих из них мы уже говорили в разных частях этой книги. Напомним некоторые из них:
Variant и Object. В этом случае на Вашей стороне будет контроль типов, что позволит избежать многих возможных ошибок.ByRef и ByVal, помните об особенностях передачи аргументов по ссылке в VBA.
(рис 10.1) Флажки вкладки Editor
Прелесть работы программиста во многом связана с отладкой. Почему программисты нарушают известные им требования, - не задают комментарии, не описывают детально суть решаемой задачи и не следуют другим полезным советам. Чаще всего, причина в нетерпении, им хочется скорее посмотреть, как же работает программа, увидеть результаты ее работы. Отладка - это некоторый детективный процесс. Вновь созданную программу мы подозреваем в том, что она работает не корректно. Презумпция невиновности здесь не работает. Если удается предъявить тест, на котором программа дает неверный результат, то доказано, что наши подозрения верны. Втайне мы всегда надеемся, что программа заработает правильно с первого раза. Но цель тестирования другая, - попытаться опровергнуть это предположение. И только потом, исправив все выявленные ошибки, получить корректно работающую программу. К сожалению, отладка не может гарантировать, что программа корректна, даже если все тесты прошли успешно. Отладка может доказать некорректность программы, но она не может доказать ее правильности.
Искусство тестера состоит в том, чтобы создать по возможности полную систему тестов, проверяющую все возможные ветви вычислений. Поясним это на самом простом примере. Пусть программа находит сумму первых N элементов массива X, содержащего M элементов. Кроме "нормального" теста, проверяющего ситуацию, в которой 1<N<M, следует проверить и крайние случаи: N=1, N=M, N=0, N<0, N>M. Но это простой случай, а циклы обычно вложенные, и внутри них производится разбор случаев, внутри которых свои циклы.
Ранее мы упоминали закон "чечако" - новичок может подвесить любую систему. Этому есть объяснение, по незнанию, он задаст одно из мало вероятных сочетаний входных данных (работая в визуальной среде, нажмет самую неподходящую для данной ситуации кнопку). Поэтому тестер, ведущий отладку, должен уметь встать и на позицию новичка, система тестов должна гарантировать, что программа корректно работает не только в "нормальных ситуациях", но и имеет "защиту от дурака" и не приведет к зацикливанию или останову в крайних, мало вероятных ситуациях.
Сложность отладки заключается и в том, что, обнаружив и исправив ошибку, вы получаете новую программу, для которой процесс отладки нужно начинать заново, снова пропустив все тесты. Известно, что в программах встречаются заколдованные места, - исправление одной ошибки ведет к появлению новой. В таких случаях лучшим выходом бывает поиск другого, принципиально иного решения задачи.
Часть ошибок программы ловится автоматически еще на этапе компиляции. Сюда относятся все синтаксические ошибки, ошибки несоответствия типов и некоторые другие. Однако
Прежде, чем приступить к подробному рассмотрению этих средств, напомним, что в ходе отладки программа может находиться в одном из трех состояний: проектирования, вычисления и прерывания. Закончив проектирование, можно запустить программу на исполнение. Прервав исполнение программы в заданной точке, перейдя в состояние прерывания, можно проконтролировать значения переменных и свойств объектов в данной точке и, если требуется, изменить эти значения "вручную". При этом можно изменить порядок выполняемых операторов, задавать следующий исполняемый оператор, можно редактировать программный текст перед продолжением вычисления. Переход из состояния вычисления в состояние прерывания может происходить по самым разным причинам, например, по достижении точки прерывания, при выполнении одного из многочисленных условий прерывания, из-за пошагового выполнения программы. Все эти возможности мы еще обсудим, а сейчас рассмотрим один особый случай. Иногда программа "зацикливается" и необходимо принудительно перевести ее в с остояние прерывания. Как остановить работающую программу? Просто нажмите знакомую еще по работе в DOS пару клавиш Ctrl+Break. На экране появится следующее диалоговое окно с сообщением об остановке.
(рис 10.2) Сообщение о прерывании работы программыВ этом окне нажатие кнопки "End" остановит исполнение программы, нажатие кнопки "Continue" возобновит ее выполнение, программа снова перейдет в состояние вычисления. Если нажать кнопку "Debug", то выполнение программы прервется, и мы перейдем в режим прерывания -
Как и во многих других случаях, интерфейс отладчика VBA избыточен, - к одним и тем же инструментальным средствам можно добраться по-разному. В зависимости от привычек можно использовать панель Debug с инструментальными кнопками, можно использовать команды из меню Debug и View, можно использовать горячие клавиши. На следующем рисунке показана панель Debug (Отладка). Заметьте, на этой панели больше кнопок, чем в стандартном варианте, предлагаемом по умолчанию. Используя режим настройки (Customize) я вынес на эту панель дополнительные кнопки, задающие инструменты отладки.
(рис 10.3) Панель отладки с инструментальными кнопкамиКнопки этой панели соответствуют командам меню "Отладка" (Debug) и меню "Вид" (View), которые представлены на следующих рисунках:

(рис 10.5) Команды меню View(рис 10.4) Команды меню DebugНа рисунках для большинства команд меню показаны соответствующие им кнопки и комбинация горячих клавиш, выполняющих вызов тех же инструментальных средств. Дадим краткое описание этих инструментальных средств:
Первая из этих кнопок включает и выключает состояние проектирования. Вторая запускает проект на компиляцию. Запуск на компиляцию стоит делать чаще, не дожидаясь завершения проектирования. Так можно раньше выявить некоторые ошибки.
Первая из этих кнопок запускает программу на выполнение. Более точно это означает следующее. Если курсор стоит в любом месте некоторой процедуры, то на выполнение запускается эта процедура. Если активна некоторая форма, то запускается эта форма, в противном случае запускается макрос. Вторая кнопка этой группы прерывает выполнение и переводит программу в состояние прерывания. Третья кнопка приостанавливает выполнение и производит сброс проекта,- чистятся все стеки, так что происходит переход в состояние проектирования.
Эти две кнопки позволяют работать с точками прерывания. Точки прерывания играют важную роль в процессе отладки. Чаще всего, для понимания поведения программы достаточно проанализировать состояние вычислений в некоторых ее точках, например, после завершения очередного цикла. Точки прерывания ставятся в таких контрольных точках программы, после чего программа запускается на выполнение. Достигнув первой по ходу вычисления точки прерывания, выполнение приостанавливается, программа переходит в состояние прерывания. Состояние программы анализируется и затем можно произвести запуск вычислений до следующей точки прерывания. Таким образом, точки прерывания размечают программу, являясь промежуточными финишами процесса выполнения.
Первая из кнопок этой группы включает и выключает точку прерывания в строке, на которой установлен курсор. Вторая позволяет одним щелчком удалить все точки прерывания. Заметьте, точку прерывания можно установить и удалить без использования этих кнопок. Слева от окна кода есть специальное поле, выделенное серым цветом. В этом поле желтый стрелкой показан курсор, задающий текущий выполняемый оператор, в этом же поле черные кружочки отмечают точки прерывания. Достаточно щелкнуть левой кнопкой мыши в этом поле, чтобы соответствующая строка стала точкой прерывания, и, наоборот, если точка прерывания была установлена, то щелчок по ней удаляет ее.
Важная группа кнопок, включающая средства управления процессом вычислений. С их помощью на важных участках можно проследить выполнение программы с точностью до оператора. Первая из кнопок (Step Into - ей соответствует нажатие клавиши F8) задает пошаговый, пооператорный режим выполнения. При вызове процедур и функций в этом режиме начинается их
Очень полезные кнопки, по крайней мере, первая из них. С ее помощью можно изменять порядок вычислений, предписанный программой. Мы уже говорили, что в процессе отладки желтая стрелка в левом поле задает строку с текущим
Следующая группа кнопок задает инструментальные средства, позволяющие наблюдать за состоянием программы, значениями ее переменных. Этим кнопкам соответствуют команды меню View. Первые три кнопки вызывают соответственно три окна наблюдения за состоянием вычислений - окно локальных переменных (Locals), окно проверки с отладочными результатами вычислений (Immediate), окно контрольных выражений (Watch). Следующая пара кнопок также предназначена для работы с контрольными выражениями, открывая специальные окна для показа и добавления контрольных выражений. Наконец, последняя кнопка в этой группе очень полезна при работе в ситуациях, когда отлаживаются рекурсивные процедуры или процедуры, где встречается глубокая вложенность вызовов. Она позволяет показать стек вызовов процедур в текущий момент. Чуть позже мы подробнее рассмотрим работу с этими окнами.
При отладке весьма полезны и другие инструментальные средства, не связанные непосредственно с панелью Debug. Они остались вне нашего рассмотрения, но о некоторых из них все-таки скажем пару слов.
Эту пару кнопок инструментальной панели Edit мы уже упоминали. Первая из них позволяет закомментировать выделенный блок, вторая - отменить сделанные комментарии. Поскольку при отладке зачастую приходится опускать выполнение отдельных фрагментов, то эти кнопки относятся к часто используемым средствам.
Еще одна полезная кнопка этой панели Quick Info. Она позволяет получить подсказку о типе переменной, обо всех параметрах вызываемой функции и их типах. Для получения подсказки достаточно подвести курсор к соответствующей переменной или функции и щелкнуть кнопку. Тут же появится окно с подсказкой. Отметим также еще одно весьма полезное свойство, если просто подвести курсор к переменной, то тут же появляется окно подсказки, в котором показано значение этой переменной. Это эффективное средство наблюдения за состоянием переменных, не требующее специальных окон, наблюдение делается на лету.
Мы хотим теперь подробнее остановиться на окнах отладчика, позволяющих следить за состоянием вычислительного процесса. Чтобы сделать изложение конкретным, обратимся к примеру предыдущей лекции, в котором показана работа с бинарным деревом, задающим словарь. Единственное сделанное добавление связано с введением в модуле глобальной переменной GlobeVar. Приведем текст отлаживаемой процедуры:
Public Sub WorkwithBinTree() Dim MyDict As New BinTree Dim englword As String, rusword As String GlobeVar = "Привет!" 'Создание словаря MyDict.SearchAndInsert key:="dictionary", info:="словарь" MyDict.SearchAndInsert key:="hardware", info:="аппаратура, аппаратные средства" MyDict.SearchAndInsert key:="processor", info:="процессор" MyDict.SearchAndInsert key:="backup", info:="резервная копия" MyDict.SearchAndInsert key:="token", info:="лексема" MyDict.SearchAndInsert key:="file", info:="файл" MyDict.SearchAndInsert key:="compiler", info:="компилятор" MyDict.SearchAndInsert key:="account", info:="учетная запись" 'Обход словаря MyDict.PrefixOrder 'Поиск в словаре englword = "account": rusword = "" MyDict.SearchAndInsert key:=englword, info:=rusword Debug.Print englword, rusword 'Удаление из словаря MyDict.DelInTree englword englword = "hardware" MyDict.DelInTree englword 'Обход словаря MyDict.PrefixOrder 'Debug.Print MyDict End Sub
Наиболее простой и эффективный способ следить за состоянием вычислений заключается в том, чтобы во время отладки включить окно локальных переменных (Locals). Напоминаем, для этого есть соответствующая инструментальная кнопка панели Debug и команда меню View. В этом окне будут отображены все описанные в текущей процедуре переменные, их значения и типы. Обновление окна происходит автоматически при каждом переключении из режима выполнения в режим прерывания, а также при перемещении по стеку вызовов процедур. Первая переменная в этом окне - это специальная переменная с именем модуля, которая показывает все глобальные переменные, описанные на уровне текущего модуля. Для модуля класса первая переменная имеет имя Me и показывает свойства класса. Окно разбито на три столбца для имени переменной, ее значения и типа. Таким образом, окно позволяет следить за всеми локальными переменными текущей выполняемой процедуры и частью ее глобальных переменных, тех, которые описаны в текущем модуле. Отметим также,
что объекты и массивы могут быть свернуты и развернуты, используя обычную технику знаков "+" и "-".
Давайте начнем отладку нашего примера, поставим первую точку прерывания после предварительного формирования словаря на операторе, запускающем
(рис 10.6) Окно локальных переменных процедуры стандартного модуляЗаметьте, в окне Locals показана глобальная переменная модуля Examples и все локальные переменные исполняемой процедуры WorkWithBinTree. Объект MyDict класса BinTree на рисунке частично раскрыт. Как видите, окно позволяет тщательно проанализировать достаточно сложную структуру этого объекта. В окне Locals можно не только наблюдать за значениями переменных, но и изменять эти значения, редактируя поле Value.
Давайте нажмем клавишу F8 для выполнения следующего PrefixOrder класса BinTree, вызванный объектом MyDict. Выполним в пошаговом режиме несколько операторов и остановимся после первой отладочной печати. Заметьте, что поскольку теперь исполнение идет в другом контексте, - исполняется модуль класса, то окно Locals полностью обновится и будет теперь задавать состояние исполняемого метода класса. Взгляните, как это выглядит на рисунке:
(рис 10.7) Окно локальных переменных метода классаКроме показа переменных окно Locals позволяет проанализировать текущее состояние стека вызовов. Для этого в правом верхнем углу окна есть специальная кнопка, щелчок которой дает тот же эффект, что соответствующая инструментальная кнопка меню Debug. Продемонстрируем ее использование чуть позже, а сейчас обращаем внимание, что результаты исполнения оператора Debug.Print появились в окне Immediate, к рассмотрению которого мы и переходим.
В этом окне появляется вся отладочная информация, поступающая в результате выполнения методов ?", после которого печатается запрашиваемое выражение. После нажатия Enter в окне появляется значение выражения. В предыдущих лекциях мы неоднократно пользовались запросами для вычисления значений различных функций. Окно позволяет изменять и значения глобальных переменных модуля. Взгляните, как выглядит окно проверки, в котором выполняются некоторые вычисления в состоянии прерывания процедуры. В частности, в этом окне дважды присваивалось новое значение глобальной переменной и выдавался запрос на получение ее значения. Здесь же был вызван на исполнение метод PostfixOrder объекта root. В этом же окне появились результаты его работы, напечатанные оператором Debug.
(рис 10.8) Вычисления в окне проверки
Привычное назначение окна Watch в той или иной среде программирования состоит в том, что оно является окном наблюдений и позволяет следить за состоянием некоторых переменных и выражений программы. Теперь, во многом, эта функция перешла к окну Locals. Тем не менее, и окно Watch используется для этой же цели. В отличие от окна Locals, выражения, появляющиеся в этом окне, не связаны с контекстом выполняемой процедуры. Программист сам формирует список наблюдаемых переменных и выражений, при этом всегда можно точно указать область определения наблюдаемой переменной.
Наблюдение за состоянием выражений перестало быть главной функцией окна Watch. Выражения, хранящиеся в этом окне, являются контрольными выражениями (может быть, точнее было бы - контролируемыми выражениями). Контрольными могут быть любые арифметические, логические или строковые выражения. Для заданных контрольных выражений отладчик позволяет следить за:
True. Это вариация предыдущего случая и можно указать много ситуаций, когда обнаружение такого факта, приостанавливающего вычисления весьма полезно при отладке.Следует только понимать, что злоупотреблять контрольными выражениями не следует, поскольку они замедляют процесс вычислений, поскольку перевычисляются на каждом шаге. Кроме того, по этой же причине следует аккуратно задавать контекст, в котором следует вычислять эти выражения.
Помимо окна Watch, в котором появляются контрольные выражения, есть еще окно для добавления нового контрольного выражения, напомним, это окно может быть вызвано соответствующей инструментальной кнопкой панели Debug или, чаще всего, командой Add Watch из контекстного меню, появляющегося при нажатии правой кнопки в окне кода. Вот как выглядит это окно:
(рис 10.9) Окно добавления контрольного выраженияВ поле "Expression" вводится новое контрольное выражение. В нашем примере - это переменная info, в общем случае это может быть любое выражение. В этом окне задается контекст, в котором выражение будет регулярно вычисляться по завершении каждого оператора. Контекст определяет процедуру, модуль и проект. В нашем примере мы указали все процедуры модуля BinTree текущего проекта. Как уже говорилось, чем шире контекст, тем больше проводится дополнительных вычислений по ходу отладки. Кроме контекста задается один из трех возможных типов контрольного выражения. Выбранный нами тип "Break When Value Changes" позволяет прервать вычисления всякий раз, когда в заданном контексте изменится значение контрольной переменной rusword. Добавленное контрольное выражение появляется в окне "Контрольные значения" (Watch).
Просмотреть значение и добавить новое контрольное выражение можно в окне быстрого просмотра Quick Watch. Для этого достаточно выделить выражение и нажать упоминавшуюся ранее инструментальную кнопку панели Debug, выполнить команду меню или нажать комбинацию горячих клавиш - Shift +F9. Вот как выглядит это окно
(рис 10.10) Окно быстрого просмотра контрольного выраженияЩелкнув по кнопке Add в этом окне, мы добавили переменную MyDict в окно Watch. Сравнивая окно Watch с окном Locals, отметим, что к трем колонкам Expression, Value и Type добавился еще один столбец Context, в котором задается контекст контрольного выражения. Взгляните, как выглядят эти окна после того, как мы добавили три контрольных выражения:
(рис 10.11) Окна отладчика после добавления контрольных выраженийЕще один вид информации о текущем состоянии вычисления представлен стеком вызовов процедур. Имя каждой вызванной процедуры помещается на вершину этого стека в момент ее вызова и убирается из него сразу после завершения этого вызова. Просмотреть стек вызовов можно, нажав клавиши Ctrl+L или выбрав инструментальную кнопку или команду "
Продолжим работу с нашим примером. Мы остановились в момент выполнения оператора, запускающего обход дерева. В ходе отладки мы ввели некоторые контрольные выражения. Продолжим теперь выполнение нашей программы. Очередное прерывание в ее работе наступило вследствие того, что изменилось значение контрольного выражения, переменная info получила новое значение в процессе работы процедуры поиска SearchAndInsert. Взгляните на соответствующее состояние окон отладчика в этот момент:
(рис 10.12) Окна отладчика в момент прерыванияЗаметьте, полученной информации достаточно, чтобы понять в какой процедуре произошло изменение значения переменной info. Но эта процедура рекурсивна и вызывалась неоднократно. Поэтому хотелось бы знать, на каком шаге ее вызова произошло изменение контрольной переменной. Для этого и служит стек вызовов, окно которого и было открыто щелчком соответствующей кнопки в окне Locals.
(рис 10.13) Окно стека вызововТри вызова функции SearchAndInsert были выполнены, прежде чем изменилось значение контрольной переменной. К сожалению, в окне стека вызовов не указаны параметры соответствующих вызовов. Но получить информацию об их значениях и о состоянии локальных переменных вызовов можно. Для этого нужно выделить интересующий вызов в стеке и щелкнуть по кнопке "Show". В результате в окне Locals ,будут отображены значения локальных переменных в момент выделенного вызова.
Разговор о средствах отладки будет не полным, если не рассмотреть специальный объект Debug.
Объект Debug специально сконструирован для проведения отладки. Он имеет два метода Print и Assert.
Его синтаксис:
Debug.Print список_выражений
Метод позволяет во время выполнения программы напечатать значения выражений из списка выражений в окне проверки Immediate. Трудно представить программу в период отладки, в которой бы многократно не использовался бы метод Print объекта Debug. Во всех наших примерах демонстрировалось его применение. Мы не будем останавливаться на некоторых деталях синтаксиса и управления выводом, при желании познакомиться с ними можно обратиться к справке.
Его синтаксис:
Debug.Assert булево_выражение
Всякий раз, когда при выполнении программы управление получает метод Assert, вычисляется значение булевого выражения и, если оно истинно, выполнение программы продолжается обычным образом. Если же значения выражения ложно, то происходит останов выполнения и программа переходит в состояние прерывания и, как обычно, начинается отладка и выяснение причин, повлекших ложность высказывания, заданного Assert выражением.
Заметьте, Assert - выражения подобны одному из типов контрольных выражений, которые также приводят к прерыванию, когда приобретают значение False. Синтаксис и выполнение отдельного оператора Debug.Assert, совершенно понятны. Однако есть смысл поговорить о том, какую роль играют Assert - утверждения в процессе отладки. Идея такова: программа разбивается на участки, каждый из которых начинается и завершается Assert - утверждением. Предполагается, что программист может задать в виде булевого выражения утверждение, которое должно быть истинно в этой точке программы. В типичном случае Assert - утверждения вставляются в начало и конец каждой процедуры и функции. В начале процедуры эти утверждения задают условия, которым должны удовлетворять исходные данные. В конце работы процедуры булевы выражения описывает требования, предъявляемые к результатам. Если начальные и конечные утверждения истинны, то полагается, что процедура была корректно вызвана и корректно завершила свою работу. Конечно же, чтобы использовать эту технику в полной мере следует иметь опыт доказательства
Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7 Debug.Assert (N >= 0) And (N < 8) If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If Debug.Assert Fact2 <= 5040 End Function
Заметьте, говорить о том, правильно или неправильно работает функция Fact2, вычисляющая факториал, не имеет особого смысла, если не упоминать интервал возможных значений ее входного параметра. В узком интервале значений, для которого она и была спроектирована, она работает корректно. Вне этого интервала использовать ее не следует. Assert - утверждение в начале этой функции приостановит выполнение, если будет сделана попытка использовать эту функцию вне тех пределов, для которых она гарантирует корректную работу. Если бы этих утверждений не было, то попытка вызвать эту функцию с отрицательным N или N, большим 7, привела бы к останову выполнения программы.
Мы уже говорили, что отладка, основанная на построении системы тестов, не может доказать правильность программы. Поэтому в P(X,Y) - программа, с заданными входными данными X и результатами Y. Предикат Q(X), определенный на входных данных, будем называть предусловием программы P, а предикат R(X,Y), связывающий входные и выходные переменные будем называть постусловием программы P. Будем также предполагать, что в ходе своей работы программа не меняет своих входных переменных X.
Программа P(X,Y) корректна по отношению к предусловию Q(X) и постусловию R(X,Y), если из истинности Q(X) до начала выполнения программы следует, что, будучи запущенной, программа завершит свою работу и по ее завершению будет истинным предикат R(X,Y). Условие корректности записывают в виде триады (триады Хоора) - Q(X) {P(X,Y)} R(X,Y)
Уже из этого определения становится ясно, что говорить о правильности следует не вообще, а по отношению к заданным спецификациям, например, в виде предусловия и постусловия. Доказать правильность триады для сложных программ, как уже говорилось, довольно сложно. Один из методов (метод Флойда) состоит в том, что программа разбивается на участки, размеченные предикатами - Q1, Q2, …QN, R. Первый из предикатов представляет предусловие программы, последний - постусловие. Тогда
Q1{P1}Q2; Q2{P2}Q3; …QN{PN}R
Нетрудно видеть, что введение Assert - утверждений является отражением
Заметьте, что вызов методов объекта Debug может встречаться только в период отладки программы. В тот момент, когда программа передается конечному пользователю, вызов методов этого объекта не должен встречаться в работающей программе. Это понятно, поскольку в этом режиме никакие отладочные окна не появляются, нет никаких окон проверки и методу Print некуда направлять свой вывод, носящий отладочный характер. И уж тем более не должно прерываться выполнение программы, если вдруг Assert - утверждение станет ложным. Конечный пользователь просто не будет знать, что нужно делать в этом случае.
Когда отладка завершена, вызовы методов Print и Assert не должны встречаться в выполняемой программе. Конечно, можно просто удалить эти вызовы из текста программы или закомментировать их. Однако жизненный цикл многих программ таков, что снова и снова приходится возвращаться к отладке ранее разработанной программы, внесению заплаток, созданию новой версии, введению новых возможностей. Поэтому удаление или комментирование вызовов объекта Debug не является лучшим способом. Гораздо удобнее иметь возможность включать или выключать отладочный режим при необходимости. Для этой цели и используются средства Debug. Идея состоит в том, что все вызовы методов этого объекта заключаются в обертку, заданную специальным оператором #If
Эта директива позволяет задать
#Const constname = expression
Имя константы строится по обычным правилам, а выражение в правой части может содержать только литералы (символьные константы) и другие Is. Эти константы, являются флажками и используются в операторе #If
Оператор If If имеет синтаксис, похожий на обычный оператор If:
#If expression Then statements [#ElseIf expression-n Then [elseifstatements]] [#Else [elsestatements]] #End If
Особенность состоит в том, что выражения состоят из констант периода компиляции и литералов, объединенных знаками операций. Все выражения вычисляются, поэтому все константы периода компиляции должны быть определены. Если выражение ложно, то соответствующие операторы не компилируются и не увеличивают тем самым размер программы. Основное назначение директивы
Приведем простой пример использования средств
'Константа conDebug включает или выключает отладочную печать
#Const conDebug = True
'#Const conDebug = False
Public Sub TestDebug()
#If conDebug Then
Debug.Print "Привет!"
#Else
MsgBox ("Привет!")
#End If
End Sub
Итак, отладка программы завершена, последняя найденная ошибка исправлена. Теперь программа должна быть передана пользователю. Значит ли это, что в ходе работы пользователя с программой не будут возникать ошибки? Обязательно, будут! Нужно предпринять специальные меры, чтобы появление этих ошибок не приводило к неприятным последствиям. Если этого не сделать, то при возникновении ошибки на экране появляется сообщение, как правило, мало что говорящее пользователю и программа завершает свою работу. Как правило, все это сопровождается нелестными высказываниями пользователя в адрес разработчика. Заметьте, ошибки этого периода (run time errors) могут быть и в правильно работающей программе. Они могут возникать из-за неверных действий самого пользователя, не знающего спецификаций. Обычно ошибки связаны с вводом данных несоответствующих типов или вводом значений, выходящих за пределы допустимого диапазона. Пользователь может пытаться открыть несуществующий файл, или файл, который он необдуманно удалил. При работе пользователя, например, в Access может быть сделана попытка открытия несуществующей таблицы или формы. В общем, у пользователя есть масса возможностей нарушить спецификации, особенно, если они не четко сформулированы. Но не стоит обольщаться, многие ошибки на совести программиста. Правильно считать, что во всех случаях виноват программист. В его задачу входит обнаружение и обработка всех исключительных ситуаций, возникающих в процессе работы программы. Сейчас мы и переходим к рассмотрению самого понятия исключительной ситуации и о тех средствах, которые есть в VBA для их обработки.
Исключительная ситуация или исключение (exception) возникает при выполнении программы и делает ее дальнейшее выполнение невозможным или нецелесообразным ввиду неопределенности, непредсказуемости или неправильности дальнейшего результата вычислений. Управление исключениями (exception handling) включает специально предусмотренное обнаружение исключений, перехват управления выполнением программы при возникновении исключения и передачу этого управления специальному разделу программы - обработчику исключения.
Определения, которые мы дали, носят общий характер и применимы к любому языку программирования. Следует сказать, что стандарт на исключения, их классификацию, способы обработки, еще не сформировался. Например, в языке Visual C++ обработка исключений значительно изощренней, чем в языке VBA. Следует сказать, что в VBA, к сожалению, не используется общепринятый термин исключение, вместо него используется термин ошибка. Суть дела от этого не меняется. Давайте начнем с рассмотрения общей схемы управления ошибками (исключениями) в языке VBA.
В языке VBA обработка ошибок сосредоточена на уровне процедуры (функции). В каждой процедуре может быть выделен один или несколько охраняемых блоков, с каждым из которых связывается свой обработчик ошибки. Если во время работы охраняемого блока возникла ошибка (исключение), то нормальный ход выполнения процедуры приостанавливается, управление ее работой перехватывается и передается обработчику ошибки. Стандартный объект Err содержит информацию об ошибке. Поэтому в обработчике ошибки имеется возможность обработать возникшую ситуацию, исправить ее, запросив, например, у пользователя дополнительные данные, и принять правильное решение о дальнейшем ходе выполнения программы. В некоторых случаях, когда устранена причина ошибки или ее последствия, управление может быть возвращено в
Давайте разберемся, что значит "возникла ошибка"? Точнее следует говорить возбуждена (raise) ошибка. Кто обнаруживает ошибку? Обнаружение исключительной ситуации и возбуждение ошибки может быть сделано самой операционной системой (VBA) или исполняемой процедурой. Ошибки, возбуждаемые операционной системой, могут быть следствием аппаратных прерываний, например, из-за деления на ноль, вычисления корня из отрицательного числа, но это могут быть и ошибки, программно обнаруживаемые операционной системой, например, при попытке открыть несуществующий файл. Все эти ошибки будем называть системными или внутренними ошибками VBA. Все они тщательно классифицированы и каждая из них однозначно идентифицируется своим номером. Другую группу ошибок составляют собственные или пользовательские ошибки, возбуждение которых предусматривает программист. Например, при работе с объектом пользовательского класса программист может и должен предусмотреть специальную процедуру Check, которая проверяет правильность
задания свойств объекта. Если обнаруживается, что свойства объекта заданы некорректно, так что выполнение операций над ним приведет к неверным результатам, то возбуждается собственная ошибка. Конечно, также как и для стандартных ошибок, ее тип должен быть полностью определен, задан ее номер и другие параметры. Возможно, программное обнаружение исключительных ситуаций и возбуждение собственных ошибок это наиболее важная и наиболее трудная часть программистской работы по управлению ошибками. Заметим, что какие бы ошибки не возбуждались, - внутренние или пользовательские, в момент возбуждения ошибки заполняются свойства объекта Err, так что он содержит всю информацию о последней возникшей ошибке.
Синтаксически On Error. В начале On Error задает метку обработчика ошибки охраняемого блока. Обработчик ошибок, как правило, завершается специальным оператором Resume, который задает точку в процедуре, которой передается управление после завершения обработки ошибки. Приведем схему процедуры с тремя
Sub ProcWithErrors() 'Первый охраняемый блок On Error GoTo ErrHadler1 ' подключение 1-го обработчика ошибок ' Первая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 1-го обработчика ошибок 'Второй охраняемый блок On Error GoTo ErrHadler2 ' подключение 2-го обработчика ошибок ' Вторая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 2-го обработчика ошибок 'Третий охраняемый блок On Error GoTo ErrHadler3 ' подключение 3-го обработчика ошибок ' Третья часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 3-го обработчика ошибок RepeatPoint: ' точка, с которой возобновляется выполнение 'после обработки ошибки в 3-ей части ... Exit Sub 'выход из процедуры при отсутствии ошибок 'ОбработкаОшибок: ErrHandler1: ' 1-ый обработчик ошибок ... Resume 'возврат к оператору, вызвавшему ошибку в 1-ой части ErrHandler2: ' 2-ой обработчик ошибок ... Resume Next 'переход к оператору, следующему за оператором 'вызвавшим ошибку во 2-ой части ErrHandler3: ' 3-ий обработчик ошибок ... Resume RepeatPoint 'переход к строке, с которой возобновляется 'выполнение после обработки ошибки в 3-ей части End Sub
Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой On Error, Resume и объекта Err с его свойствами и методами.
Имеется три варианта синтаксиса этого оператора:
On Error GoTo строка On Error Resume Next On Error GoTo 0
Рассмотрим подробно каждый из трех вариантов:
On Error GoTo строка используется, как заголовок охраняемого блока. Его обязательный аргумент строка является либо меткой строки или номером строки, задающей начало обработчика ошибки. Заметьте, обработчик ошибки - это фрагмент кода, расположенный в той же процедуре, что и On Error Resume Next также используется, как заголовок охраняемого блока. В этом случае с Resume Next, включенного непосредственно в оператор On Error. При возникновении ошибки в Err, и в случае ошибки принимает меры по ее устранению. Это довольно типичная ситуация, когда обработка возможной ошибки заранее предусмотрена и встроена в процедуру.On Error GoTo 0 является закрывающей скобкой, - он завершает On Error GoTo 0 приводит также к "чистке" свойств объекта Err аналогично методу Clear этого объекта. Синтаксис оператора трудно признать удачным, фраза GoTo 0 только сбивает с толку, поскольку 0 не рассматривается как номер строки, даже если строка с номером 0 существует. Неудачным решением является и то, что этот оператор можно опускать, если On Error GoTo 0.Заметьте, если ошибка возбуждена вне охраняемого блока, то она приведет к стандартному способу ее обработки, заключающемуся в том, что будет выдано сообщение об ошибке и выполнение будет приостановлено. Взгляните, как выглядит стандартное сообщение об ошибке:
(рис 10.14) Стандартное сообщение об ошибке периода выполненияДоступные кнопки в этом окне определяют возможные действия в этом случае. Все эти варианты, как правило, мало устраивают конечного пользователя. Вот еще один вариант такого сообщения:
(рис 10.15) Другой вид окна сообщения об ошибке периода выполнения
Оператор Resume должен быть последним
Resume [0] Resume Next Resume строка
В первом случае выполнение программы продолжается с повторного выполнения оператора, вызвавшего ошибку. Заметим, если это был оператор, инициировавший вызов процедур и функций, приведших в конечном итоге к ошибке, то управление передается этому оператору. Другими словами, управление всегда передается оператору охраняемого блока, явно или неявно инициировавшему ошибку. Вариант Resume (или Resume 0 ) естественно использовать, когда ошибка вызвана вводом неверных данных пользователем, а в обработчике ошибок у него запрашиваются новые правильные данные. Например, если в основной процедуре пользователь ввел имя несуществующего файла, в обработчике можно запросить новое имя и продолжить выполнение с повторения оператора открытия файла. Таким образом, этот вариант используется, если в обработчике ошибок устранена причина ошибки. В этом случае можно надеяться, что повторное исполнение оператора, ранее инициировавшего ошибку, теперь завершится благополучно.
В варианте Resume Next после обработки ошибки выполнение процедуры продолжается с оператора, следующего за оператором, инициировавшим ошибку. Не будем повторяться, и здесь речь идет об операторах охраняемого блока, а не тех вызванных процедур, где реально произошла ошибка. Если в первом варианте обработчик ошибки должен устранить причину ошибки, то в этом варианте обработчик устраняет последствия ошибки, выполняя, по существу, работу оператора, приведшего к ошибке. Еще одна возможная ситуация применения этого варианта, о которой мы уже упоминали, состоит в том, что следующий оператор сам анализирует причину ошибки и задает способы ее устранения.
В третьем варианте параметр строка задает метку строки или номер строки данной процедуры, на которую передается управление после обработки ошибки. Этот вариант используется в разных ситуациях, например, причина ошибки устранена, но необходимо повторить часть вычислений, предшествовавших появлению ошибки. Возможно, также, что устранены последствия ошибки, но действия надо продолжить не со следующего оператора, а с определенного участка процедуры. Третий вариант задает общую конструкцию, и мог бы использоваться во всех трех случаях.
Объект Err содержит информацию о последней ошибке выполнения. Объект Err является внутренним объектом с глобальной областью определения. Нет необходимости в создании этого объекта. Он создается вместе с проектом. Вот список свойств объекта Err и их значений:
| Свойство | Значение |
|---|---|
Number |
Номер (код) ошибки. Это свойство по умолчанию. |
Source |
Строковое выражение, представляющее источник, в котором возникла ошибка. При ошибках в стандартном модуле оно содержит имя проекта. При ошибках в модуле Source получает имя вида проект.класс. Конечно, хотелось бы, чтобы Source указывал источник возникновения ошибки более точно, хотя бы с точностью до имени процедуры, а лучше бы до оператора. Однако, этого пока не сделано. |
Description |
Строка с кратким описанием ошибки, если такая строка для кода, указанного в Number, существует. Для собственных ошибок значение этого свойства следует задавать. |
HelpFile |
Полное имя (включая диск и путь) файла справки VBA. Опять таки для собственных ошибок следует подготовить справочную систему и задавать путь к ней в этом свойстве. |
HelpContext |
Контекстный идентификатор файла справки, соответствующий ошибке с кодом, указанным в свойстве Number. |
LastDLLError |
Содержит системный код ошибки для последнего вызова DLL. Значение свойства LastDLLError доступно только для чтения. В лекции, посвященной работе с функциями Win32 API, подробно рассматривалось использование этого свойства. |
Рассмотрим пример, в котором возникает ошибка периода выполнения. Обработчик ошибки выдает сообщение о ней, используя свойства объекта Err. Затем в обработчике устраняется причина возникновения ошибки и управление возвращается оператору, инициировавшему запуск процедуры, приведшей к ошибке. Вся эта ситуация демонстрируется на примере работы с уже известной функцией fact2, вычисляющей корректно значение факториала для ограниченного диапазона значений входного параметра.
Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7 #If conDebug Then Debug.Assert (N >= 0) And (N < 8) #End If If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If #If conDebug Then Debug.Assert Fact2 <= 5040 #End If End Function
Заметьте, поскольку флаг отладки (conDebug) уже отключен, то Assert - утверждения не работают. Приведем процедуру, вызывающую функцию fact2 первый раз корректно, второй - нет, что приведет к ошибке, ее перехвату и исправлению ситуации:
Public Sub TestFact2() Dim Msg As String Dim VictoryCount As Integer, Prize As Long On Error GoTo ErrHandler1 VictoryCount = 5 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize VictoryCount = 10 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize Exit Sub ErrHandler1: Msg = "Ошибка # " Err.Number " возникла в " Err.Source _ vbCrLf " Описание: " Err.Description _ vbCrLf " HelpFile: " Err.HelpFile _ vbCrLf " HelpContext: " Err.HelpContext MsgBox Msg, vbMsgBoxHelpButton, "Error", Err.HelpFile, Err.HelpContext 'Грубое устранение причин ошибки Err.Clear If VictoryCount < 0 Then VictoryCount = 0 If VictoryCount > 7 Then VictoryCount = 7 Resume End Sub
Вот как выглядит окно сообщения, выведенное в обработчике ошибки.
(рис 10.16) Сообщение, сформированное в обработчике ошибкиЗаметьте, после выдачи сообщения процедура нормально завершает свою работу и в окне проверки Immediate появятся следующие результаты:
5 600 7 25200
Объект Err специально спроектирован для работы на этапе обнаружения и исправления ошибок периода выполнения. Он заменил ранее существовавшие функцию и оператор Err. Для совместимости с ними свойство Number реализовано, как свойство по умолчанию и его можно не указывать. Если в борьбе с ошибками на этапе отладки важную роль играет объект Debug, то не менее важна роль объекта Err при борьбе с ошибками периода выполнения. Также как и объект Debug, объект Err имеет всего два метода - Clear и Raise. Рассмотрим их подробнее.
Его синтаксис:
Err.Clear
Этот метод используется для явной очистки значений свойств объекта Err после завершения обработки ошибки. Автоматическая очистка свойств Err происходит также при выполнении операторов:
Resume любого вида;Exit Sub, Exit Function, Exit Property ;On Error любого вида.Метод Raise генерирует ошибку выполнения. У него двоякое назначение. Во-первых, он используется для моделирования стандартных, внутренних ошибок. Необходимость в этом возникает, например, при отладке обработчиков соответствующих ошибок. Но его главное назначение состоит в возбуждении собственных ошибок, когда в результате проделанного анализа обнаружена исключительная ситуация. Его синтаксис:
Err.Raise number, source, description, helpfile, helpcontext
Параметры метода имеют тот же смысл, что и соответствующие свойства объекта Err. Обязателен лишь параметр Number. При моделировании внутренних ошибок только этот параметр указывается при вызове метода. При возбуждении собственных ошибок разумно задавать все параметры, для того, чтобы были определены свойства объекта Err.
Параметр Number имеет тип Long и определяет код ошибки. Коды ошибок (как внутренних, так и определяемых пользователем) лежат в диапазоне 0-65535. При этом коды от 0 до 512 зарезервированы за системными ошибками VBA. При возбуждении собственных ошибок при задании параметра Number к ее собственному коду необходимо добавлять константу vbObjectError + 512, что позволяет системе отличать внутренние и пользовательские ошибки.
Перед вызовом метода Raise для возбуждения собственной ошибки полезно предварительно очистить объект Err методом Clear. Если Вы не зададите некоторые параметры в вызове Raise, в качестве их значений используются текущие значения соответствующих свойств объекта Err.
Мы уже говорили, что важная часть работы программиста по отладке программы состоит в том, чтобы предохранить работающую программу от прерывания ее работы при возникновении внутренних ошибок. Не менее важно, особенно при работе с объектами пользовательских классов, предусмотреть возможность появления исключительных ситуаций, генерировать и соответственно обрабатывать собственные ошибки класса. В качестве примера приведем класс Day, у которого есть два свойства - дата и температура на эту дату. Методы класса, (их реализацию мы не приводим) исходят из того, что летом должно быть жарко, а зимой - холодно. Нарушение этого условия приведет к неверным результатам. Поэтому в состав класса включен метод CheckDay, проверяющий корректность задания свойств. В случае несоблюдения требуемых условий метод генерирует собственные ошибки класса. Вот описание нашего класса:
Option Explicit 'Класс Day 'Свойства класса Private today As Date Private temperature As Integer Public Property Get Сегодня() As Date Сегодня = today End Property Public Property Let Сегодня(ByVal NewValue As Date) today = NewValue End Property Public Property Get Температура() As Integer Температура = temperature End Property Public Property Let Температура(ByVal NewValue As Integer) temperature = NewValue End Property Public Sub CheckDay() Dim Desc As String Dim Numb As Long Dim Source As String 'Проверка свойств объекта Select Case Month(Сегодня) Case 6 To 8 If Температура < 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает положительную летнюю температуру!" Numb = vbObjectError + 513 Source = " Метод CheckDay класса Day " Err.Raise Numb, Source, Desc End If Case 1 To 2, 12 If Температура > 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает отрицательную зимнюю температуру!" Numb = vbObjectError + 514 Source = " Метод CheckDay класса Day " Err.Raise Numb, Source, Desc End If End Select End Sub
Приведем теперь процедуру, работающую с объектами, этого класса:
Public Sub WorkWithDay() 'Работа с объектами класса Day Dim myday As New Day Dim Msg As String 'Охраняемый блок On Error GoTo ErrorHandler myday.Сегодня = "9.8.99" myday.Температура = -15 myday.CheckDay Debug.Print myday.Сегодня, myday.Температура Exit Sub ErrorHandler: If Err.Number = vbObjectError + 513 Then Msg = vbCrLf "Введите температуру сегодняшнего дня " _ myday.Сегодня vbCrLf " Учтите, она должна быть положительной" myday.Температура = InputBox(Err.Source vbCrLf Err.Description Msg, "CheckDay", 15) ElseIf Err.Number = vbObjectError + 514 Then Msg = vbCrLf "Введите температуру сегодняшнего дня " _ myday.Сегодня vbCrLf " Учтите, она должна быть отрицательной" myday.Температура = InputBox(Err.Source vbCrLf Err.Description Msg, "CheckDay", -15) End If Resume End Sub
Заметьте, эта процедура, работающая с объектом myday класса Day, построена по всем правилам, - в ней есть
(рис 10.17) Окно диалога с пользователемПосле того, как пользователь задал нормальную летнюю температуру, процедура нормально завершила свою работу. В окне проверки напечатаны следующие результаты:
09.08.99 25
К каждой процедуре может быть подключено несколько обработчиков ошибок. Этот факт был специально отражен в схеме процедуры, когда в этой лекции описывалась модель управления ошибками. При возникновении ошибки в процедуре только один из них может быть активным. Активным обработчиком является тот обработчик, который связан с
В процессе вычислений одни процедуры могут вызывать другие. Поэтому в момент возникновения ошибки в стеке вызовов процедур могут находиться несколько процедур: C1, C2, …Cn. Каждая из этих процедур может иметь активный обработчик ошибок. Какой же из них будет применяться для обработки ошибки? Рассмотрим применяемую стратегию обработки. Итак, пусть есть непустой стек вызовов C1, C2, …Cn, где C1 это самый внешний вызов, а Cn - самый внутренний вызов. Обработка начинается подъемом по стеку вызовов. Если в Cn имеется активный обработчик ошибки, то он и получает управление, если его нет, то в стеке проверяется следующий по порядку вызов. Если ни один из вызовов C1 - Cn не имеет активного обработчика, то выполняется стандартная обработка с выдачей сообщения об ошибке и снятия приложения. Пусть Ck - это первый, найденный в стеке вызов, для которого существует активный обработчик, и который, как было сказано, получает управление. Обработчик Ck имеет две возможности:
Во втором случае, подъем по стеку вызовов может быть продолжен, пока его не захватит обработчик, способный обработать эту ошибку, или управление не попадет последнему исполняемому стандартному обработчику. Заметим, что хорошим методическим приемом является введение некоторого
Давайте теперь разберемся с тем, какова должна быть типичная структура обработчика ошибок, чтобы он мог выполнять обе возложенные на него функции.
Обработчик, способный обработать ошибку, должен распознать ее, понять к какому классу относится ошибка - классу устранимых ошибок или классу критических ошибок, последствия которых устранить невозможно. В первом случае, обработчик устраняет причину или следствия ошибки и возвращает управление в
Как правило, каждый обработчик предназначен для обнаружения и исправления ошибок некоторого класса. Поэтому, когда он встречается с ошибкой другого класса, то он не способен ее обработать. В этом случае он передает эту функцию выше стоящему обработчику. Реализуется это тем, что обработчик вызывает метод Raise с тем же номером ошибки. При возбуждении ошибки в обработчике ошибки возобновится процесс подъема по стеку вызовов и управление сможет получить следующий активный обработчик ошибки, который в свою очередь, либо обработает ошибку, либо передаст ее вверх. Заметим, что возбуждение ошибки в обработчике ошибки может быть сознательным, но может быть и из-за того, что некорректно работает сам обработчик. В любом случае возбуждение ошибки приведет к подъему по стеку вызовов.
Типичный обработчик ошибок представляет собой оператор выбора Select, в котором каждый случай соответствует одной обрабатываемой ошибке, а для непредусмотренных ошибок происходит повторное их возбуждение и, тем самым, передача их вверх по стеку вызовов. Допустим, в K1, K2, …, Kn - обработчик ошибок этой процедуры может быть таким:
'ErrorHandler: Select Case Err.Number ' анализ кода ошибки. Case K1 … 'обработка ошибки с кодом K1 Case K2 … 'обработка ошибки с кодом K2 . . . Case Kn … 'обработка ошибки с кодом Kn Case Else 'Передача управления обработчику,выше стоящему в стеке вызовов Dim intErrNum As Integer intErrNum = Err.Number 'номер ошибки Err.Clear ' чистка объекта Err. Err.Raise Number:= intErrNum ' повторное возбуждение ошибки End Select
Метод Raise здесь используется для повторения исходной ошибки. Если произойдет ошибка, отличная от ошибок с кодами K1, K2, …, Kn, управление будет передано вверх по стеку вызовов другому активному обработчику, если таковой есть. Заметьте, перед вызовом метода Raise происходит чистка объекта Err.
Сделаем еще несколько замечаний об обработке ошибок в Office 2000:
AccessError.При работе с процедурами стандартного модуля есть еще один способ для возврата кодов ошибок, определенных пользователем. Для этой цели можно использовать функцию CVErr, возвращающую значение типа Variant с подтипом Error, которое содержит код ошибки, указанный пользователем. В вызывающей процедуре с помощью булевой функции IsError можно проверить, является ли возвращенное значение ошибкой. В следующем примере генерируется ошибка 1999, если аргумент функции Func1 является нечисловым.
Function Func1(Number As Variant) As Variant If IsNumeric(Number) Then ' Вычисление корректного результата. Func1 = Number * Number Else 'аргумент некорректен Func1 = CVErr(1999) ' возвращает код ошибки End If End Function
Проверять корректность работы Func1 можно так.
Sub Testfunc1() Dim res As Variant, arg As Variant arg = 12 res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg Else Debug.Print "Результат : ", res End If arg = "двенадцать" res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg Else Debug.Print "Результат : ", res End If End Sub
Приведем результаты вычислений:
Результат : 144 Ошибка #: Error 1999 аргумент : двенадцать
Результаты работы с системой документов должны удовлетворять заданным спецификациям. Эти результаты должны быть получены в заданное время. Точный прогноз погоды на завтра требуется сегодня, получить его послезавтра становится бессмысленным. Поэтому программисту постоянно приходится заботиться не только о корректности работы своих программ, но и об их эффективности. И здесь успех, во многом, определяется решениями, принимаемыми на самом верхнем уровне разработки проекта. Самое важное решение связано с выбором подходящей среды программирования. Здесь нужно точно учитывать перспективы развития разрабатываемой системы и выбираемой среды программирования, насколько средства, выбранные Вами сегодня, смогут удовлетворять Вашим завтрашним потребностям. Другой разрез, для многих задач не менее важный, состоит в правильном выборе структур данных и алгоритмов их обработки. Нет ничего лучше хорошего алгоритма. Он, как правило, определяет эффективность решения задач. Важную роль в повышении эффективности играет и переиспользование. Ранее созданные, тщательно проверенные и эффективные компоненты, могут определить и эффективность вновь решаемой задачи. Рассмотрение всех этих вопросов, главных источников эффективности работы системы в целом, выходит за рамки данной книги. Мы остановимся сейчас на деталях, каждая из которых в отдельности не способна существенно повысить эффективность решения. При добыче золота, можно пытаться найти самородок, а можно промывать руду, добывая песчинку за песчинкой. Так и при разработке программ, песчинки, повышающие эффективность, могут принести в результате ощутимый эффект.
Прежде, чем начать рассмотрение отдельных элементов, еще пару слов о измерениях, которые выполняет программист, для повышения эффективности своей программы. Известен эффект "bottle neck", - узких мест программы, которые могут быть причиной ее не эффективного поведения. Чтобы получить представление о том, есть ли в программе такие узкие места, требующие первостепенного вмешательства и переписывания, чаще всего требуется построение полного профиля программы. Причем и здесь важна представительная система тестов. Опять таки, мы не будем сейчас рассматривать средства построения профиля. Отметим только, что в ряде случаев можно для проведения измерений использовать функцию Timer.По ходу изложения мы несколько раз демонстрировали ее применение.
По ходу изложения мы не раз обращали внимание на средства, повышающие эффективность вычислений. Об этом шла речь, при рассмотрении модулей, при рассмотрении вопроса об объявлении переменных и выполнения операций над ними. Сейчас мы приведем еще несколько советов, некоторые из которых уже упоминались ранее.
Для уменьшения размеров требуемой памяти и ускорения выполнения операций над данными:
Variant. Работа с Variant требует больше памяти и дополнительных преобразований.Integer преобразуются к типу Long, так что разумнее задавать сразу тип Long вместо типа Integer.Single и Double следует использовать только в случае действительной необходимости. Иногда, можно воспользоваться типом Currency для выполнения подобных действий. В этом случае потребуется больше памяти, но не будет обращений к сопроцессору с плавающей точкой.Object и Variant весьма полезные типы, но применять их нужно только в случае действительной необходимости.В одном из примеров, я приводил таблицу временных измерений выполнения арифметических операций над данными разных типов. Рекомендую провести подобные замеры в более полном объеме.
Single и Double. Для уменьшения подобных преобразований, записывайте выражения так, чтобы пересечение переменных целого и плавающего типа было, по возможности, минимальным.Replace в большинстве случаев. В тех случаях, когда заменяется одна подстрока на другую такой же размерности, можно использовать функцию Mid, как в следующем примере:Public Sub TestCode() Dim Text As String Text = "Компилятор кода" Mid(Text, 1, 5) = "Транс" Debug.Print Text End Sub
vbCrLf, чем комбинацию символов Chr(13), Chr(10), задающих возврат каретки и перевод строки.If Asc(Text) = 202
работает быстрее, чем код
If Left(Text,1) = "K"
Злоупотреблять такими оптимизациями не стоит, поскольку первый фрагмент менее понятен, чем второй. Он нуждается, по крайней мере, в комментариях.
Когда требуется уменьшить размеры используемой памяти, нужно начинать с чистки массивов. Когда требуется уменьшить время выполнения программы, нужно начинать с чистки циклов. Особо тщательно нужно оптимизировать выполнение самых внутренних циклов. Каждая миллисекунда, сэкономленная на выполнении внутреннего цикла, может обернуться сэкономленными часами на выполнении всей программы. Вот несколько советов, полезных при работе с циклами:
For Each вместо обычного цикла For. В большинстве случаев это приводит к существенному выигрышу во времени исполнения цикла.Завершая эту лекцию, хочу еще раз сказать, что главная оптимизация достигается на уровне проектных решений, выбора структуры данных и алгоритмов. К ниже перечисленным советам следует относиться с определенной осторожностью. Оптимизация не должна идти во вред ясности написания программ. Не всегда в ней есть необходимость. Но если такая необходимость возникает, то, в первую очередь, нужно выявить узкие места в программе и их, в основном, и оптимизировать.
Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne10.
Занимаясь программированием уже многие годы, я не только теоретически, но и на собственном опыте осознал, что программы, которые мы, программисты, разрабатываем, относятся к средствам повышенной опасности. В простых ситуациях ошибки в программах, могут стать источником разочарований и огорчений отдельного человека. В серьезных ситуациях ошибки чреваты катастрофой. На людей, создающих программы, возлагается ответственность, если хотите, то и моральная ответственность за надежность и правильность работы их творений. И хотя программа программе рознь не нужно думать, что сегодня можно состряпать что либо на авось, а завтра, когда наступит время серьезной программы, тогда и придет черед надежному программированию. Нет, программировать надежно нужно всегда.
Можно ли создать надежную программную систему? Вспоминая опыт собственной работы, могу сказать, что в наиболее ответственных случаях, когда речь шла об экспериментах, связанных с космосом, решение заключалось в том, что программа создавалась независимо двумя коллективами, начиная от разработки алгоритма, кончая системой тестов. Только после того, как обе системы правильно работали на всех предъявленных обеими сторонами тестах, программа принималась в эксплуатацию. Это были шестидесятые годы. Сегодня мы живем в другом мире, с другими возможностями. Вот цитата из письма, которое я, как бета-тестер Office 2000 получил от команды, занимающейся отладкой этой системы:
"...Another added feature to our beta program will be the
privilege tonominate otherbeta testers . Over our beta program we receive over 500,000 requests to participate on the Office beta program. This upcoming beta we are going to allow you, our top beta tester to add your co worker, friend orneighbor to our program".
Вряд ли здесь необходим точный перевод. Речь идет о том, что одной из привилегий лучших бета - тестеров будет возможность рекомендовать тестеров для участия в новой программе тестирования. И делается это потому, что возникла проблема отбора тестеров. Заметьте, поступило 500000 заявок на участие в тестировании программного продукта. Следует заметить, что отношение к тестерам и их работе самое серьезное. Могу сказать, что ни один из посланных мной отчетов не остался без внимания. Конечно, при такой коллективной и независимой отладке можно в гораздо большей степени надеяться на надежность программ.
Поговорим сейчас о том, что должен делать каждый из программистов, работающих в среде Office 2000, чтобы создать надежный продукт и уменьшить число возможных ошибок, не надеясь на постороннюю помощь. Мы рассмотрим три темы:
Ошибки неизбежно сопровождают всякую сколь-нибудь сложную программу. Это утверждение может вызвать раздражение у начинающего программиста, но человек более опытный посчитает его само собой разумеющимся и сосредоточится на средствах, предлагаемых программным окружением для борьбы с этим неизбежным злом. Самые неприятные, дорогостоящие ошибки это те, что допущены при определении основных задач и целей приложения, при проектировании структуры его управления и потоков передачи данных, а также ошибки, связанные с неверной реализацией алгоритмов. Часто они не проявляются непосредственно в виде сбоев в работе программы, а обнаруживаются после довольно длительного использования приложения и требуют для своего исправления существенных изменений в проекте и программе. Их корни могут иметь как объективную природу (сложность решаемых задач), так и субъективную (непонимание заказчиком того, что ему надо). Как бороться с такими ошибками? Выделим два известных подхода к этой проблеме. Первый из них состоит в повышении уровня языков и систем программирования, чтобы разработчик мог оперировать при создании системы понятиями предметной области, для которой она создается. Другой подход связан с идеей быстрого прототипирования, т. е. создания на ранней стадии разработки системы ее работающего прототипа, в ходе экспериментов с которым заказчик может уточнить свои требования. Оба эти подхода нашли отражение в Office 2000. Первый реализован в объектно-ориентированном подходе к построению как самой инструментальной системы, так и создаваемых в ней приложений. Развитие в VBA возможностей пользовательских модулей классов, в частности введение полиморфизма, - еще один шаг в направлении повышения уровня языка. Благодаря классам, можно вводить в программы объекты, представляющие те или иные понятия предметных областей. Реализации второго из указанных подходов существенно способствует визуальный стиль программирования, принятый во всех приложениях Office 2000. Создавая собственные меню и диалоговые окна, можно быстро спроектировать интерфейс системы и передать пользователю (тестерам) действующий ее прототип для оценки соответствия его требованиям. Затем завершить реализацию с учетом замечаний и уточнений, возникших у тестеров при работе с прототипом. Это позволит избежать многих ошибок, неизбежных при разработке системы без контактов с ее будущими пользователями.
Одним из факторов, влияющих на надежность программ, является сам язык программирования. Известно, что язык, в котором есть объявление переменных по умолчанию, разрешены преобразования данных по умолчанию в процессе вычислений, нет строгого контроля типов, - такой язык является ненадежным, в нем значительно легче создать ненадежную программу, содержащую трудно выявляемую ошибку. Язык VBA трудно причислить к надежным языкам, он скорее занимает по шкале надежности срединное положение. Во многом, это связано с историей его возникновения. Именно поэтому надо предпринимать ряд мер, благоприятствующих повышению надежности. О многих из них мы уже говорили в разных частях этой книги. Напомним некоторые из них:
Variant и Object. В этом случае на Вашей стороне будет контроль типов, что позволит избежать многих возможных ошибок.ByRef и ByVal, помните об особенностях передачи аргументов по ссылке в VBA.
(рис 10.1) Флажки вкладки Editor
Прелесть работы программиста во многом связана с отладкой. Почему программисты нарушают известные им требования, - не задают комментарии, не описывают детально суть решаемой задачи и не следуют другим полезным советам. Чаще всего, причина в нетерпении, им хочется скорее посмотреть, как же работает программа, увидеть результаты ее работы. Отладка - это некоторый детективный процесс. Вновь созданную программу мы подозреваем в том, что она работает не корректно. Презумпция невиновности здесь не работает. Если удается предъявить тест, на котором программа дает неверный результат, то доказано, что наши подозрения верны. Втайне мы всегда надеемся, что программа заработает правильно с первого раза. Но цель тестирования другая, - попытаться опровергнуть это предположение. И только потом, исправив все выявленные ошибки, получить корректно работающую программу. К сожалению, отладка не может гарантировать, что программа корректна, даже если все тесты прошли успешно. Отладка может доказать некорректность программы, но она не может доказать ее правильности.
Искусство тестера состоит в том, чтобы создать по возможности полную систему тестов, проверяющую все возможные ветви вычислений. Поясним это на самом простом примере. Пусть программа находит сумму первых N элементов массива X, содержащего M элементов. Кроме "нормального" теста, проверяющего ситуацию, в которой 1<N<M, следует проверить и крайние случаи: N=1, N=M, N=0, N<0, N>M. Но это простой случай, а циклы обычно вложенные, и внутри них производится разбор случаев, внутри которых свои циклы.
Ранее мы упоминали закон "чечако" - новичок может подвесить любую систему. Этому есть объяснение, по незнанию, он задаст одно из мало вероятных сочетаний входных данных (работая в визуальной среде, нажмет самую неподходящую для данной ситуации кнопку). Поэтому тестер, ведущий отладку, должен уметь встать и на позицию новичка, система тестов должна гарантировать, что программа корректно работает не только в "нормальных ситуациях", но и имеет "защиту от дурака" и не приведет к зацикливанию или останову в крайних, мало вероятных ситуациях.
Сложность отладки заключается и в том, что, обнаружив и исправив ошибку, вы получаете новую программу, для которой процесс отладки нужно начинать заново, снова пропустив все тесты. Известно, что в программах встречаются заколдованные места, - исправление одной ошибки ведет к появлению новой. В таких случаях лучшим выходом бывает поиск другого, принципиально иного решения задачи.
Часть ошибок программы ловится автоматически еще на этапе компиляции. Сюда относятся все синтаксические ошибки, ошибки несоответствия типов и некоторые другие. Однако
Прежде, чем приступить к подробному рассмотрению этих средств, напомним, что в ходе отладки программа может находиться в одном из трех состояний: проектирования, вычисления и прерывания. Закончив проектирование, можно запустить программу на исполнение. Прервав исполнение программы в заданной точке, перейдя в состояние прерывания, можно проконтролировать значения переменных и свойств объектов в данной точке и, если требуется, изменить эти значения "вручную". При этом можно изменить порядок выполняемых операторов, задавать следующий исполняемый оператор, можно редактировать программный текст перед продолжением вычисления. Переход из состояния вычисления в состояние прерывания может происходить по самым разным причинам, например, по достижении точки прерывания, при выполнении одного из многочисленных условий прерывания, из-за пошагового выполнения программы. Все эти возможности мы еще обсудим, а сейчас рассмотрим один особый случай. Иногда программа "зацикливается" и необходимо принудительно перевести ее в с остояние прерывания. Как остановить работающую программу? Просто нажмите знакомую еще по работе в DOS пару клавиш Ctrl+Break. На экране появится следующее диалоговое окно с сообщением об остановке.
(рис 10.2) Сообщение о прерывании работы программыВ этом окне нажатие кнопки "End" остановит исполнение программы, нажатие кнопки "Continue" возобновит ее выполнение, программа снова перейдет в состояние вычисления. Если нажать кнопку "Debug", то выполнение программы прервется, и мы перейдем в режим прерывания -
Как и во многих других случаях, интерфейс отладчика VBA избыточен, - к одним и тем же инструментальным средствам можно добраться по-разному. В зависимости от привычек можно использовать панель Debug с инструментальными кнопками, можно использовать команды из меню Debug и View, можно использовать горячие клавиши. На следующем рисунке показана панель Debug (Отладка). Заметьте, на этой панели больше кнопок, чем в стандартном варианте, предлагаемом по умолчанию. Используя режим настройки (Customize) я вынес на эту панель дополнительные кнопки, задающие инструменты отладки.
(рис 10.3) Панель отладки с инструментальными кнопкамиКнопки этой панели соответствуют командам меню "Отладка" (Debug) и меню "Вид" (View), которые представлены на следующих рисунках:

(рис 10.5) Команды меню View(рис 10.4) Команды меню DebugНа рисунках для большинства команд меню показаны соответствующие им кнопки и комбинация горячих клавиш, выполняющих вызов тех же инструментальных средств. Дадим краткое описание этих инструментальных средств:
Первая из этих кнопок включает и выключает состояние проектирования. Вторая запускает проект на компиляцию. Запуск на компиляцию стоит делать чаще, не дожидаясь завершения проектирования. Так можно раньше выявить некоторые ошибки.
Первая из этих кнопок запускает программу на выполнение. Более точно это означает следующее. Если курсор стоит в любом месте некоторой процедуры, то на выполнение запускается эта процедура. Если активна некоторая форма, то запускается эта форма, в противном случае запускается макрос. Вторая кнопка этой группы прерывает выполнение и переводит программу в состояние прерывания. Третья кнопка приостанавливает выполнение и производит сброс проекта,- чистятся все стеки, так что происходит переход в состояние проектирования.
Эти две кнопки позволяют работать с точками прерывания. Точки прерывания играют важную роль в процессе отладки. Чаще всего, для понимания поведения программы достаточно проанализировать состояние вычислений в некоторых ее точках, например, после завершения очередного цикла. Точки прерывания ставятся в таких контрольных точках программы, после чего программа запускается на выполнение. Достигнув первой по ходу вычисления точки прерывания, выполнение приостанавливается, программа переходит в состояние прерывания. Состояние программы анализируется и затем можно произвести запуск вычислений до следующей точки прерывания. Таким образом, точки прерывания размечают программу, являясь промежуточными финишами процесса выполнения.
Первая из кнопок этой группы включает и выключает точку прерывания в строке, на которой установлен курсор. Вторая позволяет одним щелчком удалить все точки прерывания. Заметьте, точку прерывания можно установить и удалить без использования этих кнопок. Слева от окна кода есть специальное поле, выделенное серым цветом. В этом поле желтый стрелкой показан курсор, задающий текущий выполняемый оператор, в этом же поле черные кружочки отмечают точки прерывания. Достаточно щелкнуть левой кнопкой мыши в этом поле, чтобы соответствующая строка стала точкой прерывания, и, наоборот, если точка прерывания была установлена, то щелчок по ней удаляет ее.
Важная группа кнопок, включающая средства управления процессом вычислений. С их помощью на важных участках можно проследить выполнение программы с точностью до оператора. Первая из кнопок (Step Into - ей соответствует нажатие клавиши F8) задает пошаговый, пооператорный режим выполнения. При вызове процедур и функций в этом режиме начинается их
Очень полезные кнопки, по крайней мере, первая из них. С ее помощью можно изменять порядок вычислений, предписанный программой. Мы уже говорили, что в процессе отладки желтая стрелка в левом поле задает строку с текущим
Следующая группа кнопок задает инструментальные средства, позволяющие наблюдать за состоянием программы, значениями ее переменных. Этим кнопкам соответствуют команды меню View. Первые три кнопки вызывают соответственно три окна наблюдения за состоянием вычислений - окно локальных переменных (Locals), окно проверки с отладочными результатами вычислений (Immediate), окно контрольных выражений (Watch). Следующая пара кнопок также предназначена для работы с контрольными выражениями, открывая специальные окна для показа и добавления контрольных выражений. Наконец, последняя кнопка в этой группе очень полезна при работе в ситуациях, когда отлаживаются рекурсивные процедуры или процедуры, где встречается глубокая вложенность вызовов. Она позволяет показать стек вызовов процедур в текущий момент. Чуть позже мы подробнее рассмотрим работу с этими окнами.
При отладке весьма полезны и другие инструментальные средства, не связанные непосредственно с панелью Debug. Они остались вне нашего рассмотрения, но о некоторых из них все-таки скажем пару слов.
Эту пару кнопок инструментальной панели Edit мы уже упоминали. Первая из них позволяет закомментировать выделенный блок, вторая - отменить сделанные комментарии. Поскольку при отладке зачастую приходится опускать выполнение отдельных фрагментов, то эти кнопки относятся к часто используемым средствам.
Еще одна полезная кнопка этой панели Quick Info. Она позволяет получить подсказку о типе переменной, обо всех параметрах вызываемой функции и их типах. Для получения подсказки достаточно подвести курсор к соответствующей переменной или функции и щелкнуть кнопку. Тут же появится окно с подсказкой. Отметим также еще одно весьма полезное свойство, если просто подвести курсор к переменной, то тут же появляется окно подсказки, в котором показано значение этой переменной. Это эффективное средство наблюдения за состоянием переменных, не требующее специальных окон, наблюдение делается на лету.
Мы хотим теперь подробнее остановиться на окнах отладчика, позволяющих следить за состоянием вычислительного процесса. Чтобы сделать изложение конкретным, обратимся к примеру предыдущей лекции, в котором показана работа с бинарным деревом, задающим словарь. Единственное сделанное добавление связано с введением в модуле глобальной переменной GlobeVar. Приведем текст отлаживаемой процедуры:
Public Sub WorkwithBinTree() Dim MyDict As New BinTree Dim englword As String, rusword As String GlobeVar = "Привет!" 'Создание словаря MyDict.SearchAndInsert key:="dictionary", info:="словарь" MyDict.SearchAndInsert key:="hardware", info:="аппаратура, аппаратные средства" MyDict.SearchAndInsert key:="processor", info:="процессор" MyDict.SearchAndInsert key:="backup", info:="резервная копия" MyDict.SearchAndInsert key:="token", info:="лексема" MyDict.SearchAndInsert key:="file", info:="файл" MyDict.SearchAndInsert key:="compiler", info:="компилятор" MyDict.SearchAndInsert key:="account", info:="учетная запись" 'Обход словаря MyDict.PrefixOrder 'Поиск в словаре englword = "account": rusword = "" MyDict.SearchAndInsert key:=englword, info:=rusword Debug.Print englword, rusword 'Удаление из словаря MyDict.DelInTree englword englword = "hardware" MyDict.DelInTree englword 'Обход словаря MyDict.PrefixOrder 'Debug.Print MyDict End Sub
Наиболее простой и эффективный способ следить за состоянием вычислений заключается в том, чтобы во время отладки включить окно локальных переменных (Locals). Напоминаем, для этого есть соответствующая инструментальная кнопка панели Debug и команда меню View. В этом окне будут отображены все описанные в текущей процедуре переменные, их значения и типы. Обновление окна происходит автоматически при каждом переключении из режима выполнения в режим прерывания, а также при перемещении по стеку вызовов процедур. Первая переменная в этом окне - это специальная переменная с именем модуля, которая показывает все глобальные переменные, описанные на уровне текущего модуля. Для модуля класса первая переменная имеет имя Me и показывает свойства класса. Окно разбито на три столбца для имени переменной, ее значения и типа. Таким образом, окно позволяет следить за всеми локальными переменными текущей выполняемой процедуры и частью ее глобальных переменных, тех, которые описаны в текущем модуле. Отметим также,
что объекты и массивы могут быть свернуты и развернуты, используя обычную технику знаков "+" и "-".
Давайте начнем отладку нашего примера, поставим первую точку прерывания после предварительного формирования словаря на операторе, запускающем
(рис 10.6) Окно локальных переменных процедуры стандартного модуляЗаметьте, в окне Locals показана глобальная переменная модуля Examples и все локальные переменные исполняемой процедуры WorkWithBinTree. Объект MyDict класса BinTree на рисунке частично раскрыт. Как видите, окно позволяет тщательно проанализировать достаточно сложную структуру этого объекта. В окне Locals можно не только наблюдать за значениями переменных, но и изменять эти значения, редактируя поле Value.
Давайте нажмем клавишу F8 для выполнения следующего PrefixOrder класса BinTree, вызванный объектом MyDict. Выполним в пошаговом режиме несколько операторов и остановимся после первой отладочной печати. Заметьте, что поскольку теперь исполнение идет в другом контексте, - исполняется модуль класса, то окно Locals полностью обновится и будет теперь задавать состояние исполняемого метода класса. Взгляните, как это выглядит на рисунке:
(рис 10.7) Окно локальных переменных метода классаКроме показа переменных окно Locals позволяет проанализировать текущее состояние стека вызовов. Для этого в правом верхнем углу окна есть специальная кнопка, щелчок которой дает тот же эффект, что соответствующая инструментальная кнопка меню Debug. Продемонстрируем ее использование чуть позже, а сейчас обращаем внимание, что результаты исполнения оператора Debug.Print появились в окне Immediate, к рассмотрению которого мы и переходим.
В этом окне появляется вся отладочная информация, поступающая в результате выполнения методов ?", после которого печатается запрашиваемое выражение. После нажатия Enter в окне появляется значение выражения. В предыдущих лекциях мы неоднократно пользовались запросами для вычисления значений различных функций. Окно позволяет изменять и значения глобальных переменных модуля. Взгляните, как выглядит окно проверки, в котором выполняются некоторые вычисления в состоянии прерывания процедуры. В частности, в этом окне дважды присваивалось новое значение глобальной переменной и выдавался запрос на получение ее значения. Здесь же был вызван на исполнение метод PostfixOrder объекта root. В этом же окне появились результаты его работы, напечатанные оператором Debug.
(рис 10.8) Вычисления в окне проверки
Привычное назначение окна Watch в той или иной среде программирования состоит в том, что оно является окном наблюдений и позволяет следить за состоянием некоторых переменных и выражений программы. Теперь, во многом, эта функция перешла к окну Locals. Тем не менее, и окно Watch используется для этой же цели. В отличие от окна Locals, выражения, появляющиеся в этом окне, не связаны с контекстом выполняемой процедуры. Программист сам формирует список наблюдаемых переменных и выражений, при этом всегда можно точно указать область определения наблюдаемой переменной.
Наблюдение за состоянием выражений перестало быть главной функцией окна Watch. Выражения, хранящиеся в этом окне, являются контрольными выражениями (может быть, точнее было бы - контролируемыми выражениями). Контрольными могут быть любые арифметические, логические или строковые выражения. Для заданных контрольных выражений отладчик позволяет следить за:
True. Это вариация предыдущего случая и можно указать много ситуаций, когда обнаружение такого факта, приостанавливающего вычисления весьма полезно при отладке.Следует только понимать, что злоупотреблять контрольными выражениями не следует, поскольку они замедляют процесс вычислений, поскольку перевычисляются на каждом шаге. Кроме того, по этой же причине следует аккуратно задавать контекст, в котором следует вычислять эти выражения.
Помимо окна Watch, в котором появляются контрольные выражения, есть еще окно для добавления нового контрольного выражения, напомним, это окно может быть вызвано соответствующей инструментальной кнопкой панели Debug или, чаще всего, командой Add Watch из контекстного меню, появляющегося при нажатии правой кнопки в окне кода. Вот как выглядит это окно:
(рис 10.9) Окно добавления контрольного выраженияВ поле "Expression" вводится новое контрольное выражение. В нашем примере - это переменная info, в общем случае это может быть любое выражение. В этом окне задается контекст, в котором выражение будет регулярно вычисляться по завершении каждого оператора. Контекст определяет процедуру, модуль и проект. В нашем примере мы указали все процедуры модуля BinTree текущего проекта. Как уже говорилось, чем шире контекст, тем больше проводится дополнительных вычислений по ходу отладки. Кроме контекста задается один из трех возможных типов контрольного выражения. Выбранный нами тип "Break When Value Changes" позволяет прервать вычисления всякий раз, когда в заданном контексте изменится значение контрольной переменной rusword. Добавленное контрольное выражение появляется в окне "Контрольные значения" (Watch).
Просмотреть значение и добавить новое контрольное выражение можно в окне быстрого просмотра Quick Watch. Для этого достаточно выделить выражение и нажать упоминавшуюся ранее инструментальную кнопку панели Debug, выполнить команду меню или нажать комбинацию горячих клавиш - Shift +F9. Вот как выглядит это окно
(рис 10.10) Окно быстрого просмотра контрольного выраженияЩелкнув по кнопке Add в этом окне, мы добавили переменную MyDict в окно Watch. Сравнивая окно Watch с окном Locals, отметим, что к трем колонкам Expression, Value и Type добавился еще один столбец Context, в котором задается контекст контрольного выражения. Взгляните, как выглядят эти окна после того, как мы добавили три контрольных выражения:
(рис 10.11) Окна отладчика после добавления контрольных выраженийЕще один вид информации о текущем состоянии вычисления представлен стеком вызовов процедур. Имя каждой вызванной процедуры помещается на вершину этого стека в момент ее вызова и убирается из него сразу после завершения этого вызова. Просмотреть стек вызовов можно, нажав клавиши Ctrl+L или выбрав инструментальную кнопку или команду "
Продолжим работу с нашим примером. Мы остановились в момент выполнения оператора, запускающего обход дерева. В ходе отладки мы ввели некоторые контрольные выражения. Продолжим теперь выполнение нашей программы. Очередное прерывание в ее работе наступило вследствие того, что изменилось значение контрольного выражения, переменная info получила новое значение в процессе работы процедуры поиска SearchAndInsert. Взгляните на соответствующее состояние окон отладчика в этот момент:
(рис 10.12) Окна отладчика в момент прерыванияЗаметьте, полученной информации достаточно, чтобы понять в какой процедуре произошло изменение значения переменной info. Но эта процедура рекурсивна и вызывалась неоднократно. Поэтому хотелось бы знать, на каком шаге ее вызова произошло изменение контрольной переменной. Для этого и служит стек вызовов, окно которого и было открыто щелчком соответствующей кнопки в окне Locals.
(рис 10.13) Окно стека вызововТри вызова функции SearchAndInsert были выполнены, прежде чем изменилось значение контрольной переменной. К сожалению, в окне стека вызовов не указаны параметры соответствующих вызовов. Но получить информацию об их значениях и о состоянии локальных переменных вызовов можно. Для этого нужно выделить интересующий вызов в стеке и щелкнуть по кнопке "Show". В результате в окне Locals ,будут отображены значения локальных переменных в момент выделенного вызова.
Разговор о средствах отладки будет не полным, если не рассмотреть специальный объект Debug.
Объект Debug специально сконструирован для проведения отладки. Он имеет два метода Print и Assert.
Его синтаксис:
Debug.Print список_выражений
Метод позволяет во время выполнения программы напечатать значения выражений из списка выражений в окне проверки Immediate. Трудно представить программу в период отладки, в которой бы многократно не использовался бы метод Print объекта Debug. Во всех наших примерах демонстрировалось его применение. Мы не будем останавливаться на некоторых деталях синтаксиса и управления выводом, при желании познакомиться с ними можно обратиться к справке.
Его синтаксис:
Debug.Assert булево_выражение
Всякий раз, когда при выполнении программы управление получает метод Assert, вычисляется значение булевого выражения и, если оно истинно, выполнение программы продолжается обычным образом. Если же значения выражения ложно, то происходит останов выполнения и программа переходит в состояние прерывания и, как обычно, начинается отладка и выяснение причин, повлекших ложность высказывания, заданного Assert выражением.
Заметьте, Assert - выражения подобны одному из типов контрольных выражений, которые также приводят к прерыванию, когда приобретают значение False. Синтаксис и выполнение отдельного оператора Debug.Assert, совершенно понятны. Однако есть смысл поговорить о том, какую роль играют Assert - утверждения в процессе отладки. Идея такова: программа разбивается на участки, каждый из которых начинается и завершается Assert - утверждением. Предполагается, что программист может задать в виде булевого выражения утверждение, которое должно быть истинно в этой точке программы. В типичном случае Assert - утверждения вставляются в начало и конец каждой процедуры и функции. В начале процедуры эти утверждения задают условия, которым должны удовлетворять исходные данные. В конце работы процедуры булевы выражения описывает требования, предъявляемые к результатам. Если начальные и конечные утверждения истинны, то полагается, что процедура была корректно вызвана и корректно завершила свою работу. Конечно же, чтобы использовать эту технику в полной мере следует иметь опыт доказательства
Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7 Debug.Assert (N >= 0) And (N < 8) If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If Debug.Assert Fact2 <= 5040 End Function
Заметьте, говорить о том, правильно или неправильно работает функция Fact2, вычисляющая факториал, не имеет особого смысла, если не упоминать интервал возможных значений ее входного параметра. В узком интервале значений, для которого она и была спроектирована, она работает корректно. Вне этого интервала использовать ее не следует. Assert - утверждение в начале этой функции приостановит выполнение, если будет сделана попытка использовать эту функцию вне тех пределов, для которых она гарантирует корректную работу. Если бы этих утверждений не было, то попытка вызвать эту функцию с отрицательным N или N, большим 7, привела бы к останову выполнения программы.
Мы уже говорили, что отладка, основанная на построении системы тестов, не может доказать правильность программы. Поэтому в P(X,Y) - программа, с заданными входными данными X и результатами Y. Предикат Q(X), определенный на входных данных, будем называть предусловием программы P, а предикат R(X,Y), связывающий входные и выходные переменные будем называть постусловием программы P. Будем также предполагать, что в ходе своей работы программа не меняет своих входных переменных X.
Программа P(X,Y) корректна по отношению к предусловию Q(X) и постусловию R(X,Y), если из истинности Q(X) до начала выполнения программы следует, что, будучи запущенной, программа завершит свою работу и по ее завершению будет истинным предикат R(X,Y). Условие корректности записывают в виде триады (триады Хоора) - Q(X) {P(X,Y)} R(X,Y)
Уже из этого определения становится ясно, что говорить о правильности следует не вообще, а по отношению к заданным спецификациям, например, в виде предусловия и постусловия. Доказать правильность триады для сложных программ, как уже говорилось, довольно сложно. Один из методов (метод Флойда) состоит в том, что программа разбивается на участки, размеченные предикатами - Q1, Q2, …QN, R. Первый из предикатов представляет предусловие программы, последний - постусловие. Тогда
Q1{P1}Q2; Q2{P2}Q3; …QN{PN}R
Нетрудно видеть, что введение Assert - утверждений является отражением
Заметьте, что вызов методов объекта Debug может встречаться только в период отладки программы. В тот момент, когда программа передается конечному пользователю, вызов методов этого объекта не должен встречаться в работающей программе. Это понятно, поскольку в этом режиме никакие отладочные окна не появляются, нет никаких окон проверки и методу Print некуда направлять свой вывод, носящий отладочный характер. И уж тем более не должно прерываться выполнение программы, если вдруг Assert - утверждение станет ложным. Конечный пользователь просто не будет знать, что нужно делать в этом случае.
Когда отладка завершена, вызовы методов Print и Assert не должны встречаться в выполняемой программе. Конечно, можно просто удалить эти вызовы из текста программы или закомментировать их. Однако жизненный цикл многих программ таков, что снова и снова приходится возвращаться к отладке ранее разработанной программы, внесению заплаток, созданию новой версии, введению новых возможностей. Поэтому удаление или комментирование вызовов объекта Debug не является лучшим способом. Гораздо удобнее иметь возможность включать или выключать отладочный режим при необходимости. Для этой цели и используются средства Debug. Идея состоит в том, что все вызовы методов этого объекта заключаются в обертку, заданную специальным оператором #If
Эта директива позволяет задать
#Const constname = expression
Имя константы строится по обычным правилам, а выражение в правой части может содержать только литералы (символьные константы) и другие Is. Эти константы, являются флажками и используются в операторе #If
Оператор If If имеет синтаксис, похожий на обычный оператор If:
#If expression Then statements [#ElseIf expression-n Then [elseifstatements]] [#Else [elsestatements]] #End If
Особенность состоит в том, что выражения состоят из констант периода компиляции и литералов, объединенных знаками операций. Все выражения вычисляются, поэтому все константы периода компиляции должны быть определены. Если выражение ложно, то соответствующие операторы не компилируются и не увеличивают тем самым размер программы. Основное назначение директивы
Приведем простой пример использования средств
'Константа conDebug включает или выключает отладочную печать
#Const conDebug = True
'#Const conDebug = False
Public Sub TestDebug()
#If conDebug Then
Debug.Print "Привет!"
#Else
MsgBox ("Привет!")
#End If
End Sub
Итак, отладка программы завершена, последняя найденная ошибка исправлена. Теперь программа должна быть передана пользователю. Значит ли это, что в ходе работы пользователя с программой не будут возникать ошибки? Обязательно, будут! Нужно предпринять специальные меры, чтобы появление этих ошибок не приводило к неприятным последствиям. Если этого не сделать, то при возникновении ошибки на экране появляется сообщение, как правило, мало что говорящее пользователю и программа завершает свою работу. Как правило, все это сопровождается нелестными высказываниями пользователя в адрес разработчика. Заметьте, ошибки этого периода (run time errors) могут быть и в правильно работающей программе. Они могут возникать из-за неверных действий самого пользователя, не знающего спецификаций. Обычно ошибки связаны с вводом данных несоответствующих типов или вводом значений, выходящих за пределы допустимого диапазона. Пользователь может пытаться открыть несуществующий файл, или файл, который он необдуманно удалил. При работе пользователя, например, в Access может быть сделана попытка открытия несуществующей таблицы или формы. В общем, у пользователя есть масса возможностей нарушить спецификации, особенно, если они не четко сформулированы. Но не стоит обольщаться, многие ошибки на совести программиста. Правильно считать, что во всех случаях виноват программист. В его задачу входит обнаружение и обработка всех исключительных ситуаций, возникающих в процессе работы программы. Сейчас мы и переходим к рассмотрению самого понятия исключительной ситуации и о тех средствах, которые есть в VBA для их обработки.
Исключительная ситуация или исключение (exception) возникает при выполнении программы и делает ее дальнейшее выполнение невозможным или нецелесообразным ввиду неопределенности, непредсказуемости или неправильности дальнейшего результата вычислений. Управление исключениями (exception handling) включает специально предусмотренное обнаружение исключений, перехват управления выполнением программы при возникновении исключения и передачу этого управления специальному разделу программы - обработчику исключения.
Определения, которые мы дали, носят общий характер и применимы к любому языку программирования. Следует сказать, что стандарт на исключения, их классификацию, способы обработки, еще не сформировался. Например, в языке Visual C++ обработка исключений значительно изощренней, чем в языке VBA. Следует сказать, что в VBA, к сожалению, не используется общепринятый термин исключение, вместо него используется термин ошибка. Суть дела от этого не меняется. Давайте начнем с рассмотрения общей схемы управления ошибками (исключениями) в языке VBA.
В языке VBA обработка ошибок сосредоточена на уровне процедуры (функции). В каждой процедуре может быть выделен один или несколько охраняемых блоков, с каждым из которых связывается свой обработчик ошибки. Если во время работы охраняемого блока возникла ошибка (исключение), то нормальный ход выполнения процедуры приостанавливается, управление ее работой перехватывается и передается обработчику ошибки. Стандартный объект Err содержит информацию об ошибке. Поэтому в обработчике ошибки имеется возможность обработать возникшую ситуацию, исправить ее, запросив, например, у пользователя дополнительные данные, и принять правильное решение о дальнейшем ходе выполнения программы. В некоторых случаях, когда устранена причина ошибки или ее последствия, управление может быть возвращено в
Давайте разберемся, что значит "возникла ошибка"? Точнее следует говорить возбуждена (raise) ошибка. Кто обнаруживает ошибку? Обнаружение исключительной ситуации и возбуждение ошибки может быть сделано самой операционной системой (VBA) или исполняемой процедурой. Ошибки, возбуждаемые операционной системой, могут быть следствием аппаратных прерываний, например, из-за деления на ноль, вычисления корня из отрицательного числа, но это могут быть и ошибки, программно обнаруживаемые операционной системой, например, при попытке открыть несуществующий файл. Все эти ошибки будем называть системными или внутренними ошибками VBA. Все они тщательно классифицированы и каждая из них однозначно идентифицируется своим номером. Другую группу ошибок составляют собственные или пользовательские ошибки, возбуждение которых предусматривает программист. Например, при работе с объектом пользовательского класса программист может и должен предусмотреть специальную процедуру Check, которая проверяет правильность
задания свойств объекта. Если обнаруживается, что свойства объекта заданы некорректно, так что выполнение операций над ним приведет к неверным результатам, то возбуждается собственная ошибка. Конечно, также как и для стандартных ошибок, ее тип должен быть полностью определен, задан ее номер и другие параметры. Возможно, программное обнаружение исключительных ситуаций и возбуждение собственных ошибок это наиболее важная и наиболее трудная часть программистской работы по управлению ошибками. Заметим, что какие бы ошибки не возбуждались, - внутренние или пользовательские, в момент возбуждения ошибки заполняются свойства объекта Err, так что он содержит всю информацию о последней возникшей ошибке.
Синтаксически On Error. В начале On Error задает метку обработчика ошибки охраняемого блока. Обработчик ошибок, как правило, завершается специальным оператором Resume, который задает точку в процедуре, которой передается управление после завершения обработки ошибки. Приведем схему процедуры с тремя
Sub ProcWithErrors() 'Первый охраняемый блок On Error GoTo ErrHadler1 ' подключение 1-го обработчика ошибок ' Первая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 1-го обработчика ошибок 'Второй охраняемый блок On Error GoTo ErrHadler2 ' подключение 2-го обработчика ошибок ' Вторая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 2-го обработчика ошибок 'Третий охраняемый блок On Error GoTo ErrHadler3 ' подключение 3-го обработчика ошибок ' Третья часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 3-го обработчика ошибок RepeatPoint: ' точка, с которой возобновляется выполнение 'после обработки ошибки в 3-ей части ... Exit Sub 'выход из процедуры при отсутствии ошибок 'ОбработкаОшибок: ErrHandler1: ' 1-ый обработчик ошибок ... Resume 'возврат к оператору, вызвавшему ошибку в 1-ой части ErrHandler2: ' 2-ой обработчик ошибок ... Resume Next 'переход к оператору, следующему за оператором 'вызвавшим ошибку во 2-ой части ErrHandler3: ' 3-ий обработчик ошибок ... Resume RepeatPoint 'переход к строке, с которой возобновляется 'выполнение после обработки ошибки в 3-ей части End Sub
Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой On Error, Resume и объекта Err с его свойствами и методами.
Имеется три варианта синтаксиса этого оператора:
On Error GoTo строка On Error Resume Next On Error GoTo 0
Рассмотрим подробно каждый из трех вариантов:
On Error GoTo строка используется, как заголовок охраняемого блока. Его обязательный аргумент строка является либо меткой строки или номером строки, задающей начало обработчика ошибки. Заметьте, обработчик ошибки - это фрагмент кода, расположенный в той же процедуре, что и On Error Resume Next также используется, как заголовок охраняемого блока. В этом случае с Resume Next, включенного непосредственно в оператор On Error. При возникновении ошибки в Err, и в случае ошибки принимает меры по ее устранению. Это довольно типичная ситуация, когда обработка возможной ошибки заранее предусмотрена и встроена в процедуру.On Error GoTo 0 является закрывающей скобкой, - он завершает On Error GoTo 0 приводит также к "чистке" свойств объекта Err аналогично методу Clear этого объекта. Синтаксис оператора трудно признать удачным, фраза GoTo 0 только сбивает с толку, поскольку 0 не рассматривается как номер строки, даже если строка с номером 0 существует. Неудачным решением является и то, что этот оператор можно опускать, если On Error GoTo 0.Заметьте, если ошибка возбуждена вне охраняемого блока, то она приведет к стандартному способу ее обработки, заключающемуся в том, что будет выдано сообщение об ошибке и выполнение будет приостановлено. Взгляните, как выглядит стандартное сообщение об ошибке:
(рис 10.14) Стандартное сообщение об ошибке периода выполненияДоступные кнопки в этом окне определяют возможные действия в этом случае. Все эти варианты, как правило, мало устраивают конечного пользователя. Вот еще один вариант такого сообщения:
(рис 10.15) Другой вид окна сообщения об ошибке периода выполнения
Оператор Resume должен быть последним
Resume [0] Resume Next Resume строка
В первом случае выполнение программы продолжается с повторного выполнения оператора, вызвавшего ошибку. Заметим, если это был оператор, инициировавший вызов процедур и функций, приведших в конечном итоге к ошибке, то управление передается этому оператору. Другими словами, управление всегда передается оператору охраняемого блока, явно или неявно инициировавшему ошибку. Вариант Resume (или Resume 0 ) естественно использовать, когда ошибка вызвана вводом неверных данных пользователем, а в обработчике ошибок у него запрашиваются новые правильные данные. Например, если в основной процедуре пользователь ввел имя несуществующего файла, в обработчике можно запросить новое имя и продолжить выполнение с повторения оператора открытия файла. Таким образом, этот вариант используется, если в обработчике ошибок устранена причина ошибки. В этом случае можно надеяться, что повторное исполнение оператора, ранее инициировавшего ошибку, теперь завершится благополучно.
В варианте Resume Next после обработки ошибки выполнение процедуры продолжается с оператора, следующего за оператором, инициировавшим ошибку. Не будем повторяться, и здесь речь идет об операторах охраняемого блока, а не тех вызванных процедур, где реально произошла ошибка. Если в первом варианте обработчик ошибки должен устранить причину ошибки, то в этом варианте обработчик устраняет последствия ошибки, выполняя, по существу, работу оператора, приведшего к ошибке. Еще одна возможная ситуация применения этого варианта, о которой мы уже упоминали, состоит в том, что следующий оператор сам анализирует причину ошибки и задает способы ее устранения.
В третьем варианте параметр строка задает метку строки или номер строки данной процедуры, на которую передается управление после обработки ошибки. Этот вариант используется в разных ситуациях, например, причина ошибки устранена, но необходимо повторить часть вычислений, предшествовавших появлению ошибки. Возможно, также, что устранены последствия ошибки, но действия надо продолжить не со следующего оператора, а с определенного участка процедуры. Третий вариант задает общую конструкцию, и мог бы использоваться во всех трех случаях.
Объект Err содержит информацию о последней ошибке выполнения. Объект Err является внутренним объектом с глобальной областью определения. Нет необходимости в создании этого объекта. Он создается вместе с проектом. Вот список свойств объекта Err и их значений:
| Свойство | Значение |
|---|---|
Number |
Номер (код) ошибки. Это свойство по умолчанию. |
Source |
Строковое выражение, представляющее источник, в котором возникла ошибка. При ошибках в стандартном модуле оно содержит имя проекта. При ошибках в модуле Source получает имя вида проект.класс. Конечно, хотелось бы, чтобы Source указывал источник возникновения ошибки более точно, хотя бы с точностью до имени процедуры, а лучше бы до оператора. Однако, этого пока не сделано. |
Description |
Строка с кратким описанием ошибки, если такая строка для кода, указанного в Number, существует. Для собственных ошибок значение этого свойства следует задавать. |
HelpFile |
Полное имя (включая диск и путь) файла справки VBA. Опять таки для собственных ошибок следует подготовить справочную систему и задавать путь к ней в этом свойстве. |
HelpContext |
Контекстный идентификатор файла справки, соответствующий ошибке с кодом, указанным в свойстве Number. |
LastDLLError |
Содержит системный код ошибки для последнего вызова DLL. Значение свойства LastDLLError доступно только для чтения. В лекции, посвященной работе с функциями Win32 API, подробно рассматривалось использование этого свойства. |
Рассмотрим пример, в котором возникает ошибка периода выполнения. Обработчик ошибки выдает сообщение о ней, используя свойства объекта Err. Затем в обработчике устраняется причина возникновения ошибки и управление возвращается оператору, инициировавшему запуск процедуры, приведшей к ошибке. Вся эта ситуация демонстрируется на примере работы с уже известной функцией fact2, вычисляющей корректно значение факториала для ограниченного диапазона значений входного параметра.
Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7 #If conDebug Then Debug.Assert (N >= 0) And (N < 8) #End If If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If #If conDebug Then Debug.Assert Fact2 <= 5040 #End If End Function
Заметьте, поскольку флаг отладки (conDebug) уже отключен, то Assert - утверждения не работают. Приведем процедуру, вызывающую функцию fact2 первый раз корректно, второй - нет, что приведет к ошибке, ее перехвату и исправлению ситуации:
Public Sub TestFact2() Dim Msg As String Dim VictoryCount As Integer, Prize As Long On Error GoTo ErrHandler1 VictoryCount = 5 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize VictoryCount = 10 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize Exit Sub ErrHandler1: Msg = "Ошибка # " Err.Number " возникла в " Err.Source _ vbCrLf " Описание: " Err.Description _ vbCrLf " HelpFile: " Err.HelpFile _ vbCrLf " HelpContext: " Err.HelpContext MsgBox Msg, vbMsgBoxHelpButton, "Error", Err.HelpFile, Err.HelpContext 'Грубое устранение причин ошибки Err.Clear If VictoryCount < 0 Then VictoryCount = 0 If VictoryCount > 7 Then VictoryCount = 7 Resume End Sub
Вот как выглядит окно сообщения, выведенное в обработчике ошибки.
(рис 10.16) Сообщение, сформированное в обработчике ошибкиЗаметьте, после выдачи сообщения процедура нормально завершает свою работу и в окне проверки Immediate появятся следующие результаты:
5 600 7 25200
Объект Err специально спроектирован для работы на этапе обнаружения и исправления ошибок периода выполнения. Он заменил ранее существовавшие функцию и оператор Err. Для совместимости с ними свойство Number реализовано, как свойство по умолчанию и его можно не указывать. Если в борьбе с ошибками на этапе отладки важную роль играет объект Debug, то не менее важна роль объекта Err при борьбе с ошибками периода выполнения. Также как и объект Debug, объект Err имеет всего два метода - Clear и Raise. Рассмотрим их подробнее.
Его синтаксис:
Err.Clear
Этот метод используется для явной очистки значений свойств объекта Err после завершения обработки ошибки. Автоматическая очистка свойств Err происходит также при выполнении операторов:
Resume любого вида;Exit Sub, Exit Function, Exit Property ;On Error любого вида.Метод Raise генерирует ошибку выполнения. У него двоякое назначение. Во-первых, он используется для моделирования стандартных, внутренних ошибок. Необходимость в этом возникает, например, при отладке обработчиков соответствующих ошибок. Но его главное назначение состоит в возбуждении собственных ошибок, когда в результате проделанного анализа обнаружена исключительная ситуация. Его синтаксис:
Err.Raise number, source, description, helpfile, helpcontext
Параметры метода имеют тот же смысл, что и соответствующие свойства объекта Err. Обязателен лишь параметр Number. При моделировании внутренних ошибок только этот параметр указывается при вызове метода. При возбуждении собственных ошибок разумно задавать все параметры, для того, чтобы были определены свойства объекта Err.
Параметр Number имеет тип Long и определяет код ошибки. Коды ошибок (как внутренних, так и определяемых пользователем) лежат в диапазоне 0-65535. При этом коды от 0 до 512 зарезервированы за системными ошибками VBA. При возбуждении собственных ошибок при задании параметра Number к ее собственному коду необходимо добавлять константу vbObjectError + 512, что позволяет системе отличать внутренние и пользовательские ошибки.
Перед вызовом метода Raise для возбуждения собственной ошибки полезно предварительно очистить объект Err методом Clear. Если Вы не зададите некоторые параметры в вызове Raise, в качестве их значений используются текущие значения соответствующих свойств объекта Err.
Мы уже говорили, что важная часть работы программиста по отладке программы состоит в том, чтобы предохранить работающую программу от прерывания ее работы при возникновении внутренних ошибок. Не менее важно, особенно при работе с объектами пользовательских классов, предусмотреть возможность появления исключительных ситуаций, генерировать и соответственно обрабатывать собственные ошибки класса. В качестве примера приведем класс Day, у которого есть два свойства - дата и температура на эту дату. Методы класса, (их реализацию мы не приводим) исходят из того, что летом должно быть жарко, а зимой - холодно. Нарушение этого условия приведет к неверным результатам. Поэтому в состав класса включен метод CheckDay, проверяющий корректность задания свойств. В случае несоблюдения требуемых условий метод генерирует собственные ошибки класса. Вот описание нашего класса:
Option Explicit 'Класс Day 'Свойства класса Private today As Date Private temperature As Integer Public Property Get Сегодня() As Date Сегодня = today End Property Public Property Let Сегодня(ByVal NewValue As Date) today = NewValue End Property Public Property Get Температура() As Integer Температура = temperature End Property Public Property Let Температура(ByVal NewValue As Integer) temperature = NewValue End Property Public Sub CheckDay() Dim Desc As String Dim Numb As Long Dim Source As String 'Проверка свойств объекта Select Case Month(Сегодня) Case 6 To 8 If Температура < 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает положительную летнюю температуру!" Numb = vbObjectError + 513 Source = " Метод CheckDay класса Day " Err.Raise Numb, Source, Desc End If Case 1 To 2, 12 If Температура > 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает отрицательную зимнюю температуру!" Numb = vbObjectError + 514 Source = " Метод CheckDay класса Day " Err.Raise Numb, Source, Desc End If End Select End Sub
Приведем теперь процедуру, работающую с объектами, этого класса:
Public Sub WorkWithDay() 'Работа с объектами класса Day Dim myday As New Day Dim Msg As String 'Охраняемый блок On Error GoTo ErrorHandler myday.Сегодня = "9.8.99" myday.Температура = -15 myday.CheckDay Debug.Print myday.Сегодня, myday.Температура Exit Sub ErrorHandler: If Err.Number = vbObjectError + 513 Then Msg = vbCrLf "Введите температуру сегодняшнего дня " _ myday.Сегодня vbCrLf " Учтите, она должна быть положительной" myday.Температура = InputBox(Err.Source vbCrLf Err.Description Msg, "CheckDay", 15) ElseIf Err.Number = vbObjectError + 514 Then Msg = vbCrLf "Введите температуру сегодняшнего дня " _ myday.Сегодня vbCrLf " Учтите, она должна быть отрицательной" myday.Температура = InputBox(Err.Source vbCrLf Err.Description Msg, "CheckDay", -15) End If Resume End Sub
Заметьте, эта процедура, работающая с объектом myday класса Day, построена по всем правилам, - в ней есть
(рис 10.17) Окно диалога с пользователемПосле того, как пользователь задал нормальную летнюю температуру, процедура нормально завершила свою работу. В окне проверки напечатаны следующие результаты:
09.08.99 25
К каждой процедуре может быть подключено несколько обработчиков ошибок. Этот факт был специально отражен в схеме процедуры, когда в этой лекции описывалась модель управления ошибками. При возникновении ошибки в процедуре только один из них может быть активным. Активным обработчиком является тот обработчик, который связан с
В процессе вычислений одни процедуры могут вызывать другие. Поэтому в момент возникновения ошибки в стеке вызовов процедур могут находиться несколько процедур: C1, C2, …Cn. Каждая из этих процедур может иметь активный обработчик ошибок. Какой же из них будет применяться для обработки ошибки? Рассмотрим применяемую стратегию обработки. Итак, пусть есть непустой стек вызовов C1, C2, …Cn, где C1 это самый внешний вызов, а Cn - самый внутренний вызов. Обработка начинается подъемом по стеку вызовов. Если в Cn имеется активный обработчик ошибки, то он и получает управление, если его нет, то в стеке проверяется следующий по порядку вызов. Если ни один из вызовов C1 - Cn не имеет активного обработчика, то выполняется стандартная обработка с выдачей сообщения об ошибке и снятия приложения. Пусть Ck - это первый, найденный в стеке вызов, для которого существует активный обработчик, и который, как было сказано, получает управление. Обработчик Ck имеет две возможности:
Во втором случае, подъем по стеку вызовов может быть продолжен, пока его не захватит обработчик, способный обработать эту ошибку, или управление не попадет последнему исполняемому стандартному обработчику. Заметим, что хорошим методическим приемом является введение некоторого
Давайте теперь разберемся с тем, какова должна быть типичная структура обработчика ошибок, чтобы он мог выполнять обе возложенные на него функции.
Обработчик, способный обработать ошибку, должен распознать ее, понять к какому классу относится ошибка - классу устранимых ошибок или классу критических ошибок, последствия которых устранить невозможно. В первом случае, обработчик устраняет причину или следствия ошибки и возвращает управление в
Как правило, каждый обработчик предназначен для обнаружения и исправления ошибок некоторого класса. Поэтому, когда он встречается с ошибкой другого класса, то он не способен ее обработать. В этом случае он передает эту функцию выше стоящему обработчику. Реализуется это тем, что обработчик вызывает метод Raise с тем же номером ошибки. При возбуждении ошибки в обработчике ошибки возобновится процесс подъема по стеку вызовов и управление сможет получить следующий активный обработчик ошибки, который в свою очередь, либо обработает ошибку, либо передаст ее вверх. Заметим, что возбуждение ошибки в обработчике ошибки может быть сознательным, но может быть и из-за того, что некорректно работает сам обработчик. В любом случае возбуждение ошибки приведет к подъему по стеку вызовов.
Типичный обработчик ошибок представляет собой оператор выбора Select, в котором каждый случай соответствует одной обрабатываемой ошибке, а для непредусмотренных ошибок происходит повторное их возбуждение и, тем самым, передача их вверх по стеку вызовов. Допустим, в K1, K2, …, Kn - обработчик ошибок этой процедуры может быть таким:
'ErrorHandler: Select Case Err.Number ' анализ кода ошибки. Case K1 … 'обработка ошибки с кодом K1 Case K2 … 'обработка ошибки с кодом K2 . . . Case Kn … 'обработка ошибки с кодом Kn Case Else 'Передача управления обработчику,выше стоящему в стеке вызовов Dim intErrNum As Integer intErrNum = Err.Number 'номер ошибки Err.Clear ' чистка объекта Err. Err.Raise Number:= intErrNum ' повторное возбуждение ошибки End Select
Метод Raise здесь используется для повторения исходной ошибки. Если произойдет ошибка, отличная от ошибок с кодами K1, K2, …, Kn, управление будет передано вверх по стеку вызовов другому активному обработчику, если таковой есть. Заметьте, перед вызовом метода Raise происходит чистка объекта Err.
Сделаем еще несколько замечаний об обработке ошибок в Office 2000:
AccessError.При работе с процедурами стандартного модуля есть еще один способ для возврата кодов ошибок, определенных пользователем. Для этой цели можно использовать функцию CVErr, возвращающую значение типа Variant с подтипом Error, которое содержит код ошибки, указанный пользователем. В вызывающей процедуре с помощью булевой функции IsError можно проверить, является ли возвращенное значение ошибкой. В следующем примере генерируется ошибка 1999, если аргумент функции Func1 является нечисловым.
Function Func1(Number As Variant) As Variant If IsNumeric(Number) Then ' Вычисление корректного результата. Func1 = Number * Number Else 'аргумент некорректен Func1 = CVErr(1999) ' возвращает код ошибки End If End Function
Проверять корректность работы Func1 можно так.
Sub Testfunc1() Dim res As Variant, arg As Variant arg = 12 res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg Else Debug.Print "Результат : ", res End If arg = "двенадцать" res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg Else Debug.Print "Результат : ", res End If End Sub
Приведем результаты вычислений:
Результат : 144 Ошибка #: Error 1999 аргумент : двенадцать
Результаты работы с системой документов должны удовлетворять заданным спецификациям. Эти результаты должны быть получены в заданное время. Точный прогноз погоды на завтра требуется сегодня, получить его послезавтра становится бессмысленным. Поэтому программисту постоянно приходится заботиться не только о корректности работы своих программ, но и об их эффективности. И здесь успех, во многом, определяется решениями, принимаемыми на самом верхнем уровне разработки проекта. Самое важное решение связано с выбором подходящей среды программирования. Здесь нужно точно учитывать перспективы развития разрабатываемой системы и выбираемой среды программирования, насколько средства, выбранные Вами сегодня, смогут удовлетворять Вашим завтрашним потребностям. Другой разрез, для многих задач не менее важный, состоит в правильном выборе структур данных и алгоритмов их обработки. Нет ничего лучше хорошего алгоритма. Он, как правило, определяет эффективность решения задач. Важную роль в повышении эффективности играет и переиспользование. Ранее созданные, тщательно проверенные и эффективные компоненты, могут определить и эффективность вновь решаемой задачи. Рассмотрение всех этих вопросов, главных источников эффективности работы системы в целом, выходит за рамки данной книги. Мы остановимся сейчас на деталях, каждая из которых в отдельности не способна существенно повысить эффективность решения. При добыче золота, можно пытаться найти самородок, а можно промывать руду, добывая песчинку за песчинкой. Так и при разработке программ, песчинки, повышающие эффективность, могут принести в результате ощутимый эффект.
Прежде, чем начать рассмотрение отдельных элементов, еще пару слов о измерениях, которые выполняет программист, для повышения эффективности своей программы. Известен эффект "bottle neck", - узких мест программы, которые могут быть причиной ее не эффективного поведения. Чтобы получить представление о том, есть ли в программе такие узкие места, требующие первостепенного вмешательства и переписывания, чаще всего требуется построение полного профиля программы. Причем и здесь важна представительная система тестов. Опять таки, мы не будем сейчас рассматривать средства построения профиля. Отметим только, что в ряде случаев можно для проведения измерений использовать функцию Timer.По ходу изложения мы несколько раз демонстрировали ее применение.
По ходу изложения мы не раз обращали внимание на средства, повышающие эффективность вычислений. Об этом шла речь, при рассмотрении модулей, при рассмотрении вопроса об объявлении переменных и выполнения операций над ними. Сейчас мы приведем еще несколько советов, некоторые из которых уже упоминались ранее.
Для уменьшения размеров требуемой памяти и ускорения выполнения операций над данными:
Variant. Работа с Variant требует больше памяти и дополнительных преобразований.Integer преобразуются к типу Long, так что разумнее задавать сразу тип Long вместо типа Integer.Single и Double следует использовать только в случае действительной необходимости. Иногда, можно воспользоваться типом Currency для выполнения подобных действий. В этом случае потребуется больше памяти, но не будет обращений к сопроцессору с плавающей точкой.Object и Variant весьма полезные типы, но применять их нужно только в случае действительной необходимости.В одном из примеров, я приводил таблицу временных измерений выполнения арифметических операций над данными разных типов. Рекомендую провести подобные замеры в более полном объеме.
Single и Double. Для уменьшения подобных преобразований, записывайте выражения так, чтобы пересечение переменных целого и плавающего типа было, по возможности, минимальным.Replace в большинстве случаев. В тех случаях, когда заменяется одна подстрока на другую такой же размерности, можно использовать функцию Mid, как в следующем примере:Public Sub TestCode() Dim Text As String Text = "Компилятор кода" Mid(Text, 1, 5) = "Транс" Debug.Print Text End Sub
vbCrLf, чем комбинацию символов Chr(13), Chr(10), задающих возврат каретки и перевод строки.If Asc(Text) = 202
работает быстрее, чем код
If Left(Text,1) = "K"
Злоупотреблять такими оптимизациями не стоит, поскольку первый фрагмент менее понятен, чем второй. Он нуждается, по крайней мере, в комментариях.
Когда требуется уменьшить размеры используемой памяти, нужно начинать с чистки массивов. Когда требуется уменьшить время выполнения программы, нужно начинать с чистки циклов. Особо тщательно нужно оптимизировать выполнение самых внутренних циклов. Каждая миллисекунда, сэкономленная на выполнении внутреннего цикла, может обернуться сэкономленными часами на выполнении всей программы. Вот несколько советов, полезных при работе с циклами:
For Each вместо обычного цикла For. В большинстве случаев это приводит к существенному выигрышу во времени исполнения цикла.Завершая эту лекцию, хочу еще раз сказать, что главная оптимизация достигается на уровне проектных решений, выбора структуры данных и алгоритмов. К ниже перечисленным советам следует относиться с определенной осторожностью. Оптимизация не должна идти во вред ясности написания программ. Не всегда в ней есть необходимость. Но если такая необходимость возникает, то, в первую очередь, нужно выявить узкие места в программе и их, в основном, и оптимизировать.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.