Основы офисного программирования и язык VBA

         

Аргументы, являющиеся массивами


Аргументы процедуры могут быть массивами. Процедуре передается имя массива, а размерность массива определяется встроенными функциями LBound и UBound. Приведем пример процедуры, вычисляющей скалярное произведение векторов:

Public Function ScalarProduct(X() As Integer, Y() As Integer) As Integer 'Вычисляет скалярное произведение двух векторов. 'Предполагается, что границы массивов совпадают.

Dim i As Integer, Sum As Integer Sum = 0 For i = LBound(X) To UBound(X) Sum = Sum + X(i) * Y(i) Next i ScalarProduct = Sum End Function

Оба параметра процедуры, передаваемые по ссылке, являются массивами, работа с которыми в теле процедуры не представляет затруднений, благодаря тому, что функции LBound и UBound позволяют установить границы массива по любому измерению. Приведем программу, в которой вызывается функция ScalarProduct:

Public Sub TestScalarProduct() Dim A(1 To 5) As Integer Dim B(1 To 5) As Integer Dim C As Variant Dim Res As Integer Dim i As Integer

C = Array(1, 2, 3, 4, 5) For i = 1 To 5 A(i) = C(i - 1) Next i C = Array(5, 4, 3, 2, 1) For i = 1 To 5 B(i) = C(i - 1) Next i Res = ScalarProduct(A, B) Debug.Print Res End Sub



Деревья поиска


Для того, чтобы в полной мере продемонстрировать рекурсивные процедуры, рассмотрим создание класса, определяющего динамическую структуру данных - двоичное дерево поиска. Деревья поиска широко используются в программировании при работе с данными, требующими выполнения таких операций над ними, как поиск, сортировка, удаление и вставка. Деревья поиска применяются для представления словарей, справочников и подобных структур, где информация упорядочена с использованием ключей.

Бинарным будем называть дерево, у которого каждая вершина имеет одного или двух потомков, называемых левым и правым сыном (поддеревом). В дальнейшем будем полагать, что узел нашего дерева содержит информационное поле info и поле ключа - key. Деревом поиска (двоичным или лексикографическим деревом) будем называть бинарное дерево, в котором ключ каждой вершины больше ключа, хранящегося в корне левого поддерева, и меньше ключа, хранящегося в корне правого поддерева.

В предыдущих лекциях мы уже рассматривали создание классов, задающих динамические структуры данных. Одним из таких примеров было создание класса, описывающего линейный список. Поэтому общая схема создания таких классов должна быть ясна. Напомним, что в таких случаях обычно создаются два класса. Один из них задает элементы динамической структуры, второй - операции над элементами.



В модулях этого документа находятся


DocOne9



   В модулях этого документа находятся примеры главы 9


Функции с побочным эффектом


В классическом варианте все аргументы функции являются входными параметрами, и единственный результат вычисления функции - это ее возвращаемое значение., примером является функция Cube. Чаще всего, однако, используются функции с побочным эффектом, то есть такие функции, которые помимо получения значения функции изменяют значения некоторых результирующих параметров, передаваемых функции по ссылке. Например,

Public Function SideEffect(ByVal X As Integer, ByRef Y As Integer) As Integer SideEffect = X + Y Y = Y + 1 End Function

Public Sub TestSideEffect() Dim X As Integer, Y As Integer, Z As Integer X = 3: Y = 5 Z = X + Y + SideEffect(X, Y) Debug.Print X, Y, Z X = 3: Y = 5 Z = SideEffect(X, Y) + X + Y Debug.Print X, Y, Z End Sub



Вот результаты вычислений:

3 6 16 3 6 17

Как видите, обращаться с функциями, обладающими побочным эффектом, следует с осторожностью. В нашем примере результат вычисления суммы трех слагаемых зависит от порядка их записи. Это противоречит основным принципам математики. Более того, следует понимать, что результат вычисления непредсказуем, поскольку VBA может для увеличения эффективности изменять порядок действий при вычислении арифметического выражения. Поэтому лучше не использовать в выражении вызов функции с побочным эффектом, изменяющей значения входящих в него переменных.



Использование именованных аргументов


В предыдущих примерах фактические параметры вызова процедуры или функции располагались в том же порядке, что и формальные параметры в ее заголовке. Это не всегда удобно, особенно если некоторые аргументы необязательны (Optional). VBA позволяет указывать значения аргументов в произвольном порядке, используя их имена. При этом после имени аргумента ставятся двоеточие и знак равенства, после которого помещается значение аргумента (фактический параметр). Например, вызов рассмотренной выше процедуры-функции MyFunc может выглядеть так:

Myfunc Age:= 25, Name:= "Alex", Newdate:= DateOfArrival

Удобство такого способа особенно проявляется при вызове процедур с необязательными аргументами, которые всегда помещаются в конец списка аргументов в заголовке процедуры. Пусть, например, заголовок процедуры ProcEx:

Sub ProcEx(Name As String, Optional Age As Integer, Optional City = "Москва")

Список ее аргументов включает один обязательный аргумент Name и два необязательных: Age и City, - причем для последнего задано значение по умолчанию "Москва". Если при вызове этой процедуры второй аргумент не требуется, то при вызове, не использующем именованных параметров, сам параметр опускается, но, выделяющая его запятая, должна оставаться:

ProcEx "Оля",,"Тверь"

Вместо этого можно использовать вызов с именами аргументов:

ProcEx City:="Тверь", Name:="Оля"

в котором не требуется заменять пропущенный аргумент запятыми и соблюдать определенный порядок следования аргументов. Если некий необязательный аргумент не задан при вызове, вместо него подставляется значение, определенное пользователем по умолчанию, если и такое не определено, подставляется значение, определенное по умолчанию для соответствующего типа. Например, при вызове:

ProcEx Name:="Оля"

в качестве значения аргумента Age в процедуру передастся 0, а в качестве аргумента City - явно заданное по умолчанию значение "Москва".

Как процедура "узнает", передан ли ей при вызове необязательный аргумент? Для этого можно воспользоваться функцией IsMissing.
Она по имени аргумента возвращает логическое значение True, когда значение аргумента не передано в процедуру, и False, если аргумент задан. Но это все работает только в том случае, если параметр имеет тип Variant. Для всех остальных типов данных полагается, что в процедуру всегда передано значение параметра, явно или неявно заданное по умолчанию. Поэтому, если такая проверка необходима, то параметр должен иметь тип Variant. Отметим также, что для массива аргументов ParamArray функция IsMissing всегда возвращает False, и для установления его пустоты нужно проверять, что верхняя граница индекса меньше нижней.

Рассмотрим функцию от двух аргументов, второй из которых необязателен:

Function TwoArgs(I As Integer, Optional X As Variant) As Variant If IsMissing(X) Then ' если 2-ой аргумент отсутствует, то вернуть 1-ый. TwoArgs = I Else ' если 2-ой аргумент есть, то вернуть их произведение TwoArgs = I * X End If End Function

Вот результаты нескольких вызовов этой функции в окне отладки:

? TwoArgs(5,7) 35 ? TwoArgs(5.5) 6 ? TwoArgs(5, 5.5) 27,5 ? TwoArgs(5, "6") 30


Класс BinTree


Класс BinTree содержит одно свойство - объект класса TreeNode, задающий корень дерева и группу операций над элементами дерева. Одним из основных методов класса является метод SearchAndInsert, который, по существу, реализует две операции - поиска элемента в дереве по заданному ключу и вставки элемента в дерево. Заметьте, что вставка должна быть реализована так, чтобы выполнялось основное условие дерева поиска: для каждой вершины ее ключ больше всех ключей вершин левого поддерева и меньше всех ключей вершин правого поддерева. Такая структура обеспечивает эффективное выполнение операций поиска, так как в этом случае для поиска требуется просмотр вершин, лежащих только на одной ветви дерева, ведущей от корня к искомому элементу. Этот метод обеспечивает создание класса, позволяя, начав с корня, вставлять элемент за элементом. Кроме этого метода мы определим методы для удаления элементов и группу методов обхода дерева. Все методы реализованы с использованием рекурсивных алгоритмов. Приведем описание класса:

Пример 9.4.

(html, txt)

Все методы класса довольно подробно прокомментированы, однако хотелось бы подчеркнуть некоторые моменты:

Начнем с общего замечания, связанного с реализацией рекурсивных алгоритмов. Рекурсия это мощный инструмент, полезный при решении многих задач по обработке данных. Для тех, кто не привык писать рекурсивные программы, мы рекомендуем внимательно разобрать реализацию приведенных методов класса. Каждое рекурсивное определение содержит некоторый базис, позволяющий найти решение в простейшем случае без использования рекурсии, а затем вся задача сводится к нескольким подобным задачам, но меньшей размерности. Если число задач, к которым сводится исходная задача, не меньше двух, то можно заведомо говорить, что рекурсивное решение намного проще не рекурсивного алгоритма и использование рекурсии оправданно. Для пояснения этих общих утверждений обратимся к примеру. В нашем классе приведены три метода обхода бинарного дерева: PrefixOrder, InfixOrder, PostfixOrder.
Написать не рекурсивный алгоритм, который обходил бы все узлы дерева некоторым заданным образом не так то просто. Другое дело рекурсивное определение. Действительно базисное решение очевидно, - когда дерево пусто, то ничего и делать не надо. Если же оно не пусто, то у нас есть корень дерева, а у него два потомка, которые в свою очередь являются деревьями. Поэтому для обхода всего дерева достаточно посетить корень, а затем обойти (рекурсивно) оба поддерева. Меняя порядок посещения корня и поддеревьев, получаем три различных способа обхода дерева. Заметим, именно благодаря тому, что сама структура данных рекурсивна, рекурсивные алгоритмы естественным образом описывают решения задач по обработке таких данных. Рекурсивные определения просты и понятны, но напоминают некоторый фокус. Наиболее сложно воспроизвести вычисления, выполняемые рекурсивным алгоритмом.Для простоты в методе SearchAndInsert мы совместили две операции поиска элемента по заданному ключу и вставки нового элемента. Если в дереве найден элемент с заданным ключом, то предполагается, что речь идет о поиске и возвращается информация из информационного поля этого элемента. Если в дереве нет элемента с таким ключом, то создается новый узел дерева. Заметьте, что наше решение не позволяет производить замену элемента, а в процессе поиска не уведомляет об отсутствии элемента с заданным ключомУдаление элемента из дерева поиска осложняется тем, что нужно поддерживать структуру дерева поиска. В тех случаях, когда нужно удалить элемент, у которого есть два потомка, вызывается специальная процедура ReplaceAndDelete. Эта процедура ищет кандидата, который мог бы заменить удаляемый элемент, сохраняя структуру дерева.Недостатком деревьев поиска является то, что они могут быть плохо сбалансированы и могут иметь относительно длинные ветви. Так, если при создании дерева поиска, ключи будут поступать в отсортированном порядке, то дерево будет представлено одной ветвью. Работа с этой структурой данных предполагает, что при создании и добавлении элементов в дерево ключи поступают в случайном порядке, хорошо перемешанные.Эта структура особенно применима в тех случаях, когда в процессе работы над данными широко используются все операции - поиск, вставка и удаление.Мы не стали писать реализацию этого класса, оперирующего с данными, хранящимися в списках Excel или базе данных Access, поскольку это выходит за рамки этой лекции.



End Sub

Public Sub ReplaceAndDelete(q As TreeNode) ' Заменяет узел на самый правый If Not (root.right.root Is Nothing) Then root.right.ReplaceAndDelete q Else 'Найден самый правый q.key = root.key: q.info = root.info Set root = root.left.root End If

End Sub

Пример 9.4.

Все методы класса довольно подробно прокомментированы, однако хотелось бы подчеркнуть некоторые моменты:

Начнем с общего замечания, связанного с реализацией рекурсивных алгоритмов. Рекурсия это мощный инструмент, полезный при решении многих задач по обработке данных. Для тех, кто не привык писать рекурсивные программы, мы рекомендуем внимательно разобрать реализацию приведенных методов класса. Каждое рекурсивное определение содержит некоторый базис, позволяющий найти решение в простейшем случае без использования рекурсии, а затем вся задача сводится к нескольким подобным задачам, но меньшей размерности. Если число задач, к которым сводится исходная задача, не меньше двух, то можно заведомо говорить, что рекурсивное решение намного проще не рекурсивного алгоритма и использование рекурсии оправданно. Для пояснения этих общих утверждений обратимся к примеру. В нашем классе приведены три метода обхода бинарного дерева: PrefixOrder, InfixOrder, PostfixOrder. Написать не рекурсивный алгоритм, который обходил бы все узлы дерева некоторым заданным образом не так то просто. Другое дело рекурсивное определение. Действительно базисное решение очевидно, - когда дерево пусто, то ничего и делать не надо. Если же оно не пусто, то у нас есть корень дерева, а у него два потомка, которые в свою очередь являются деревьями. Поэтому для обхода всего дерева достаточно посетить корень, а затем обойти (рекурсивно) оба поддерева. Меняя порядок посещения корня и поддеревьев, получаем три различных способа обхода дерева. Заметим, именно благодаря тому, что сама структура данных рекурсивна, рекурсивные алгоритмы естественным образом описывают решения задач по обработке таких данных. Рекурсивные определения просты и понятны, но напоминают некоторый фокус.


Наиболее сложно воспроизвести вычисления, выполняемые рекурсивным алгоритмом.Для простоты в методе SearchAndInsert мы совместили две операции поиска элемента по заданному ключу и вставки нового элемента. Если в дереве найден элемент с заданным ключом, то предполагается, что речь идет о поиске и возвращается информация из информационного поля этого элемента. Если в дереве нет элемента с таким ключом, то создается новый узел дерева. Заметьте, что наше решение не позволяет производить замену элемента, а в процессе поиска не уведомляет об отсутствии элемента с заданным ключомУдаление элемента из дерева поиска осложняется тем, что нужно поддерживать структуру дерева поиска. В тех случаях, когда нужно удалить элемент, у которого есть два потомка, вызывается специальная процедура ReplaceAndDelete. Эта процедура ищет кандидата, который мог бы заменить удаляемый элемент, сохраняя структуру дерева.Недостатком деревьев поиска является то, что они могут быть плохо сбалансированы и могут иметь относительно длинные ветви. Так, если при создании дерева поиска, ключи будут поступать в отсортированном порядке, то дерево будет представлено одной ветвью. Работа с этой структурой данных предполагает, что при создании и добавлении элементов в дерево ключи поступают в случайном порядке, хорошо перемешанные. Эта структура особенно применима в тех случаях, когда в процессе работы над данными широко используются все операции - поиск, вставка и удаление.Мы не стали писать реализацию этого класса, оперирующего с данными, хранящимися в списках Excel или базе данных Access, поскольку это выходит за рамки этой лекции.


Класс TreeNode


Как следует из определения бинарного дерева, каждый его узел содержит ссылки на левого и правого сына. Кроме того, предполагается, что информация, хранящаяся в узле, сопровождается ключом. Несколько упрощая ситуацию, можно предложить следующее описание класса TreeNode, задающее описание узла бинарного дерева поиска:

'Class TreeNode 'Элемент дерева

Public key As String Public info As String Public left As New BinTree Public right As New BinTree



Классификация процедур


Процедуры VBA можно классифицировать по нескольким признакам: по способу использования (вызова) в программе, по способу запуска процедуры на выполнение, по способу создания кода процедуры, по месту нахождения кода процедуры в проекте.

Процедуры VBA подразделяются на подпрограммы и функции. Первые описываются ключевым словом Sub, вторые - Function. Мы очень редко используем термин подпрограмма, характерный для VBA, и вместо него используем термин процедура, более распространенный в программировании. Иногда, правда, это может приводить к недоразумениям, поскольку в зависимости от контекста под процедурой понимается как подпрограмма, так и функция. Различие между этими видами процедур скорее синтаксическое, так как преобразовать процедуру одного вида в эквивалентную процедуру другого вида совсем не сложно. В языке С/С++, как известно, есть только функции и нет процедур, по крайней мере формально.

По способу создания кода процедуры делятся на обычные, разрабатываемые "вручную", и на процедуры, код которых создается автоматически генератором макросов (MacroRecoder); их называют также макро-процедурами или командными процедурами, поскольку их код - это последовательность вызовов команд соответствующего приложения Office 97. И это разделение в известной степени условно, так как довольно типичны процедуры, каркасы которых, созданные генератором макросов, затем изменяют и дописывают вручную.

По способу запуска процедур на выполнение можно выделить в отдельную группу процедуры, запускаемые автоматически при наступлении того или иного события, - мы называем их процедурами обработки событий.

По положению в проекте различаются процедуры, находящиеся в специальных программных единицах - стандартных модулях, модулях классов и модулях, связанными с объектами, реагирующими на события.

Еще один специальный тип процедур - процедуры-свойства Property Let, Property Set и Property Get. Они служат для задания и получения значений закрытых свойств класса.

Главное назначение процедур во всех языках программирования состоит в том, что при их вызове они изменяют состояние программного проекта, - изменяют значения переменных (свойства объектов), описанных в модулях проекта. У процедур VBA сфера действия шире. Их главное назначение состоит в изменении состояния системы документов, частью которого является изменение состояния самого программного проекта. Поэтому процедуры VBA оперируют, в основном, с объектами Office 2000. Заметьте, есть два способа, с помощью которых процедура получает и передает информацию, изменяя тем самым состояние системы документов. Первый и основной способ состоит в использовании параметров процедуры. При вызове процедуры ее аргументы, соответствующие входным параметрам получают значение, так процедура получает информацию от внешней среды, в результате работы процедуры формируются значения выходных параметров, переданных ей по ссылке, тем самым изменяется состояние проекта и документов. Второй способ состоит в использовании процедурой глобальных переменных и объектов, как для получения, так и для передачи информации.



Конструкция ParamArray


Иногда, когда в процедуру следует передать только один массив, для этой цели можно использовать конструкцию ParamArray. Следующая процедура PosNeg подсчитывает суммы поступлений Positive и расходов Negative, указанные в массиве Sums:

Sub PosNeg(Positive As Integer, Negative As Integer, ParamArray Sums() As Variant) Dim I As Integer Positive = 0: Negative = 0 For I = 0 To UBound(Sums()) ' цикл по всем элементам массива If Sums(I) > 0 Then Positive = Positive + Sums(I) Else Negative = Negative - Sums(I) End If Next I End Sub

Вызов процедуры PosNeg может иметь такой вид:

Public Sub TestPosNeg() Dim Incomes As Integer, Expences As Integer

PosNeg Incomes, Expences, -20, 100, 25, -44, -23, -60, 120 Debug.Print Incomes, Expences End Sub

В результате переменная Incomes получит значение 245, а переменная Expences - 147. Заметьте, преимуществом использования массива аргументов ParamArray является возможность непосредственного перечисления элементов массива в момент вызова.

Однако такое использование массива аргументов ParamArray не исчерпывает всех его возможностей. В более сложных ситуациях передаваемые аргументы могут иметь разные типы. Мы приведем сейчас пример, в котором, во-первых, действуют объекты Office 2000, а, во-вторых, используется передача параметров через массив аргументов ParamArray.



Описание и создание процедур


Ранее мы говорили, что в офисном программировании целью работы является создание системы документов. С системой документов связан программный проект, представляющий совокупность программных проектов, связанных с каждым отдельным документом. Проекты разных документов связаны между собой и могут иметь общие данные и общие процедуры и функции, доступные из разных проектов. Каждый проект состоит из модулей разного типа. Каждый модуль содержит объявления переменных, процедур и функций. Процедуры и функции являются минимальными модульными конструкциями, из которых строится программный проект.

Процедура (функция) - это программная единица VBA, включающая операторы описания ее локальных данных и исполняемые операторы. Обычно в процедуру объединяют регулярно выполняемую последовательность действий, решающую отдельную задачу или подзадачу. Особенность процедур VBA в том, что они работают в мощном окружении Office 97 и могут использовать в качестве элементарных действий большое количество встроенных методов и функций, оперирующих с разнообразными объектами этой системы. Поэтому структура управления типичной процедуры прикладной офисной системы довольно проста: она состоит из последовательности вызовов встроенных процедур и функций, управляемой небольшим количеством условных операторов и циклов. Ее размеры не должны превышать нескольких десятков строк. Если в Вашей процедуре несколько сотен строк, это, скорее всего, значит, что задачу, решаемую процедурой, можно разбить на несколько самостоятельных подзадач, для решения каждой из которых следует написать отдельную процедуру. Это облегчит понимание программы и ее отладку. Разумеется, эти замечания носят неформальный методологический характер, поскольку сам VBA никак не ограничивает ни размер процедуры, ни сложность ее структуры управления.



Пользовательские функции, принимающие сложный объект Range


Известно, что в Excel объект Range может представлять несмежную область и являться объединением нескольких интервалов ячеек. Иначе говоря, один объект Range может задавать несколько массивов рабочего листа. Можно ли такой объект передать пользовательской функции и, если да, как его обрабатывать? Ответ: "можно", хотя соответствующий формальный параметр следует описывать особым образом. Процедуры и функции VBA допускают произвольное число параметров, это достигается за счет того, что один, последний по счету формальный параметр может иметь спецификатор ParamArray. В этом случае данный параметр задает фактически массив параметров с произвольным числом элементов. Именно эта техника и применяется для передачи в пользовательскую функцию сложного объекта Range, представляющего не один, а произвольное число массивов. У такой функции последний параметр должен иметь спецификатор ParamArray и быть массивом типа Variant.

Рассмотрим предыдущую задачу, немного усложнив ее, полагая, что при проверке кандидата на "медианность" используется произвольное число массивов. Вот как выглядит функция, решающая эту задачу:

Пример 9.2.

(html, txt)

Комментируя работу этой функции, отметим:

Эта функция может (и будет) вызываться как из процедур VBA, так и из формул рабочего листа Excel.Формально функция по-прежнему имеет два параметра Cand и M. Правда, теперь они поменялись местами, и параметр M стал последним. Фактически у этой функции теперь произвольное число параметров, поскольку параметр M, сохранив тип Variant, стал теперь массивом. Спецификатор ParamArray подчеркивает, что это специальный массив с произвольным числом элементов.Для работы с массивом M используется цикл типа For Each. В цикле выделяется очередной элемент Elem типа Variant, а дальше используется уже знакомый по функции IsMediana алгоритм проверки элемента Cand.Разбор случаев делается независимо для каждого из элементов массива M.

Демонстрацию использования этой функции начнем с ее вызова в процедуре VBA, которая передает ей целочисленный массив элементов:


Известно, что в Excel объект Range может представлять несмежную область и являться объединением нескольких интервалов ячеек. Иначе говоря, один объект Range может задавать несколько массивов рабочего листа. Можно ли такой объект передать пользовательской функции и, если да, как его обрабатывать? Ответ: "можно", хотя соответствующий формальный параметр следует описывать особым образом. Процедуры и функции VBA допускают произвольное число параметров, это достигается за счет того, что один, последний по счету формальный параметр может иметь спецификатор ParamArray. В этом случае данный параметр задает фактически массив параметров с произвольным числом элементов. Именно эта техника и применяется для передачи в пользовательскую функцию сложного объекта Range, представляющего не один, а произвольное число массивов. У такой функции последний параметр должен иметь спецификатор ParamArray и быть массивом типа Variant.

Рассмотрим предыдущую задачу, немного усложнив ее, полагая, что при проверке кандидата на "медианность" используется произвольное число массивов. Вот как выглядит функция, решающая эту задачу:

Public Function IsMedianaForAll(Cand As Variant, ParamArray M() As Variant) As Integer 'Эта функция осуществляет те же вычисления, что и функция IsMediana 'Важное отличие состоит в том, что аргумент M может быть задан сложным объектом 'Range как объединение массивов. Dim i As Integer, j As Integer Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 Dim Elem As Variant 'Теперь M - это массив параметров, а Elem - его элемент. For Each Elem In M 'Анализ типа параметра Elem If TypeName(Elem) = "Range" Then For i = 1 To Elem.Rows.Count For j = 1 To Elem.Columns.Count If Elem.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf Elem.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i ElseIf TypeName(Elem) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound.


Public Sub TestIsMedianaForAll() Const Size = 7 Dim Mas( 1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMedianaForAll(Cand, Mas) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub

Приведем результаты выполнения этой процедуры:

Integer() Массив: 9 1 14 11 11 18 0 Кандидат: 12 Результат: -3

А теперь покажем, как эта функция вызывается в формулах рабочего листа Excel На том же рабочем листе, где мы проводили эксперименты с формулами, вызывающими функцию IsMediana, мы записали еще несколько формул, вызывающих функцию IsMedianaForAll. Заметьте, ее аргумент может иметь значительно более сложный вид, чем при вызове функции IsMedina.


Рис. 9.2.  Вызов функции IsMedianaForAll, допускающей сложные объекты Range

Проанализируем четыре сделанных вызова:

=IsMedianaForAll(7;M;N). В этом вызове наш кандидат - число 7 - проверяется по отношению к объединению двух массивов рабочего листа, заданных своими именами M и N. Формальных параметров у функции два, а фактических при вызове задается три. Два последних можно рассматривать как сложный объект Range, представляющий несмежную область ячеек и объединение вектора M и матрицы N. С программистской точки зрения, можно полагать, что передается массив с произвольным числом элементов, где каждый из них в свою очередь является массивом. Такой фактический параметр является допустимым значением формального параметра нашей функции, имеющего спецификатор ParamArray.=IsMedianaForAll(4,5;N;M)). В этом вызове мало нового в сравнении с предыдущим. Изменен порядок следования массивов N и M, изменен кандидат, - им стало число 4.5, не входящее ни в один из массивов. Как показывает результат, это число является медианой объединенных массивов.=IsMedianaForAll(7; {4;7;2}; {9;12;5}).Здесь в роли аргументов выступают массивы, заданные в виде констант, заключенных в фигурные скобки. Фактическое значение параметра M в этом случае представляет массив из двух элементов, каждый из которых в свою очередь является массивом.=IsMedianaForAll(7; {4;7;2}; {9;12;5}; M). Ситуация в этом вызове сложнее, так как число аргументов возросло, но, что более важно, среди них есть как массивы - константы, так и массив рабочего листа - вектор M. Тем не менее, все работает правильно.



Dim Val As Variant For Each Val In Elem If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val ElseIf TypeName(Elem) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(Elem) To UBound(Elem) If Elem(i) > Cand Then Pos = Pos + 1 ElseIf Elem(i) < Cand Then Neg = Neg + 1 End If Next i Else MsgBox ("При вызове IsMedianaForAll один из аргументов" _ & "не является массивом или объектом Range!") End If Next Elem IsMedianaForAll = Pos - Neg End Function

Пример 9.2.

Комментируя работу этой функции, отметим:

Эта функция может (и будет) вызываться как из процедур VBA, так и из формул рабочего листа Excel.Формально функция по-прежнему имеет два параметра Cand и M. Правда, теперь они поменялись местами, и параметр M стал последним. Фактически у этой функции теперь произвольное число параметров, поскольку параметр M, сохранив тип Variant, стал теперь массивом. Спецификатор ParamArray подчеркивает, что это специальный массив с произвольным числом элементов.Для работы с массивом M используется цикл типа For Each. В цикле выделяется очередной элемент Elem типа Variant, а дальше используется уже знакомый по функции IsMediana алгоритм проверки элемента Cand.Разбор случаев делается независимо для каждого из элементов массива M.

Демонстрацию использования этой функции начнем с ее вызова в процедуре VBA, которая передает ей целочисленный массив элементов:

Public Sub TestIsMedianaForAll() Const Size = 7 Dim Mas(1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMedianaForAll(Cand, Mas) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub

Приведем результаты выполнения этой процедуры:



Integer() Массив: 9 1 14 11 11 18 0 Кандидат: 12 Результат: -3

А теперь покажем, как эта функция вызывается в формулах рабочего листа Excel На том же рабочем листе, где мы проводили эксперименты с формулами, вызывающими функцию IsMediana, мы записали еще несколько формул, вызывающих функцию IsMedianaForAll. Заметьте, ее аргумент может иметь значительно более сложный вид, чем при вызове функции IsMedina.


Рис. 9.2.  Вызов функции IsMedianaForAll, допускающей сложные объекты Range

Проанализируем четыре сделанных вызова:

=IsMedianaForAll(7;M;N). В этом вызове наш кандидат - число 7 - проверяется по отношению к объединению двух массивов рабочего листа, заданных своими именами M и N. Формальных параметров у функции два, а фактических при вызове задается три. Два последних можно рассматривать как сложный объект Range, представляющий несмежную область ячеек и объединение вектора M и матрицы N. С программистской точки зрения, можно полагать, что передается массив с произвольным числом элементов, где каждый из них в свою очередь является массивом. Такой фактический параметр является допустимым значением формального параметра нашей функции, имеющего спецификатор ParamArray.=IsMedianaForAll(4,5;N;M)). В этом вызове мало нового в сравнении с предыдущим. Изменен порядок следования массивов N и M, изменен кандидат, - им стало число 4.5, не входящее ни в один из массивов. Как показывает результат, это число является медианой объединенных массивов.=IsMedianaForAll(7; {4;7;2}; {9;12;5}). Здесь в роли аргументов выступают массивы, заданные в виде констант, заключенных в фигурные скобки. Фактическое значение параметра M в этом случае представляет массив из двух элементов, каждый из которых в свою очередь является массивом.=IsMedianaForAll(7; {4;7;2}; {9;12;5}; M). Ситуация в этом вызове сложнее, так как число аргументов возросло, но, что более важно, среди них есть как массивы - константы, так и массив рабочего листа - вектор M. Тем не менее, все работает правильно.


M As Variant, Cand As


Public Function IsMediana( M As Variant, Cand As Variant) As Integer 'Дан массив M и элемент Cand. В качестве результата возвращается 'разность между числом элементов массива M, больших и меньших Cand. Dim i As Integer, j As Integer Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 'Анализ типа параметра M If TypeName(M) = "Range" Then For i = 1 To M.Rows.Count For j = 1 To M.Columns.Count If M.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf M.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i IsMediana = Pos - Neg ElseIf TypeName(M) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In M If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val IsMediana = Pos - Neg ElseIf TypeName(M) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(M) To UBound(M) If M(i) > Cand Then Pos = Pos + 1 ElseIf M(i) < Cand Then Neg = Neg + 1 End If Next i IsMediana = Pos - Neg Else MsgBox ("При вызове функции:IsMediana(M,Cand)" _ & "- M не является массивом или объектом Range!") End If End Function
Пример 9.1.
Закрыть окно




Public Function IsMedianaForAll(Cand As Variant, ParamArray M() As Variant) As Integer ' Эта функция осуществляет те же вычисления, что и функция IsMediana 'Важное отличие состоит в том, что аргумент M может быть задан сложным объектом 'Range как объединение массивов. Dim i As Integer, j As Integer Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 Dim Elem As Variant 'Теперь M - это массив параметров, а Elem - его элемент. For Each Elem In M 'Анализ типа параметра Elem If TypeName(Elem) = "Range" Then For i = 1 To Elem.Rows.Count For j = 1 To Elem.Columns.Count If Elem.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf Elem.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i ElseIf TypeName(Elem) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In Elem If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val ElseIf TypeName(Elem) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(Elem) To UBound(Elem) If Elem(i) > Cand Then Pos = Pos + 1 ElseIf Elem(i) < Cand Then Neg = Neg + 1 End If Next i Else MsgBox ("При вызове IsMedianaForAll один из аргументов" _ & "не является массивом или объектом Range!") End If Next Elem IsMedianaForAll = Pos - Neg End Function
Пример 9.2.
Закрыть окно




Public Sub TestRecursive() 'Сравнение по времени рекурсивной и нерекурсивной реализации факториала. Dim i As Long, Res As Long Dim Start As Single, Finish As Single 'Рекурсивное вычисление факториала Start = Timer For i = 1 To 100000 Res = Fact(12) Next i Finish = Timer Debug.Print "Время рекурсивных вычислений:", Finish - Start
'Нерекурсивное вычисление факториала Start = Timer For i = 1 To 100000 Res = Fact1(12) Next i Finish = Timer Debug.Print "Время нерекурсивных вычислений:", Finish - Start End Sub
Пример 9.3.
Закрыть окно




Option Explicit
'Класс BinTree 'Бинарным будем называть дерево, у которого каждая вершина имеет 'одного или двух потомков, называемых левым и правым сыном (поддеревом). 'В дальнейшем будем полагать, что узел нашего дерева содержит 'информационное поле info и поле ключа - key. 'Деревом поиска (двоичным или лексикографическим деревом) будем называть 'бинарное дерево, в котором ключ каждой вершины больше ключа, хранящегося 'в корне левого поддерева, и меньше ключа, хранящегося в корне правого поддерева. 'Рассмотрим операции над деревом поиска: поиск, включение, удаление элементов 'и обход дерева. Все операции сохраняют структуру дерева поиска.
Public root As TreeNode
Public Sub PrefixOrder() 'Префиксный обход дерева (корень, левое поддерево, правое)
If Not (root Is Nothing) Then With root Debug.Print "key: ",.key, "info: ",.info .left.PrefixOrder .right.PrefixOrder End With End If
End Sub
Public Sub InfixOrder() 'Инфиксный обход дерева (левое поддерево, корень, правое)
If Not (root Is Nothing) Then With root .left.InfixOrder Debug.Print "key: ",.key, "info: ",.info .right.InfixOrder End With End If
End Sub
Public Sub PostfixOrder() 'Постфиксный обход дерева (левое поддерево, правое, корень)
If Not (root Is Nothing) Then With root .left.PostfixOrder .right.PostfixOrder Debug.Print "key: ",.key, "info: ",.info End With End If
End Sub
Public Sub SearchAndInsert(key As String, info As String) 'Если в дереве есть узел с ключом key, 'то возвращается информация в этом узле - работает поиск 'Если такого узла нет, то создается новый узел и его поля 'заполняются информацией, - работает вставка. 'Вначале поиск If root Is Nothing Then ' элемент не найден и происходит вставка Set root = New TreeNode root.key = key: root.info = info ElseIf key < root.key Then 'Поиск в левом поддереве root.left.SearchAndInsert key, info ElseIf key > root.key Then 'Поиск в правом поддереве root.right.SearchAndInsert key, info Else 'Элемент найден - возвращается результат поиска info = root.info End If
End Sub
Public Sub DelInTree(key As String) 'Эта процедура позволяет удалить элемент дерева с заданным ключом 'Удаление с сохранением структуры дерева более сложная операция, 'чем вставка или поиск. Причина сложности в том, что при удалении 'элемента остаются два его потомка, которые необходимо корректно 'связать с оставшимися элементами, чтобы не нарушить структуру дерева поиска. 'В программе анализируются три случая: 'Удаляется лист дерева (нет потомков - нет проблем), 'Удаляется узел с одним потомком (потомок замещает удаленный узел), 'Есть два потомка. В этом случае узел может быть заменен одним из двух 'возможных кандидатов, не имеющих двух потомков. 'Кандидатами являются самый левый узел правого подддерева и 'самый правый узел левого поддерева. 'Мы производим удаление в левом поддереве.
Dim q As TreeNode If root Is Nothing Then Debug.Print "Key is not found" ElseIf key < root.key Then 'Удаляем из левого поддерева root.left.DelInTree key ElseIf key > root.key Then 'Удаляем из правого поддерева root.right.DelInTree key Else 'Удаление узла Set q = root If q.right.root Is Nothing Then Set root = q.left.root ElseIf q.left.root Is Nothing Then Set root = q.right.root Else 'есть два потомка q.left.ReplaceAndDelete q End If Set q = Nothing End If
End Sub
Public Sub ReplaceAndDelete(q As TreeNode) 'Заменяет узел на самый правый If Not (root.right.root Is Nothing) Then root.right.ReplaceAndDelete q Else 'Найден самый правый q.key = root.key: q.info = root.info Set root = root.left.root End If
End Sub
Пример 9.4.
Закрыть окно




Public Sub WorkwithBinTree() Dim MyDict As New BinTree Dim englword As String, rusword As String 'Создание словаря
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
End Sub
Пример 9.5.
Закрыть окно



Работа со словарем


Используем класс BinTree для работы со словарем. В нашем примере работы с классом будет создаваться словарь, в нем будет осуществляться поиск и удаление элементов. Вот текст процедуры, выполняющей эти операции:

Пример 9.5.

(html, txt)

Приведем результаты ее работы:

key: dictionary info: словарь key: backup info: резервная копия key: account info: учетная запись key: compiler info: компилятор key: hardware info: аппаратура, аппаратные средства key: file info: файл key: processor info: процессор key: token info: лексема account учетная запись key: dictionary info: словарь key: backup info: резервная копия key: compiler info: компилятор key: file info: файл key: processor info: процессор key: token info: лексема

Обратите внимание, процедура обхода дерева в префиксном порядке печатает слова из словаря не в том порядке, в каком он создавался. Это и понятно, поскольку дерево создается, как лексикографическое дерево поиска. Взгляните, как выглядит дерево поиска нашего словаря после его первоначального создания.


увеличить изображение
Рис. 9.3.  Лексикографическое дерево, задающее словарь



Рекурсивные процедуры


VBA допускает создание рекурсивных процедур, т. е. процедур, при вычислении вызывающих самих себя. Вызовы рекурсивной процедуры могут непосредственно входить в ее тело, или она может вызывать себя через другие процедуры. В последнем случае в модуле есть несколько связанных рекурсивных процедур. Стандартный пример рекурсивной процедуры - функция-факториал Fact(N)= N!. Вот ее определение в VBA:

Function Fact(N As Integer) As Long If N <= 1 Then ' базис индукции. Fact = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact = Fact(N - 1) * N End If End Function

Так как каждый вызов процедуры требует накладных расходов, эффективнее для факториала итеративная программа:

Function Fact1(N As Integer) As Long Dim Fact As Long, i As Integer Fact = 1 ' 0! =1. If N > 1 Then ' цикл вместо рекурсии. For i = 1 To N Fact = Fact * i Next i End If Fact1 = Fact End Function

Приведем процедуру, оценивающую время исполнения рекурсивного и не рекурсивного варианта:

Пример 9.3.

(html, txt)

Вот результаты вычислений, приведенные для двух запусков тестовой процедуры:

Время рекурсивных вычислений: 6,238281 Время нерекурсивных вычислений: 2,304688 Время рекурсивных вычислений: 6,25 Время нерекурсивных вычислений: 2,253906

Как видите, в данном случае не рекурсивный вариант работает почти в три раза быстрее. Кроме проблем со временем исполнения, рекурсивные процедуры могут легко исчерпать и стековую память, в которой размещаются аргументы каждого рекурсивного вызова. Поэтому избегайте неконтролируемого размножения рекурсивных вызовов и заменяйте рекурсивные алгоритмы на итеративные там, где использование рекурсии по существу не требуется.

Польза от рекурсивных процедур в большей мере может проявиться при обработке данных, имеющих рекурсивную структуру (скажем, иерархическую или сетевую). Основные структуры данных (объекты) Office 97 вообще-то не являются рекурсивными: один рабочий лист Excel не может быть значением ячейки другого, одна таблица Access - элементом другой и т.д. Но данные, хранящиеся на рабочих листах Excel или в БД Access, сами по себе могут задавать "рекурсивные" отношения, и для их успешной обработки следует пользоваться рекурсивными процедурами. Мы рассмотрим сейчас класс, для работы с двоичными деревьями поиска. Деревья представляют рекурсивную структуру данных, поэтому и операции над ними естественным образом определяются рекурсивными алгоритмами.



Синтаксис процедур и функций


Описание процедуры Sub в VBA имеет такой вид.

[Private | Public] [Static] Sub имя([список-аргументов]) тело-процедуры End Sub Ключевое слово Public в заголовке процедуры используется, чтобы объявить процедуру общедоступной, т. е. дать возможность вызывать ее из всех других процедур всех модулей любого проекта. Если модуль, в котором описана процедура, содержит закрывающий оператор Option Private, процедура будет доступна лишь модулям своего проекта. Альтернативный ключ Private используется, чтобы закрыть процедуру от всех модулей, кроме того, в котором она описана. По умолчанию процедура считается общедоступной.Ключевое слово Static означает, что значения локальных (объявленных в теле процедуры) переменных будут сохраняться в промежутках между вызовами процедуры (используемые процедурой глобальные переменные, описанные вне ее тела, при этом не сохраняются).Параметр имя - это имя процедуры, удовлетворяющее стандартным условиям VBA на имена переменных.Необязательный параметр список-аргументов - это последовательность разделенных запятыми переменных, задающих передаваемые процедуре при вызове параметры. Заметьте, что аргументы или, как мы часто говорим, формальные параметры, задаваемые при описании процедуры, всегда представляют только имена (идентификаторы). В то же время при вызове процедуры ее аргументы - фактические параметры могут быть не только именами, но и выражениями.Последовательность операторов тело-процедуры задает программу выполнения процедуры. Тело процедуры может включать как "пассивные" операторы объявления локальных данных процедуры (переменных, массивов, объектов и др.), так и "активные" - они изменяют состояния аргументов, локальных и внешних (глобальных) переменных и объектов. В тело могут входить также операторы Exit Sub, приводящие к немедленному завершению процедуры и передаче управления в вызывающую программу. Каждая процедура в VBA определяется отдельно от других, т. е. тело одной процедуры не может включать описания других процедур и функций.


Рассмотрим подробнее структуру одного аргумента из списка-аргументов.
[Optional] [ByVal | ByRef] [ParamArray] переменная[()] [As тип] [= значение-по-умолчанию] Ключевое слово Optional означает, что заданный им аргумент является возможным, необязательным, - его необязательно задавать в момент вызова процедуры. Для таких аргументов можно задать значение по умолчанию. Необязательные аргументы всегда помещаются в конце списка аргументов.Альтернативные ключи ByVal и ByRef определяют способ передачи аргумента в процедуру. ByVal означает, что аргумент передается по значению, т. е. при вызове процедуры будет создаваться локальная копия переменной с начальным передаваемым значением и изменения этой локальной переменной во время выполнения процедуры не отразятся на значении переменной, передавшей свое значение в процедуру при вызове. Передача по значению возможна только для входных параметров, которые передают информацию в процедуру, но не являются результатами. Для таких параметров передача по значению зачастую удобнее, чем передача по ссылке, поскольку в момент вызова аргумент может быть задан сколь угодно сложным выражением. Заметим, что входные параметры, являющиеся объектами, массивами или переменными пользовательского типа, передаются по ссылке, что позволяет избежать создание копий. Выражения над такими аргументами все равно недопустимы, поэтому передача по значению теряет свой смысл.ByRef означает, что аргумент передается по ссылке, т. е. все изменения значения передаваемой переменной при выполнении процедуры будут непосредственно происходить с переменной-аргументом из вызвавшей данную процедуру программы. В VBA по умолчанию аргументы передаются по ссылке (ByRef). Это не совсем удобно для программистов, привыкших к другим языкам (например, Паскалю или С), где по умолчанию аргументы передаются по значению. Поэтому при описании процедуры рекомендуем явно указывать способ передачи каждого аргумента. Отметим также одну интересную особенность, которую не следует использовать, но которую следует учитывать, - VBA допускает, чтобы фактическое значение аргумента, передаваемого по ссылке, было константой или выражением соответствующего типа.


В таком случае этот аргумент рассматривается как передаваемый по значению, и никаких сообщений об ошибке не выдается, даже если этот аргумент встречается в левой части присвоения.Процедура VBA допускает возможность иметь необязательные аргументы, которые можно опускать в момент вызова. Обобщением такого подхода является возможность иметь переменное, заранее не фиксированное число аргументов. Достигается это за счет того, что один из параметров (последний в списке) может задавать массив аргументов, - в этом случае он задается с описателем ParamArray. Если список-аргументов включает массив аргументов ParamArray, ключ Optional использовать в списке нельзя. Ключевое слово ParamArray. может появиться перед последним аргументом в списке, чтобы указать, что этот аргумент - массив с произвольным числом элементов типа Variant. Перед ним нельзя использовать ключи ByVal, ByRef или Optional.Переменная - это имя переменной, представляющей аргумент.Если после имени переменной заданы круглые скобки, то это означает, что соответствующий параметр является массивом.Параметр тип задает тип значения, передаваемого в процедуру. Он может быть одним из базисных типов VBA (не допускаются только строки String c фиксированной длиной). Обязательные аргументы могут также иметь тип определенной пользователем записи или класса. Если тип аргумента не указан, то по умолчанию ему приписывается тип Variant. Ну и, конечно же, в этом мощь VBA, тип может быть одним из типов Office 2000.Для необязательных (Optional) аргументов можно явно задать значение-по умолчанию. Это константа или константное выражение, значение которого передается в процедуру, если при ее вызове соответствующий аргумент не задан. Для аргументов типа объект (Object) в качестве значения по умолчанию можно задать только Nothing.
Синтаксис определения процедур-функций похож на определение обычных процедур:
[Public | Private] [Static] Function имя [(список-аргументов)] [As тип-значения] тело-функции End Function
Отличие лишь в том, что вместо ключевого слова Sub для объявления функции используется ключевое слово Function, а после списка аргументов следует указать параметр тип-значения, определяющий тип возвращаемого функцией значения.


В теле функции должен быть использован оператор присвоения вида:
имя = выражение
Здесь, в левой части оператора стоит имя функции, а в правой - значение выражения, задающего результат вычисления функции. Если при выходе из функции переменной имя значение явно не присвоено, функция возвращает значение соответствующего типа, определенное по умолчанию. Для числовых типов это 0, для строк - строка нулевой длины (""), для типа Variant функция вернет значение Empty, для ссылок на объекты - Nothing.
Чтобы немедленно завершить вычисления функции и выйти из нее, в теле функции можно использовать оператор:
Exit Function
Основное отличие процедур от функций состоит в способе их использования в вызывающей программе. Следующая функция Cube возвращает аргумент, возведенный в куб:
Function cube(ByVal N As Integer) As Long cube= N*N*N End Function
Вызов этой функции может иметь вид
Dim x As Integer, y As Integer y = 2 x = cube(y+3)
Мы уже говорили, что любую функцию можно преобразовать в эквивалентную ей процедуру. Когда функция преобразуется в процедуру, то появляется дополнительный параметр, необходимый для задания результата. Так что у эквивалентной процедуры Cube1 два аргумента:
Sub cube1(ByVal N As Integer, ByRef C As Long) C= N*N*N ' получение результата в переменной, заданной по ссылке End Sub
Ее можно использовать для такого же возведения в куб:
cube1 y+3, x
Эта взаимозаменяемость отнюдь не означает, что безразлично, какой вид процедур использовать в программе. Если бы выражение, в котором участвует функция, было сложнее, например,
x = cube(y)+sin(cube(x))
то его вычисление с помощью процедуры Cube1 потребовало бы выполнения нескольких операторов и ввода дополнительных переменных:
cube1 y,z cube1 x,u x=z+ sin(u)

Создание процедур обработки событий


VBA является языком, в котором, как и в большинстве современных объектно-ориентированных языков, реализована концепция программирования, управляемого событиями (event driven programming). Здесь нет понятия программы, которая начинает выполняться от Begin до End. В противоположность этому есть множество объектов Office 2000, представляющих документы и их компоненты, каждый из которых может реагировать на события. Пользователи системы документов и операционная система могут инициировать события в мире объектов. В ответ на возникновение события операционная система посылает сообщение соответствующему объекту. Реакцией объекта на получение сообщения является вызов процедуры - обработчика события. Задача программиста сводится к написанию обработчиков событий для объектов. Заметьте, все обычные процедуры и функции VBA, о которых мы говорили выше, вызываются прямо или косвенно из процедур обработки событий, если только речь не идет о режиме отладки. Именно эти процедуры являются спусковым крючком, приводящим к последовательности вызовов обычных процедур и функций.

С каждым из объектов Office 2000 связан набор событий, на которые он может реагировать. Процедуры обработки этих событий располагаются в модулях, связанных с объектами, реагирующими на события. Для кнопок меню, у которых есть только один обработчик события, соответствующая процедура может находиться в стандартном модуле или модуле макросов. Office 2000 позволяет при описании собственных классов, создаваемых программистом, задать определенный набор событий. Обработчики событий таких объектов создаются по определенной технологии. Все эти вопросы были уже подробно рассмотрены в предыдущих лекциях. Не будем сейчас повторяться и для напоминания ограничимся лишь простым примером. Вот, например, заготовка для события Close документа Word:

Private Sub Document_Close() End Sub

Дополнив ее:

Private Sub Document_Close() Dim I As Integer

For I = 1 To 5 ' 5 раз подается Beep ' звуковой сигнал Next I End Sub

мы получим процедуру, которая будет 5 раз подавать звуковой сигнал при закрытии документа. Эта процедура находится в модуле, связанном с объектом ThisDocument, и вызывается в момент закрытия документа.



Создание процедуры


Здесь мы рассмотрим создание процедур, текст которых пишется вручную. Чтобы создать новую процедуру, нужно:

открыть в окне проектов "Проект-(VBA)Project" (Project Explorer) папку с модулем (формой, документом, рабочим листом и т. п.), к которому требуется добавить процедуру, и, щелкнув этот модуль, открыть окно редактора с кодами процедур модуля;перейти в редактор, набрать ключевое слово (Sub, Function или Property), имя процедуры и ее аргументы; затем нажмите клавишу Enter, и VBA поместит ниже строку с соответствующим закрывающим оператором (End Sub, End Function, End Property);написать текст процедуры между ее заголовком и закрывающим оператором.

Как правило, следует "автоматизировать" работу, вызвав диалоговое окно "Вставка процедуры" (Insert Procedure). Последовательность действий в этом случае такая:

выбрать в меню Вставка (Insert) команду Процедура (Procedure);в поле Имя (Name) появившегося окна "Вставка процедуры" (Insert Procedure) ввести имя процедуры.указать в группе кнопок-переключателей Тип (Type) тип создаваемой процедуры: Подпрограмма (Sub), Функция (Function) или Свойство (Property);указать в группе кнопок-переключателей "Область определения" (Scope) вид доступа к процедуре: Общая (Public) или Личная (Private);пометить, если нужно, флажок "Все локальные переменные считать статическими" (All Local Variables as Statics), чтобы в заголовок процедуры добавился ключ Static;щелкнуть кнопку OK - в окне редактора появится заготовка процедуры, состоящая из ее заголовка (без параметров) и закрывающего оператора;добавить параметры в заголовок процедуры и написать текст процедуры между ее заголовком и закрывающим оператором.



Вызовы функций


Оформление вызова функции зависит от того, требуется ли использовать ее значение в вызывающей процедуре. Если Вы хотите передать вычисляемое функцией значение в переменную или применить его в выражении правой части оператора присвоения, то вызов пользовательской функции имеет тот же вид, что и вызов встроенной функции, например, sin(x). При вызове указывается имя функции, а после него идет заключенный в круглые скобки список фактических параметров. Например, если заголовок функции MyFunc:

Func Myfunc(Name As String, Age As Integer, Newdate As Date) As Integer

использовать ее значение можно с помощью вызовов:

val= Myfunc("Alex",25, "10/04/97")

или

x = sqrt(Myfunc("Alex",25, "10/04/97")) + x

Если же значение, вычисляемое функцией, нас не интересует и нужно воспользоваться лишь ее побочными эффектами, вызов функции может иметь ту же форму, что и вызов процедуры Sub. Например:

Myfunc "Alex",I, "10/04/97"

или:

Call Myfunc(Myson, 25, DateOfArrival)



Вызовы процедур Sub


Вызов обычной процедуры Sub из другой процедуры можно оформить по-разному. Первый способ:

имя список-фактических-параметров

где имя - имя вызываемой процедуры, а список-фактических-параметров - список аргументов, передаваемых процедуре; он должен соответствовать списку аргументов, заданному в описании процедуры. Задать этот список можно разными способами. В простейшем случае в нем перечисляются через запятую значения передаваемых процедуре аргументов в том же порядке, что и в списке аргументов из заголовка процедуры.

Может оказаться, что в одном проекте несколько модулей содержат процедуры с одинаковыми именами. Для различения этих процедур нужно при их вызове указывать имя процедуры через точку после имени модуля, в котором она определена. Например, если каждый из двух модулей Mod1 и Mod2 содержит определение процедуры ReadData, а в процедуре MyProc нужно воспользоваться процедурой из Mod2, этот вызов имеет вид:

Sub Myproc() ... Mod2.ReadData ... End Sub

Если требуется использовать процедуры с одинаковыми именами из разных проектов, добавьте к именам модуля и процедуры имя проекта. Например, если модуль Mod2 входит в проект MyBook, тот же вызов можно уточнить так:

MyBooks.Mod2.ReadData

Второй способ вызова процедур связан с использованием оператора Call. В этом случае вызов процедуры выглядит так:

Call имя(список-фактических-параметров)

Обратите внимание на то, что в этом случае список-фактических-параметров заключен в круглые скобки, а в первом случае - нет. Попытка вызывать процедуру без оператора Call, но с заданием круглых скобок является источником синтаксических ошибок особенно для разработчиков с большим опытом программирования на Паскале или С, где списки параметров всегда заключаются в скобки. Следует обратить внимание на одну важную и, пожалуй, неприятную особенность вызова процедур VBA. Если процедура VBA имеет только один параметр, то она может быть вызвана без оператора Call и с использованием круглых скобок, не сообщая об ошибке вызова. Это было бы не так страшно, если бы возвращался правильный результат.
К сожалению, это не так, проиллюстрируем сказанное примером:

Public Sub MyInc(ByRef X As Integer) X = X + 1 End Sub

Public Sub TestInc() Dim X As Integer X = 1 'Вызов процедуры с параметром, заключенным в скобки, 'синтаксически допустим, но работает не корректно! MyInc (X) Debug.Print X

'Корректный вызов MyInc X Debug.Print X

'Это тоже корректный вызов Call MyInc(X) Debug.Print X

End Sub

Вот результаты ее работы:

1 2 3

Хотя при первом вызове процедура нормально вызывается и увеличивает значение результата, но по завершении ее работы значение аргумента не изменяется. В этой ситуации не действует описатель ByRef, вызов идет так, будто параметр описан с описателем ByVal.

Если же процедура имеет более одного параметра, то попытка вызвать ее, заключив параметры в круглые скобки и не предварив этот вызов ключевым словом Call, приводит к синтаксической ошибке. Вот простой пример:

Public Sub SumXY(ByVal X As Integer, ByVal Y As Integer, ByRef Z As Integer) Z = X + Y End Sub

Public Sub TestSumXY() Dim a As Integer, b As Integer, c As Integer a = 3: b = 5 'SumXY (a, b, c) 'Синтаксическая ошибка SumXY a, b, c Debug.Print c

End Sub

В этом примере некорректный вызов процедуры SumXY будет обнаружен на этапе проверки синтаксиса.

Рассмотрим еще одну особенность вызова VBA процедур, связанную с аргументами, передаваемыми по ссылке. Как правило, в языках программирования для таких аргументов возможное значение фактического параметра ограничивается, - он должен быть именем переменной, ссылка на которую передается процедуре. VBA допускает возможность задания для таких аргументов констант и выражений в момент вызова. Все эти допущения делают язык менее надежным и чреваты серьезными ошибками. Именно поэтому мы останавливаем на них Ваше внимание.

Пусть, например, процедура CompVal c 4 аргументами, которая в зависимости от положительности z возвращает в переменной y либо увеличенное, либо уменьшенное на 100 значение x и сообщает об этом в строковой переменной w, определена следующим образом.



Sub CompVal(ByVal x As Single, ByRef y As Single, _ ByVal z As Integer, ByRef w As String)

If z > 0 Then ' увеличение y = x + 100 w = "increase" Else ' уменьшение y = x - 100 w = "decrease" End If End Sub

Рассмотрим процедуру TestCompVal, в которой несколько раз вызывается процедура CompVal:

Sub TestCompVal() Dim a As Single Dim b As Single Dim n As Integer Dim S As String

n = 5: a = 7.4 ' значения параметров CompVal a, b, n, S ' 1-ый вызов Debug.Print b, S

CompVal 7.4, b, 5, S ' 2-ой вызов Debug.Print b, S

CompVal 0, 0, 0, S ' 3-ий вызов Debug.Print b, S

CompVal 0, 0, 0, "В чем дело?" ' 4-ый вызов Debug.Print b, S End Sub

В результате выполнения этой процедуры будут напечатаны следующие результаты:

107,4 increase 107,4 increase 107,4 decrease 107,4 decrease

Первые два вызова корректны. Следующие два вызова хотя и допустимы в языке VBA, но приводят к тому, что параметры, переданные по ссылке, не меняют своих значений в ходе выполнения процедуры и, по существу, вызов ByRef по умолчанию заменяется вызовом ByVal. Конечно, было бы лучше, если бы эта программа выдавала ошибки на этапе проверки синтаксиса.


Задача о медиане


Для массива M и элемента Cand вычислить разность между числом элементов массива M, больших и меньших Cand.

Это вариация задачи о медиане - "среднем" элементе - массива. Медиану можно определить, например, таким алгоритмом: упорядочив массив, взять элемент, находящийся в середине. Есть и более эффективные алгоритмы. Но мы решили ограничиться более простой задачей - проверкой на "медианность". Заметим: если все элементы массива M различны и число их нечетно, то для медианы искомая в задаче разность равна 0. В общем случае, значение разности является мерой близости параметра Cand к медиане массива M. Но займемся программистскими аспектами этой задачи. У функции, ее реализующей, на входе - массив, а на выходе - скаляр. Мы хотели бы, чтобы эта функция могла вызываться в формулах рабочего листа, а в качестве фактического параметра ей могли быть переданы как объект Range, так и массив Visual Basic. Вот как мы реализовали эту функцию, назвав ее IsMediana:

Пример 9.1.

(html, txt)

Прокомментируем работу функции IsMediana.

Функция IsMediana может (и будет) вызываться как из процедур VBA, так и из рабочих формул листа Excel. Обратите внимание, она работает с объектами Office 2000 - Range, Cells, Rows и другими.Функции, чьи аргументы имеют универсальный тип Variant, целесообразно строить по принципу разбора случаев. Алгоритм обработки зависит от типа фактического параметра, задаваемого в момент вызова.Стандартная функция TypeName(V) возвращает в качестве результата конкретный тип параметра V.Работа функции IsMediana(M,Cand) начинается с вызова TypeName(M). Далее разбираются четыре возможных случая: M - объект Range, M - массив типа Variant(), M - настоящий целочисленный массив VBA, M имеет любой другой тип.В первом случае функция IsMediana вызывается в формуле рабочего листа Excel и в качестве фактического параметра ей передается объект Range - интервал ячеек этого листа. Следовательно, функция TypeName возвратит строку "Range" в качестве результата.
При обработке этого случая организуется цикл по числу строк и столбцов объекта Range, используя свойство Cells этого объекта.Во втором случае обработка основана на том, что функции передан массив типа Variant(). Это возможно, когда при вызове нашей функции в формуле рабочего листа ей передается константа, задающая массив. Ниже мы приведем примеры подобного вызова. Для таких массивов не определены функции границ UBound и LBound. Поэтому обработка в этом случае основана на использовании цикла For Each.В третьем случае функция получает при вызове обычный массив VBA и обработка идет стандартным для массивов способом. Мы приведем пример вызова нашей функции из обычной процедуры VBA, передающей в момент вызова целочисленный массив.В четвертом случае, когда наш параметр не является ни массивом, ни объектом Range, в качестве результата по умолчанию выдается 0. Но выдается также и окно сообщений с предупреждением о возникшей ситуации.
Начнем с того, что приведем процедуру VBA, вызывающую нашу функцию. Вот ее текст:
Public Sub TestIsMediana() Const Size = 7 Dim Mas(1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMediana(Mas, Cand) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub
Вот результаты ее работы:
Массив: 3 8 14 0 3 8 2 Кандидат: 2 Результат: 4
В данном варианте вызове анализ типа переданного параметра показал, что он является обычным массивом, соответственно был выбран третий вариант обработки, не требующий работы с объектами Office 2000.
Теперь покажем, что эту же функцию можно вызывать в формулах рабочего листа Excel, передавая ей в момент вызова объекты Range в разной форме, а также массивы, заданные константой - массивом. Посмотрим, как это выглядит на экране, и разберем примеры нескольких различных вызовов функции IsMediana в формулах рабочего листа:



увеличить изображение
Рис. 9.1.  Вызов функции IsMediana в формулах рабочего листа
На рабочем листе мы сформировали два массива: вектор M, вытянутый в виде столбца, и прямоугольную матрицу N. Вектор M записан в ячейках C6:C11, матрица N - в F5:I6. В ячейки E8:E15 мы поместили формулы, вызывающие функцию IsMediana. Они не являются формулами над массивами, несмотря на то, что параметром может быть массив рабочего листа. Важно, что результат - скаляр. Если бы результат, возвращаемый функцией, был массивом, формулу следовало бы вызывать как формулу над массивами. Для скалярного результата это не так.
В двух первых вызовах функции IsMediana (в ячейках E8, E9) передается в качестве параметров имя массива рабочего листа "M" и разные кандидаты: 7 и 8. Они оба годятся на роль медианы этого массива. В следующих двух вызовах проверяются кандидаты на медиану массива N. Как видите, оба кандидата 4 и 3 одинаково близки к медиане. Следующие два вызова в ячейках E12 и E13 демонстрируют возможность указания непосредственно диапазона ячеек в момент вызова, что позволяет, например, работать с частью массива. В следующем вызове вообще не используются в качестве входных данных элементы рабочего листа. Входным параметром M является массив - константа, заключенный в фигурные скобки, а его элементы разделяются символом ";". В этих случаях фактический параметр уже не является объектом Range, а имеет тип массива с элементами Variant. Поэтому и функция IsMediana будет работать по-другому, в отличие от предыдущих вызовов. Разбор случаев в зависимости от результата, возвращаемого функцией TypeName, приведет к выбору второго варианта. Наконец, вызов, записанный в формуле из ячейки E15, демонстрирует случай, когда входной параметр M - обычное число и, следовательно, не является ни объектом Range, ни массивом. Как следствие, разбор случаев в функции IsMediana приводит к четвертому варианту и появлению на экране окна сообщений.


Следующие два вызова в ячейках E12 и E13 демонстрируют возможность указания непосредственно диапазона ячеек в момент вызова, что позволяет, например, работать с частью массива. В следующем вызове вообще не используются в качестве входных данных элементы рабочего листа. Входным параметром M является массив - константа, заключенный в фигурные скобки, а его элементы разделяются символом ";". В этих случаях фактический параметр уже не является объектом Range, а имеет тип массива с элементами Variant. Поэтому и функция IsMediana будет работать по-другому, в отличие от предыдущих вызовов. Разбор случаев в зависимости от результата, возвращаемого функцией TypeName, приведет к выбору второго варианта. Наконец, вызов, записанный в формуле из ячейки E15, демонстрирует случай, когда входной параметр M - обычное число и, следовательно, не является ни объектом Range, ни массивом. Как следствие, разбор случаев в функции IsMediana приводит к четвертому варианту и появлению на экране окна сообщений.

Циклы


Когда требуется уменьшить размеры используемой памяти, нужно начинать с чистки массивов. Когда требуется уменьшить время выполнения программы, нужно начинать с чистки циклов. Особо тщательно нужно оптимизировать выполнение самых внутренних циклов. Каждая миллисекунда, сэкономленная на выполнении внутреннего цикла, может обернуться сэкономленными часами на выполнении всей программы. Вот несколько советов, полезных при работе с циклами:

Все вычисления, которые могут быть сделаны вне цикла, должны быть вынесены из него.Предусматривайте возможность досрочного завершения цикла, когда решение задачи уже получено.Аккуратно работайте в цикле с элементами массивов. Старайтесь избегать лишнего вычисления индексных выражений и обращений к элементам массива. Часто введение дополнительных переменных позволяет существенно ускорить работу с элементами массивов.При работе с коллекциями объектов Office 2000 используйте цикл For Each вместо обычного цикла For. В большинстве случаев это приводит к существенному выигрышу во времени исполнения цикла.

Завершая эту лекцию, хочу еще раз сказать, что главная оптимизация достигается на уровне проектных решений, выбора структуры данных и алгоритмов. К ниже перечисленным советам следует относиться с определенной осторожностью. Оптимизация не должна идти во вред ясности написания программ. Не всегда в ней есть необходимость. Но если такая необходимость возникает, то, в первую очередь, нужно выявить узкие места в программе и их, в основном, и оптимизировать.



Директива #const


Эта директива позволяет задать константы условной компиляции. Ее синтаксис:

#Const constname = expression

Имя константы строится по обычным правилам, а выражение в правой части может содержать только литералы (символьные константы) и другие константы условной компиляции, соединенные знаками арифметических и логических операций за исключением операции Is. Эти константы, являются флажками и используются в операторе #If



Доказательство правильности программ


Мы уже говорили, что отладка, основанная на построении системы тестов, не может доказать правильность программы. Поэтому в теоретическом программировании были предприняты большие усилия по разработке методов доказательства правильности программ, такие же строгие, как и методы доказательства правильности теорем. На практике эти методы не получили широкого распространения по двум причинам. Во-первых, построить доказательство правильности программы сложнее, чем написать саму программу. Во-вторых, ошибки в доказательстве столь же возможны, как и в самой программе. Тем не менее, знание основ доказательства правильности программ должно быть частью образования программиста. Умение строго доказывать правильность простых программ помогает программисту лучше понять, как следует разрабатывать корректно работающие, сложные программы. Не ставя целью сколь либо полный обзор этого важного направления, остановимся лишь на самом понятии правильности программы. Действительно, мы многократно использовали этот термин, но что значит правильно (корректно) работающая программа? Вот одно из возможных определений. Пусть 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 - утверждений является отражением метода Флойда. Хотя использование этих утверждений не предполагает проведения строгого доказательства, но это практически реализуемая попытка в этом направлении. Все что может быть сделано для повышения надежности программы, должно быть сделано.



Функция CvErr


При работе с процедурами стандартного модуля есть еще один способ для возврата кодов ошибок, определенных пользователем. Для этой цели можно использовать функцию 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 можно так.

Пример 10.6.

(html, txt)

Приведем результаты вычислений:

Результат : 144 Ошибка #: Error 1999 аргумент : двенадцать



#If … Then … #Else директива


Оператор 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



Искусство отладки


Прелесть работы программиста во многом связана с отладкой. Почему программисты нарушают известные им требования, - не задают комментарии, не описывают детально суть решаемой задачи и не следуют другим полезным советам. Чаще всего, причина в нетерпении, им хочется скорее посмотреть, как же работает программа, увидеть результаты ее работы. Отладка - это некоторый детективный процесс. Вновь созданную программу мы подозреваем в том, что она работает не корректно. Презумпция невиновности здесь не работает. Если удается предъявить тест, на котором программа дает неверный результат, то доказано, что наши подозрения верны. Втайне мы всегда надеемся, что программа заработает правильно с первого раза. Но цель тестирования другая, - попытаться опровергнуть это предположение. И только потом, исправив все выявленные ошибки, получить корректно работающую программу. К сожалению, отладка не может гарантировать, что программа корректна, даже если все тесты прошли успешно. Отладка может доказать некорректность программы, но она не может доказать ее правильности.

Искусство тестера состоит в том, чтобы создать по возможности полную систему тестов, проверяющую все возможные ветви вычислений. Поясним это на самом простом примере. Пусть программа находит сумму первых N элементов массива X, содержащего M элементов. Кроме "нормального" теста, проверяющего ситуацию, в которой 1<N<M, следует проверить и крайние случаи: N=1, N=M, N=0, N<0, N>M. Но это простой случай, а циклы обычно вложенные, и внутри них производится разбор случаев, внутри которых свои циклы.

Ранее мы упоминали закон "чечако" - новичок может подвесить любую систему. Этому есть объяснение, по незнанию, он задаст одно из мало вероятных сочетаний входных данных (работая в визуальной среде, нажмет самую неподходящую для данной ситуации кнопку). Поэтому тестер, ведущий отладку, должен уметь встать и на позицию новичка, система тестов должна гарантировать, что программа корректно работает не только в "нормальных ситуациях", но и имеет "защиту от дурака" и не приведет к зацикливанию или останову в крайних, мало вероятных ситуациях.

Сложность отладки заключается и в том, что, обнаружив и исправив ошибку, вы получаете новую программу, для которой процесс отладки нужно начинать заново, снова пропустив все тесты. Известно, что в программах встречаются заколдованные места, - исправление одной ошибки ведет к появлению новой. В таких случаях лучшим выходом бывает поиск другого, принципиально иного решения задачи.



Класс и обработка ошибок


Мы уже говорили, что важная часть работы программиста по отладке программы состоит в том, чтобы предохранить работающую программу от прерывания ее работы при возникновении внутренних ошибок. Не менее важно, особенно при работе с объектами пользовательских классов, предусмотреть возможность появления исключительных ситуаций, генерировать и соответственно обрабатывать собственные ошибки класса. В качестве примера приведем класс Day, у которого есть два свойства - дата и температура на эту дату. Методы класса, (их реализацию мы не приводим) исходят из того, что летом должно быть жарко, а зимой - холодно. Нарушение этого условия приведет к неверным результатам. Поэтому в состав класса включен метод CheckDay, проверяющий корректность задания свойств. В случае несоблюдения требуемых условий метод генерирует собственные ошибки класса. Вот описание нашего класса:

Пример 10.4.

(html, txt)

Приведем теперь процедуру, работающую с объектами, этого класса:

Пример 10.5.

(html, txt)

Заметьте, эта процедура, работающая с объектом myday класса Day, построена по всем правилам, - в ней есть охраняемый блок. При возникновении ошибки, а она действительно возникает из-за некорректного задания свойств объекта, производится захват ошибки, управление передается предусмотренному обработчику ошибки. В обработчике пользователю разъясняется суть ситуации, приведшей к ошибке, после чего он вводит корректные данные. Взгляните, как выглядит окно для диалога с пользователем на этом этапе:


Рис. 10.17.  Окно диалога с пользователем

После того, как пользователь задал нормальную летнюю температуру, процедура нормально завершила свою работу. В окне проверки напечатаны следующие результаты:

09.08.99 25


Приведем теперь процедуру, работающую с объектами, этого класса:

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

Пример 10.5.

Заметьте, эта процедура, работающая с объектом myday класса Day, построена по всем правилам, - в ней есть охраняемый блок. При возникновении ошибки, а она действительно возникает из-за некорректного задания свойств объекта, производится захват ошибки, управление передается предусмотренному обработчику ошибки. В обработчике пользователю разъясняется суть ситуации, приведшей к ошибке, после чего он вводит корректные данные. Взгляните, как выглядит окно для диалога с пользователем на этом этапе:


Рис. 10.17.  Окно диалога с пользователем

После того, как пользователь задал нормальную летнюю температуру, процедура нормально завершила свою работу. В окне проверки напечатаны следующие результаты:

09.08.99 25


Математические операции


При работе с целыми используйте целочисленные операции, - деление нацело, взятие остатка и другие, не выводящие за пределы целых чисел.В выражениях, где одновременно присутствуют целые числа и числа с плавающей точкой перед проведением операций всегда происходит преобразование к типам с плавающей точкой Single и Double. Для уменьшения подобных преобразований, записывайте выражения так, чтобы пересечение переменных целого и плавающего типа было , по возможности, минимальным.



Метод Assert


Его синтаксис:

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, привела бы к останову выполнения программы.



Метод Clear


Его синтаксис:

Err.Clear

Этот метод используется для явной очистки значений свойств объекта Err после завершения обработки ошибки. Автоматическая очистка свойств Err происходит также при выполнении операторов:

оператора Resume любого вида;Exit Sub, Exit Function, Exit Property;оператора On Error любого вида.



Метод Print


Его синтаксис:

Debug.Print список_выражений

Метод позволяет во время выполнения программы напечатать значения выражений из списка выражений в окне проверки Immediate. Трудно представить программу в период отладки, в которой бы многократно не использовался бы метод Print объекта Debug. Во всех наших примерах демонстрировалось его применение. Мы не будем останавливаться на некоторых деталях синтаксиса и управления выводом, при желании познакомиться с ними можно обратиться к справке.



Метод Raise


Метод 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.



Модель управления ошибками в языке VBA.


В языке VBA обработка ошибок сосредоточена на уровне процедуры (функции). В каждой процедуре может быть выделен один или несколько охраняемых блоков, с каждым из которых связывается свой обработчик ошибки. Если во время работы охраняемого блока возникла ошибка (исключение), то нормальный ход выполнения процедуры приостанавливается, управление ее работой перехватывается и передается обработчику ошибки. Стандартный объект Err содержит информацию об ошибке. Поэтому в обработчике ошибки имеется возможность обработать возникшую ситуацию, исправить ее, запросив, например, у пользователя дополнительные данные, и принять правильное решение о дальнейшем ходе выполнения программы. В некоторых случаях, когда устранена причина ошибки или ее последствия, управление может быть возвращено в охраняемый блок, так что вычисления будут продолжены. В некоторых случаях работа программы приостанавливается с выдачей пользователю вразумительного объяснения причин, приведших к невозможности дальнейшего выполнения пр ограммы.

Давайте разберемся, что значит "возникла ошибка"? Точнее следует говорить возбуждена (raise) ошибка. Кто обнаруживает ошибку? Обнаружение исключительной ситуации и возбуждение ошибки может быть сделано самой операционной системой (VBA) или исполняемой процедурой. Ошибки, возбуждаемые операционной системой, могут быть следствием аппаратных прерываний, например, из-за деления на ноль, вычисления корня из отрицательного числа, но это могут быть и ошибки, программно обнаруживаемые операционной системой, например, при попытке открыть несуществующий файл. Все эти ошибки будем называть системными или внутренними ошибками VBA. Все они тщательно классифицированы и каждая из них однозначно идентифицируется своим номером. Другую группу ошибок составляют собственные или пользовательские ошибки, возбуждение которых предусматривает программист. Например, при работе с объектом пользовательского класса программист может и должен предусмотреть специальную процедуру Check, которая проверяет правильность задания свойств объекта.
Если обнаруживается, что свойства объекта заданы некорректно, так что выполнение операций над ним приведет к неверным результатам, то возбуждается собственная ошибка. Конечно, также как и для стандартных ошибок, ее тип должен быть полностью определен, задан ее номер и другие параметры. Возможно, программное обнаружение исключительных ситуаций и возбуждение собственных ошибок это наиболее важная и наиболее трудная часть программистской работы по управлению ошибками. Заметим, что какие бы ошибки не возбуждались, - внутренние или пользовательские, в момент возбуждения ошибки заполняются свойства объекта Err, так что он содержит всю информацию о последней возникшей ошибке.

Синтаксически охраняемый блок окружен специальными операторами On Error. В начале блока оператор On Error задает метку обработчика ошибки охраняемого блока. Обработчик ошибок, как правило, завершается специальным оператором Resume, который задает точку в процедуре, которой передается управление после завершения обработки ошибки. Приведем схему процедуры с тремя охраняемыми блоками:

Пример 10.2.

(html, txt)

Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой охраняемый блок может содержать вызовы процедуры процедур и функций. Поэтому реальная ситуация обычно такова, - один из операторов охраняемого блока запускает цепочку вызовов процедур и функций, каждая из которых имеет свои охраняемые блоки и свои обработчики ошибок. Ошибка может произойти на каком-то шаге в одной из вызванных процедур. Какие обработчики будут вызываться и в каком порядке, об этом поговорим чуть позже. Чтобы разобраться с деталями, вначале стоит подробно рассмотреть возможности используемых средств - операторов On Error, Resume и объекта Err с его свойствами и методами.



ErrHandler3: ' 3-ий обработчик ошибок ... Resume RepeatPoint 'переход к строке, с которой возобновляется 'выполнение после обработки ошибки в 3-ей части End Sub

Пример 10.2.

Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой охраняемый блок может содержать вызовы процедуры процедур и функций. Поэтому реальная ситуация обычно такова, - один из операторов охраняемого блока запускает цепочку вызовов процедур и функций, каждая из которых имеет свои охраняемые блоки и свои обработчики ошибок. Ошибка может произойти на каком-то шаге в одной из вызванных процедур. Какие обработчики будут вызываться и в каком порядке, об этом поговорим чуть позже. Чтобы разобраться с деталями, вначале стоит подробно рассмотреть возможности используемых средств - операторов On Error, Resume и объекта Err с его свойствами и методами.


Написание надежных программ


Ошибки неизбежно сопровождают всякую сколь-нибудь сложную программу. Это утверждение может вызвать раздражение у начинающего программиста, но человек более опытный посчитает его само собой разумеющимся и сосредоточится на средствах, предлагаемых программным окружением для борьбы с этим неизбежным злом. Самые неприятные, дорогостоящие ошибки это те, что допущены при определении основных задач и целей приложения, при проектировании структуры его управления и потоков передачи данных, а также ошибки, связанные с неверной реализацией алгоритмов. Часто они не проявляются непосредственно в виде сбоев в работе программы, а обнаруживаются после довольно длительного использования приложения и требуют для своего исправления существенных изменений в проекте и программе. Их корни могут иметь как объективную природу (сложность решаемых задач), так и субъективную (непонимание заказчиком того, что ему надо). Как бороться с такими ошибками? Выделим два известных подхода к этой проблеме. Первый из них состоит в повышении ур овня языков и систем программирования, чтобы разработчик мог оперировать при создании системы понятиями предметной области, для которой она создается. Другой подход связан с идеей быстрого прототипирования, т. е. создания на ранней стадии разработки системы ее работающего прототипа, в ходе экспериментов с которым заказчик может уточнить свои требования. Оба эти подхода нашли отражение в Office 2000. Первый реализован в объектно-ориентированном подходе к построению как самой инструментальной системы, так и создаваемых в ней приложений. Развитие в VBA возможностей пользовательских модулей классов, в частности введение полиморфизма, - еще один шаг в направлении повышения уровня языка. Благодаря классам, можно вводить в программы объекты, представляющие те или иные понятия предметных областей. Реализации второго из указанных подходов существенно способствует визуальный стиль программирования, принятый во всех приложениях Office 2000. Создавая собственные меню и диалоговые окна, можно быстро спроектировать интерф ейс системы и передать пользователю (тестерам) действующий ее прототип для оценки соответствия его требованиям.
Затем завершить реализацию с учетом замечаний и уточнений, возникших у тестеров при работе с прототипом. Это позволит избежать многих ошибок, неизбежных при разработке системы без контактов с ее будущими пользователями.

Одним из факторов, влияющих на надежность программ, является сам язык программирования. Известно, что язык, в котором есть объявление переменных по умолчанию, разрешены преобразования данных по умолчанию в процессе вычислений, нет строгого контроля типов, - такой язык является ненадежным, в нем значительно легче создать ненадежную программу, содержащую трудно выявляемую ошибку. Язык VBA трудно причислить к надежным языкам, он скорее занимает по шкале надежности срединное положение. Во многом, это связано с историей его возникновения. Именно поэтому надо предпринимать ряд мер, благоприятствующих повышению надежности. О многих из них мы уже говорили в разных частях этой книги. Напомним некоторые из них:

Проследите, чтобы все флажки на вкладке Editor из меню Tools|Options были включены. Автоматическая проверка синтаксиса в процессе написания программ, подсказка о значениях переменных, подсказка о параметрах функции, - все подключаемые свойства крайне полезны. Особое внимание обращаем на флажок "Require Variable Declaration", при включении которого в каждый модуль вставляется опция Option Explicit, принуждающая явно объявлять все переменные. С этим включенным флажком у языка VBA становится одним недостатком меньше.При объявлении переменных старайтесь указать точный тип переменной и объекта. Избегайте объявлений типа Variant и Object. В этом случае на Вашей стороне будет контроль типов, что позволит избежать многих возможных ошибок.При объявлении процедур явно указывайте описатели ByRef и ByVal, помните об особенностях передачи аргументов по ссылке в VBA.Не забывайте о разумных размерах модулей и процедур, старайтесь создавать процедуры, модули и компоненты, допускающие переиспользование.Наконец, еще раз напомним, что хорошие спецификации залог того, что программа допускает возможность изменения в процессе жизненного цикла, и что она будет корректно работать у конечного пользователя.Поэтому комментарии в тексте программы, хорошая справочная система, - все это важнейшие факторы, повышающие надежность программ.


Рис. 10.1.  Флажки вкладки Editor


Объект Debug и его методы


Объект Debug специально сконструирован для проведения отладки. Он имеет два метода Print и Assert.



Объект Err


Объект Err содержит информацию о последней ошибке выполнения. Объект Err является внутренним объектом с глобальной областью определения. Нет необходимости в создании этого объекта. Он создается вместе с проектом. Вот список свойств объекта Err и их значений:

Таблица 10.1. Описание свойств объекта 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 первый раз корректно, второй - нет, что приведет к ошибке, ее перехвату и исправлению ситуации:

Пример 10.3.

(html, txt)

Вот как выглядит окно сообщения, выведенное в обработчике ошибки.


Рис. 10.16.  Сообщение, сформированное в обработчике ошибки

Заметьте, после выдачи сообщения процедура нормально завершает свою работу и в окне проверки Immediate появятся следующие результаты:

5 600 7 25200

Объект Err специально спроектирован для работы на этапе обнаружения и исправления ошибок периода выполнения. Он заменил ранее существовавшие функцию и оператор Err. Для совместимости с ними свойство Number реализовано, как свойство по умолчанию и его можно не указывать. Если в борьбе с ошибками на этапе отладки важную роль играет объект Debug, то не менее важна роль объекта Err при борьбе с ошибками периода выполнения. Также как и объект Debug, объект Err имеет всего два метода - Clear и Raise. Рассмотрим их подробнее.


Объявление переменных


Для уменьшения размеров требуемой памяти и ускорения выполнения операций над данными:

При работе с большими массивами данных, размер которых, как правило, может изменяться, используйте динамические массивы или динамические структуры данных.Объявляйте переменные в строгом соответствии с их возможным типом. Не используйте без крайней необходимости тип Variant. Работа с Variant требует больше памяти и дополнительных преобразований.При задании типа, как правило, используйте тип с более узкой областью определения, достаточной для представления возможных значений. Здесь могут быть исключения, специфические для VBA. Так переменные типа Integer преобразуются к типу Long, так что разумнее задавать сразу тип Long вместо типа Integer.Операции с плавающей точкой выполняются более долго, поскольку требуют обращения к сопроцессору. Поэтому тип Single и Double следует использовать только в случае действительной необходимости. Иногда, можно воспользоваться типом Currency для выполнения подобных действий. В этом случае потребуется больше памяти, но не будет обращений к сопроцессору с плавающей точкой.При работе с объектами следует вводить переменные соответствующего объектного типа. Они хранят ссылки на объекты, а работа со ссылками выполняется значительно быстрее, чем с самими объектами.При объявлении объектных переменных избегайте объявления Object, указывайте явный тип объекта. Типы Object и Variant весьма полезные типы, но применять их нужно только в случае действительной необходимости.

В одном из примеров, я приводил таблицу временных измерений выполнения арифметических операций над данными разных типов. Рекомендую провести подобные замеры в более полном объеме.



Обработчики ошибок и вложенные вызовы процедур


К каждой процедуре может быть подключено несколько обработчиков ошибок. Этот факт был специально отражен в схеме процедуры, когда в этой лекции описывалась модель управления ошибками. При возникновении ошибки в процедуре только один из них может быть активным. Активным обработчиком является тот обработчик, который связан с охраняемым блоком, в теле которого возникла ошибка. Заметьте, несмотря на то, что подключенных обработчиков в процедуре может быть несколько, в момент возникновения ошибки в ней может не быть активного обработчика, если ошибка возникла вне охраняемых блоков.

В процессе вычислений одни процедуры могут вызывать другие. Поэтому в момент возникновения ошибки в стеке вызовов процедур могут находиться несколько процедур: C1, C2, …Cn. Каждая из этих процедур может иметь активный обработчик ошибок. Какой же из них будет применяться для обработки ошибки? Рассмотрим применяемую стратегию обработки. Итак, пусть есть непустой стек вызовов C1, C2, …Cn, где C1 это самый внешний вызов, а Cn - самый внутренний вызов. Обработка начинается подъемом по стеку вызовов. Если в Cn имеется активный обработчик ошибки, то он и получает управление, если его нет, то в стеке проверяется следующий по порядку вызов. Если ни один из вызовов C1 - Cn не имеет активного обработчика, то выполняется стандартная обработка с выдачей сообщения об ошибке и снятия приложения. Пусть Ck - это первый, найденный в стеке вызов, для которого сущест вует активный обработчик, и который, как было сказано, получает управление. Обработчик Ck имеет две возможности:

Обработать ошибку.Передать обработку ошибки обработчику, выше стоящему в стеке вызовов.

Во втором случае, подъем по стеку вызовов может быть продолжен, пока его не захватит обработчик, способный обработать эту ошибку, или управление не попадет последнему исполняемому стандартному обработчику. Заметим, что хорошим методическим приемом является введение некоторого универсального обработчика, который бы обрабатывал все непредусмотренные другими обработчиками ошибки. Например, такой обработчик мог бы вести журнал ошибок.

Давайте теперь разберемся с тем, какова должна быть типичная структура разработчика ошибок, чтобы он мог выполнять обе возложенные на него функции.



Окна наблюдения


Мы хотим теперь подробнее остановиться на окнах отладчика, позволяющих следить за состоянием вычислительного процесса. Чтобы сделать изложение конкретным, обратимся к примеру предыдущей лекции, в котором показана работа с бинарным деревом, задающим словарь. Единственное сделанное добавление связано с введением в модуле глобальной переменной GlobeVar. Приведем текст отлаживаемой процедуры:

Пример 10.1.

(html, txt)



Окно контрольных выражений - Watch


Привычное назначение окна 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 или выбрав инструментальную кнопку или команду "Call Stack…" в меню View. Чаще всего, в окно "Стек вызова" попадают из окна Locals, щелкнув в нем имеющуюся для этой цели кнопку "…".

Продолжим работу с нашим примером. Мы остановились в момент выполнения оператора, запускающего обход дерева. В ходе отладки мы ввели некоторые контрольные выражения. Продолжим теперь выполнение нашей программы. Очередное прерывание в ее работе наступило вследствие того, что изменилось значение контрольного выражения, переменная info получила новое значение в процессе работы процедуры поиска SearchAndInsert. Взгляните на соответствующее состояние окон отладчика в этот момент:


увеличить изображение
Рис. 10.12.  Окна отладчика в момент прерывания

Заметьте, полученной информации достаточно, чтобы понять в какой процедуре произошло изменение значения переменной info. Но эта процедура рекурсивна и вызывалась неоднократно. Поэтому хотелось бы знать, на каком шаге ее вызова произошло изменение контрольной переменной. Для этого и служит стек вызовов, окно которого и было открыто щелчком соответствующей кнопки в окне Locals.


Рис. 10.13.  Окно стека вызовов

Три вызова функции SearchAndInsert были выполнены, прежде чем изменилось значение контрольной переменной. К сожалению, в окне стека вызовов не указаны параметры соответствующих вызовов. Но получить информацию об их значениях и о состоянии локальных переменных вызовов можно. Для этого нужно выделить интересующий вызов в стеке и щелкнуть по кнопке "Show".В результате в окне Locals ,будут отображены значения локальных переменных в момент выделенного вызова.

Разговор о средствах отладки будет не полным, если не рассмотреть специальный объект Debug.


Окно локальных переменных - Locals


Наиболее простой и эффективный способ следить за состоянием вычислений заключается в том, чтобы во время отладки включить окно локальных переменных (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, к рассмотрению которого мы и переходим.


Окно проверки - Immediate


В этом окне появляется вся отладочная информация, поступающая в результате выполнения методов Print и Assert объекта Debug. В этом основное назначение этого окна. Об объекте Debug мы еще поговорим особо, а сейчас рассмотрим другие возможности, которые предоставляет окно проверки. Еще одно назначение этого окна состоит в том, что оно представляет блокнот или калькулятор, в котором можно производить какие-либо дополнительные вычисления. Во-первых, в этом окне можно выполнять любые операторы, допустимые в контексте процедуры, выполнение которой было приостановлено. Отсюда, в частности, следует, что в окне можно изменять значения доступных переменных. Во-вторых, в этом окне можно выполнять запросы на вычисление выражений. Запрос начинается со знака "?", после которого печатается запрашиваемое выражение. После нажатия Enter в окне появляется значение выражения. В предыдущих лекциях мы неоднократно пользовались запросами для вычисления значений р азличных функций. Окно позволяет изменять и значения глобальных переменных модуля. Взгляните, как выглядит окно проверки, в котором выполняются некоторые вычисления в состоянии прерывания процедуры. В частности, в этом окне дважды присваивалось новое значение глобальной переменной и выдавался запрос на получение ее значения. Здесь же был вызван на исполнение метод PostfixOrder объекта root. В этом же окне появились результаты его работы, напечатанные оператором Debug.


увеличить изображение
Рис. 10.8.  Вычисления в окне проверки



Оператор On Error


Имеется три варианта синтаксиса этого оператора:

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 существует. Неудачным решением является и то, что этот оператор можно опускать, если охраняемый блок завершается вместе с самой процедурой. Лучше бы иметь завершающую структурную скобку, как это сделано в VBA для всех управляющих структур.
В процедурах, состоящих из нескольких охраняемых блоков, применение этого оператора обязательно. Прежде чем объявить новый охраняемый блок, нужно отключить текущий оператором On Error GoTo 0.

Заметьте, если ошибка возбуждена вне охраняемого блока, то она приведет к стандартному способу ее обработки, заключающемуся в том, что будет выдано сообщение об ошибке и выполнение будет приостановлено. Взгляните, как выглядит стандартное сообщение об ошибке:


Рис. 10.14.  Стандартное сообщение об ошибке периода выполнения

Доступные кнопки в этом окне определяют возможные действия в этом случае. Все эти варианты, как правило, мало устраивают конечного пользователя. Вот еще один вариант такого сообщения:


Рис. 10.15.  Другой вид окна сообщения об ошибке периода выполнения


Оператор Resume


Оператор Resume должен быть последним выполняемым оператором обработчика ошибок. Он определяет точку процедуры, которой передается управление по завершении обработки ошибки. У него также три варианта синтаксиса:

Resume [0] Resume Next Resume строка

В первом случае выполнение программы продолжается с повторного выполнения оператора, вызвавшего ошибку. Заметим, если это был оператор, инициировавший вызов процедур и функций, приведших в конечном итоге к ошибке, то управление передается этому оператору. Другими словами, управление всегда передается оператору охраняемого блока, явно или неявно инициировавшему ошибку. Вариант Resume (или Resume 0) естественно использовать, когда ошибка вызвана вводом неверных данных пользователем, а в обработчике ошибок у него запрашиваются новые правильные данные. Например, если в основной процедуре пользователь ввел имя несуществующего файла, в обработчике можно запросить новое имя и продолжить выполнение с повторения оператора открытия файла. Таким образом, этот вариант используется, если в обработчике ошибок устранена причина ошибки. В этом случае можно надеяться, что повторное исполнение оператора, ранее инициировавшего ошибку, теперь завершится благополучно.

В варианте Resume Next после обработки ошибки выполнение процедуры продолжается с оператора, следующего за оператором, инициировавшим ошибку. Не будем повторяться, и здесь речь идет об операторах охраняемого блока, а не тех вызванных процедур, где реально произошла ошибка. Если в первом варианте обработчик ошибки должен устранить причину ошибки, то в этом варианте обработчик устраняет последствия ошибки, выполняя, по существу, работу оператора, приведшего к ошибке. Еще одна возможная ситуация применения этого варианта, о которой мы уже упоминали, состоит в том, что следующий оператор сам анализирует причину ошибки и задает способы ее устранения.

В третьем варианте параметр строка задает метку строки или номер строки данной процедуры, на которую передается управление после обработки ошибки. Этот вариант используется в разных ситуациях, например, причина ошибки устранена, но необходимо повторить часть вычислений, предшествовавших появлению ошибки. Возможно, также, что устранены последствия ошибки, но действия надо продолжить не со следующего оператора, а с определенного участка процедуры. Третий вариант задает общую конструкцию, и мог бы использоваться во всех трех случаях.



Оптимизация программ


Результаты работы с системой документов должны удовлетворять заданным спецификациям. Эти результаты должны быть получены в заданное время. Точный прогноз погоды на завтра требуется сегодня, получить его послезавтра становится бессмысленным. Поэтому программисту постоянно приходится заботиться не только о корректности работы свих программ, но и об их эффективности. И здесь успех, во многом, определяется решениями, принимаемыми на самом верхнем уровне разработки проекта. Самое важное решение связано с выбором подходящей среды программирования. Здесь нужно точно учитывать перспективы развития разрабатываемой системы и выбираемой среды программирования, насколько средства, выбранные Вами сегодня, смогут удовлетворять Вашим завтрашним потребностям. Другой разрез, для многих задач не менее важный, состоит в правильном выборе структур данных и алгоритмов их обработки. Нет ничего лучше хорошего алгоритма. Он, как правило, определяет эффективность решения задач. Важную роль в повышении эффективности играет и переис пользование. Ранее созданные, тщательно проверенные и эффективные компоненты, могут определить и эффективность вновь решаемой задачи. Рассмотрение всех этих вопросов, главных источников эффективности работы системы в целом, выходит за рамки данной книги. Мы остановимся сейчас на деталях, каждая из которых в отдельности не способна существенно повысить эффективность решения. При добыче золота, можно пытаться найти самородок, а можно промывать руду, добывая песчинку за песчинкой. Так и при разработке программ, песчинки, повышающие эффективность, могут принести в результате ощутимый эффект.

Прежде, чем начать рассмотрение отдельных элементов, еще пару слов о измерениях, которые выполняет программист, для повышения эффективности своей программы. Известен эффект "bottle neck", - узких мест программы, которые могут быть причиной ее не эффективного поведения. Чтобы получить представление о том, есть ли в программе такие узкие места, требующие первостепенного вмешательства и переписывания, чаще всего требуется построение полного профиля программы. Причем и здесь важна представительная система тестов. Опять таки, мы не будем сейчас рассматривать средства построения профиля. Отметим только, что в ряде случаев можно для проведения измерений использовать функцию Timer.По ходу изложения мы несколько раз демонстрировали ее применение.



Ошибки периода выполнения и их обработка


Итак, отладка программы завершена, последняя найденная ошибка исправлена. Теперь программа должна быть передана пользователю. Значит ли это, что в ходе работы пользователя с программой не будут возникать ошибки? Обязательно, будут! Нужно предпринять специальные меры, чтобы появление этих ошибок не приводило к неприятным последствиям. Если этого не сделать, то при возникновении ошибки на экране появляется сообщение, как правило, мало что говорящее пользователю и программа завершает свою работу. Как правило, все это сопровождается нелестными высказываниями пользователя в адрес разработчика. Заметьте, ошибки этого периода (run time errors) могут быть и в правильно работающей программе. Они могут возникать из-за неверных действий самого пользователя, не знающего спецификаций. Обычно ошибки связаны с вводом данных несоответствующих типов или вводом значений, выходящих за пределы допустимого диапазона. Пользователь может пытаться открыть несуществующий файл, или файл, который он необдуманно удалил. При работе пользователя, например, в Access может быть сделана попытка открытия несуществующей таблицы или формы. В общем, у пользователя есть масса возможностей нарушить спецификации, особенно, если они не четко сформулированы. Но не стоит обольщаться, многие ошибки на совести программиста. Правильно считать, что во всех случаях виноват программист. В его задачу входит обнаружение и обработка всех исключительных ситуаций, возникающих в процессе работы программы. Сейчас мы и переходим к рассмотрению самого понятия исключительной ситуации и о тех средствах, которые есть в VBA для их обработки.

Исключительная ситуация или исключение (exception) возникает при выполнении программы и делает ее дальнейшее выполнение невозможным или нецелесообразным ввиду неопределенности, непредсказуемости или неправильности дальнейшего результата вычислений. Управление исключениями (exception handling) включает специально предусмотренное обнаружение исключений, перехват управления выполнением программы при возникновении исключения и передачу этого управления специальному разделу программы - обработчику исключения.

Определения, которые мы дали, носят общий характер и применимы к любому языку программирования. Следует сказать, что стандарт на исключения, их классификацию, способы обработки, еще не сформировался. Например, в языке Visual C++ обработка исключений значительно изощренней, чем в языке VBA. Следует сказать, что в VBA, к сожалению, не используется общепринятый термин исключение, вместо него используется термин ошибка. Суть дела от этого не меняется. Давайте начнем с рассмотрения общей схемы управления ошибками (исключениями) в языке VBA.



Отладка


Занимаясь программированием уже многие годы, я не только теоретически, но и на собственном опыте осознал, что программы, которые мы, программисты, разрабатываем, относятся к средствам повышенной опасности. В простых ситуациях ошибки в программах, могут стать источником разочарований и огорчений отдельного человека. В серьезных ситуациях ошибки чреваты катастрофой. На людей, создающих программы, возлагается ответственность, если хотите, то и моральная ответственность за надежность и правильность работы их творений. И хотя программа программе рознь не нужно думать, что сегодня можно состряпать что либо на авось, а завтра, когда наступит время серьезной программы, тогда и придет черед надежному программированию. Нет, программировать надежно нужно всегда.

Можно ли создать надежную программную систему? Вспоминая опыт собственной работы, могу сказать, что в наиболее ответственных случаях, когда речь шла об экспериментах, связанных с космосом, решение заключалось в том, что программа создавалась независимо двумя коллективами, начиная от разработки алгоритма, кончая системой тестов. Только после того, как обе системы правильно работали на всех предъявленных обеими сторонами тестах, программа принималась в эксплуатацию. Это были шестидесятые годы. Сегодня мы живем в другом мире, с другими возможностями. Вот цитата из письма, которое я, как бета-тестер Office 2000 получил от команды, занимающейся отладкой этой системы:

"...Another added feature to our beta program will be the privilege to nominate other beta 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 or neighbor to our program".

Вряд ли здесь необходим точный перевод. Речь идет о том, что одной из привилегий лучших бета - тестеров будет возможность рекомендовать тестеров для участия в новой программе тестирования. И делается это потому, что возникла проблема отбора тестеров. Заметьте, поступило 500000 заявок на участие в тестировании программного продукта. Следует заметить, что отношение к тестерам и их работе самое серьезное. Могу сказать, что ни один из посланных мной отчетов не остался без внимания. Конечно, при такой коллективной и независимой отладке можно в гораздо большей степени надеяться на надежность программ.

Поговорим сейчас о том, что должен делать каждый из программистов, работающих в среде Office 2000, чтобы создать надежный продукт и уменьшить число возможных ошибок, не надеясь на постороннюю помощь. Мы рассмотрим три темы:

Как написать, по возможности, надежную программу?Как вести отладку? Средства отладки Office 2000.Ошибки периода исполнения и их обработка.



Панель отладки и команды меню


Как и во многих других случаях, интерфейс отладчика VBA избыточен, - к одним и тем же инструментальным средствам можно добраться по-разному. В зависимости от привычек можно использовать панель Debug с инструментальными кнопками, можно использовать команды из меню Debug и View, можно использовать горячие клавиши. На следующем рисунке показана панель Debug (Отладка). Заметьте, на этой панели больше кнопок, чем в стандартном варианте, предлагаемом по умолчанию. Используя режим настройки (Customize) я вынес на эту панель дополнительные кнопки, задающие инструменты отладки.


Рис. 10.3.  Панель отладки с инструментальными кнопками

Кнопки этой панели соответствуют командам меню "Отладка" (Debug) и меню "Вид" (View), которые представлены на следующих рисунках:


Рис. 10.4.  Команды меню View


Рис. 10.5.  Команды меню Debug

На рисунках для большинства команд меню показаны соответствующие им кнопки и комбинация горячих клавиш, выполняющих вызов тех же инструментальных средств. Дадим краткое описание этих инструментальных средств:

Первая из этих кнопок включает и выключает состояние проектирования. Вторая запускает проект на компиляцию. Запуск на компиляцию стоит делать чаще, не дожидаясь завершения проектирования. Так можно раньше выявить некоторые ошибки.

Первая из этих кнопок запускает программу на выполнение. Более точно это означает следующее. Если курсор стоит в любом месте некоторой процедуры, то на выполнение запускается эта процедура. Если активна некоторая форма, то запускается эта форма, в противном случае запускается макрос. Вторая кнопка этой группы прерывает выполнение и переводит программу в состояние прерывания. Третья кнопка приостанавливает выполнение и производит сброс проекта,- чистятся все стеки, так что происходит переход в состояние проектирования.

Эти две кнопки позволяют работать с точками прерывания. Точки прерывания играют важную роль в процессе отладки. Чаще всего, для понимания поведения программы достаточно проанализировать состояние вычислений в некоторых ее точках, например, после завершения очередного цикла.
Точки прерывания ставятся в таких контрольных точках программы, после чего программа запускается на выполнение. Достигнув первой по ходу вычисления точки прерывания, выполнение приостанавливается, программа переходит в состояние прерывания. Состояние программы анализируется и затем можно произвести запуск вычислений до следующей точки прерывания. Таким образом, точки прерывания размечают программу, являясь промежуточными финишами процесса выполнения.

Первая из кнопок этой группы включает и выключает точку прерывания в строке, на которой установлен курсор. Вторая позволяет одним щелчком удалить все точки прерывания. Заметьте, точку прерывания можно установить и удалить без использования этих кнопок. Слева от окна кода есть специальное поле, выделенное серым цветом. В этом поле желтый стрелкой показан курсор, задающий текущий выполняемый оператор, в этом же поле черные кружочки отмечают точки прерывания. Достаточно щелкнуть левой кнопкой мыши в этом поле, чтобы соответствующая строка стала точкой прерывания, и, наоборот, если точка прерывания была установлена, то щелчок по ней удаляет ее.

Важная группа кнопок, включающая средства управления процессом вычислений. С их помощью на важных участках можно проследить выполнение программы с точностью до оператора. Первая из кнопок (Step Into - ей соответствует нажатие клавиши F8) задает пошаговый, пооператорный режим выполнения. При вызове процедур и функций в этом режиме начинается их пошаговое выполнение. После выполнения очередного оператора происходит прерывание и программа доступна для корректировки. Вторая кнопка (Step Over - Shift + F8) вызов процедур и функций выполняет за один шаг, что позволяет не задерживаться на выполнении уже отлаженных модулей. Третья кнопка (Step Out - Ctrl + Shift +F8) позволяет прервать пошаговое выполнение процедуры и вернуться к этому режиму уже в вызывающей процедуре. Четвертая из этих кнопок (Run to Cursor - Ctrl + F8) позволяет установить курсор в нужную позицию и запустить программу на выполнение.


Прерывание наступит, когда процесс вычислений дойдет до стр оки, указанной курсором. Заметьте, что чаще используют горячие клавиши, чем кнопки или команды меню.

Очень полезные кнопки, по крайней мере, первая из них. С ее помощью можно изменять порядок вычислений, предписанный программой. Мы уже говорили, что в процессе отладки желтая стрелка в левом поле задает строку с текущим выполняемым оператором. VBA позволяет самому программисту устанавливать, какой оператор будет выполняться следующим, при этом, что очень важно, можно производить откат назад и возвращаться к повторному исполнению ранее выполненного оператора. Чаще всего это полезно, когда в выполняемую процедуру внесены изменения, тут же можно проанализировать эффект исправлений без того, чтобы все вычисления производить заново. Итак, если в режиме прерывания поставить курсор на любой из операторов выполняемой процедуры и щелкнуть первую из кнопок данной группы, то желтая стрелка будет перенесена к этому оператору. Этот оператор станет текущим и будет следующим выполняемым оператором. Конечно, все это можно делать в пределах выполняемой процедуры. Это замечательное для отладки свойство возможно благодаря тому, что VBA является интерпретируемым языком. Заметим, что перенос стрелки, отмечающей текущий оператор, чаще всего делается с помощью мышки простым перетаскиванием желтой стрелки в левом поле вверх или вниз к нужному оператору (строке).

Следующая группа кнопок задает инструментальные средства, позволяющие наблюдать за состоянием программы, значениями ее переменных. Этим кнопкам соответствуют команды меню View. Первые три кнопки вызывают соответственно три окна наблюдения за состоянием вычислений - окно локальных переменных (Locals), окно проверки с отладочными результатами вычислений (Immediate), окно контрольных выражений (Watch). Следующая пара кнопок также предназначена для работы с контрольными выражениями, открывая специальные окна для показа и добавления контрольных выражений. Наконец, последняя кнопка в этой группе очень полезна при работе в ситуациях, когда отлаживаются рекурсивные процедуры или процедуры, где встречается глубокая вложенность вызовов.


Она позволяет показать стек вызовов процедур в текущий момент. Чуть позже мы подробнее рассмотрим работу с этими окнами.

При отладке весьма полезны и другие инструментальные средства, не связанные непосредственно с панелью Debug. Они остались вне нашего рассмотрения, но о некоторых из них все-таки скажем пару слов.

Эту пару кнопок инструментальной панели Edit мы уже упоминали. Первая из них позволяет закомментировать выделенный блок, вторая - отменить сделанные комментарии. Поскольку при отладке зачастую приходится опускать выполнение отдельных фрагментов, то эти кнопки относятся к часто используемым средствам.

Еще одна полезная кнопка этой панели Quick Info. Она позволяет получить подсказку о типе переменной, обо всех параметрах вызываемой функции и их типах. Для получения подсказки достаточно подвести курсор к соответствующей переменной или функции и щелкнуть кнопку. Тут же появится окно с подсказкой. Отметим также еще одно весьма полезное свойство, если просто подвести курсор к переменной, то тут же появляется окно подсказки, в котором показано значение этой переменной. Это эффективное средство наблюдения за состоянием переменных, не требующее специальных окон, наблюдение делается на лету.


Приемы оптимизации кода


По ходу изложения мы не раз обращали внимание на средства, повышающие эффективность вычислений. Об этом шла речь, при рассмотрении модулей, при рассмотрении вопроса об объявлении переменных и выполнения операций над ними. Сейчас мы приведем еще несколько советов, некоторые из которых уже упоминались ранее.



Dim MyDict As New BinTree


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
Пример 10.1.
Закрыть окно




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
Пример 10.2.
Закрыть окно




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.3.
Закрыть окно




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
Пример 10.4.
Закрыть окно




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
Пример 10.5.
Закрыть окно




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
Пример 10.6.
Закрыть окно



Средства отладки


Часть ошибок программы ловится автоматически еще на этапе компиляции. Сюда относятся все синтаксические ошибки, ошибки несоответствия типов и некоторые другие. Однако синтаксически корректная программа нуждается в отладке, поскольку, хотя результаты вычислений и получены, но они не соответствуют требуемым спецификациям. Чаще всего, еще не отлаженная программа на одних исходных данных работает правильно, на других - дает ошибочный результат. Искусство отладки состоит в том, чтобы обнаружить все ситуации, в которых работа программы приводит к ошибочным вычислениям. VBA обладает весьма изощренными средствами, предназначенными для отладки программ, т.е. для обнаружения ошибок в программах (тестирования) и их исправления. Есть две группы средств VBA, помогающие программисту выявить и исправить ошибки:

Первая группа позволяет контролировать ход вычислительного процесса, т.е. порядок следования операторов в процедурах, порядок вызова самих процедур. При необходимости в процессе отладки разрешается изменять этот порядок, можно, например, пропускать исполнение некоторых операторов, или повторно возвращаться к их исполнениюВторая группа средств позволяет контролировать изменение состояния вычислительного процесса (значений переменных и свойств объектов) в процессе выполнения. И здесь можно вмешаться и изменить состояние, задав по ходу дела новые значения для тех или иных переменных.

Прежде, чем приступить к подробному рассмотрению этих средств, напомним, что в ходе отладки программа может находиться в одном из трех состояний: проектирования, вычисления и прерывания. Закончив проектирование, можно запустить программу на исполнение. Прервав исполнение программы в заданной точке, перейдя в состояние прерывания, можно проконтролировать значения переменных и свойств объектов в данной точке и, если требуется, изменить эти значения "вручную". При этом можно изменить порядок выполняемых операторов, задавать следующий исполняемый оператор, можно редактировать программный текст перед продолжением вычисления.
Переход из состояния вычисления в состояние прерывания может происходить по самым разным причинам, например, по достижении точки прерывания, при выполнении одного из многочисленных условий прерывания, из-за пошагового выполнения программы. Все эти возможности мы еще обсудим, а сейчас рассмотрим один особый случай. Иногда программа "зацикливается" и необходимо принудительно перевести ее в с остояние прерывания. Как остановить работающую программу? Просто нажмите знакомую еще по работе в DOS пару клавиш Ctrl+Break. На экране появится следующее диалоговое окно с сообщением об остановке.


Рис. 10.2.  Сообщение о прерывании работы программы

В этом окне нажатие кнопки "End" остановит исполнение программы, нажатие кнопки "Continue" возобновит ее выполнение, программа снова перейдет в состояние вычисления. Если нажать кнопку "Debug", то выполнение программы прервется, и мы перейдем в режим прерывания - режим отладки. Активным станет окно с текстом программы, а в нем будет выделен оператор, на котором прервалось исполнение.


Строковые операции


При работе со строками используйте введенные в Office 2000 функции Replace, функции разбора строки и другие. Ранее мы подробно рассказали об их достоинствах.Избегайте, по возможности, использования конкатенации. Используйте Replace в большинстве случаев. В тех случаях, когда заменяется одна подстрока на другую такой же размерности, можно использовать функцию Mid, как в следующем примере:

Public Sub TestCode() Dim Text As String Text = "Компилятор кода" Mid(Text, 1, 5) = "Транс" Debug.Print Text End Sub

Строковые константы VBA могут сократить время вычислений, позволяя избежать вызовов функций. Так, например, эффективнее использовать константу vbCrLf, чем комбинацию символов Chr(13), Chr(10), задающих возврат каретки и перевод строки.Иногда медленные операции над строками можно изменить на операции работы с их кодами. Код

If Asc(Text) = 202

работает быстрее, чем код

If Left(Text,1) = "K"

Злоупотреблять такими оптимизациями не стоит, поскольку первый фрагмент менее понятен, чем второй. Он нуждается, по крайней мере, в комментариях.



Структура обработчика ошибок


Обработчик, способный обработать ошибку, должен распознать ее, понять к какому классу относится ошибка - классу устранимых ошибок или классу критических ошибок, последствия которых устранить невозможно. В первом случае, обработчик устраняет причину или следствия ошибки и возвращает управление в охраняемый блок для продолжения вычислений. Во втором случае, обработчик завершает исполнение, предварительно сообщив пользователю всю возможную информацию о причинах, приведших к такому результату.

Как правило, каждый обработчик предназначен для обнаружения и исправления ошибок некоторого класса. Поэтому, когда он встречается с ошибкой другого класса, то он не способен ее обработать. В этом случае он передает эту функцию выше стоящему обработчику. Реализуется это тем, что обработчик вызывает метод 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:

Коды всех внутренних, перехватываемых ошибок можно найти в разделе справочной системы "Trappable Errors" (Перехватываемые ошибки).Если ошибка выполнения возникла в некоем объекте вне VBA (например, в рабочей странице Excel) и не обработана этим объектом, а возвращена в VBA-программу, она будет автоматически преобразована VBA в ошибку с кодом 440, которая определена как "Automation Error" (Ошибка программирования объектов). Такую ошибку желательно сразу же обработать. Если же Вы хотите передать ее на обработку вверх в вызывающую процедуру, желательно возбудить ошибку со своим специальным номером, чтобы вызывающая процедура могла различать ошибки, возникающие в разных объектах.Объекты Office 2000, кроме рассмотренных выше средств работы с ошибками, могут иметь дополнительные средства для их распознавания и обработки. Например, для диалоговых окон и элементов управления определено событие Error, позволяющее обрабатывать их специфические ошибки, которые не могут быть переданы в VBA. Информация об ошибках операций доступа к базам данных может быть получена с помощью объекта Error и семейства Errors из библиотеки объектов доступа к данным (DAO). Описание ошибки Microsoft Access или объекта доступа к данным можно получить по номеру ошибки методом AccessError.


Условная компиляция и отладка


Заметьте, что вызов методов объекта Debug может встречаться только в период отладки программы. В тот момент, когда программа передается конечному пользователю, вызов методов этого объекта не должен встречаться в работающей программе. Это понятно, поскольку в этом режиме никакие отладочные окна не появляются, нет никаких окон проверки и методу Print некуда направлять свой вывод, носящий отладочный характер. И уж тем более не должно прерываться выполнение программы, если вдруг Assert - утверждение станет ложным. Конечный пользователь просто не будет знать, что нужно делать в этом случае.

Когда отладка завершена, вызовы методов Print и Assert не должны встречаться в выполняемой программе. Конечно, можно просто удалить эти вызовы из текста программы или закомментировать их. Однако жизненный цикл многих программ таков, что снова и снова приходится возвращаться к отладке ранее разработанной программы, внесению заплаток, созданию новой версии, введению новых возможностей. Поэтому удаление или комментирование вызовов объекта Debug не является лучшим способом. Гораздо удобнее иметь возможность включать или выключать отладочный режим при необходимости. Для этой цели и используются средства условной компиляции. Рассмотрим их применение на примере вызовов методов объекта Debug. Идея состоит в том, что все вызовы методов этого объекта заключаются в обертку, заданную специальным оператором #If условной компиляции. Этот оператор проверяет истинность выражения, заданного, как правило, константой периода компиляции. Эта константа играет р оль флажка, в зависимости от ее значения и будут выполняться отладочные операторы. По-видимому, достаточно было бы одного примера, но скажем об этом чуть подробнее.