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

Процедуры и функции

Разбить на страницы
Показывать лекцию целиком

Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne9, DocOne9.

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

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

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

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

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

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

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

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

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

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

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

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

Описание процедуры 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)

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

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

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

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

  • открыть в окне проектов "Проект-(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 - в окне редактора появится заготовка процедуры, состоящая из ее заголовка (без параметров) и закрывающего оператора;
  • добавить параметры в заголовок процедуры и написать текст процедуры между ее заголовком и закрывающим оператором.
  • Создание процедур обработки событий

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

    Вызовы процедур и функций

    Вызовы процедур 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. Конечно, было бы лучше, если бы эта программа выдавала ошибки на этапе проверки синтаксиса.

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

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

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

    В предыдущих примерах фактические параметры вызова процедуры или функции располагались в том же порядке, что и формальные параметры в ее заголовке. Это не всегда удобно, особенно если некоторые аргументы необязательны ( 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

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

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

    Конструкция 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.

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

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

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

    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

    Прокомментируем работу функции 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 приводит к четвертому варианту и появлению на экране окна сообщений.

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

    Известно, что в 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.
    		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

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

  • Эта функция может (и будет) вызываться как из процедур 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. Тем не менее, все работает правильно.
  • Рекурсивные процедуры

    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

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

    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

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

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

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

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

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

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

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

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

    Класс TreeNode

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

    'Class TreeNode
    'Элемент дерева
    
    Public key As String
    Public info As String
    Public left As New BinTree
    Public right As New BinTree

    Класс BinTree

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

    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

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

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

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

    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

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

    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) Лексикографическое дерево, задающее словарь
    Страницы:

    Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne9, DocOne9.

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

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

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

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

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

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

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

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

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

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

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

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

    Описание процедуры 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)

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

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

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

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

  • открыть в окне проектов "Проект-(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 - в окне редактора появится заготовка процедуры, состоящая из ее заголовка (без параметров) и закрывающего оператора;
  • добавить параметры в заголовок процедуры и написать текст процедуры между ее заголовком и закрывающим оператором.
  • Создание процедур обработки событий

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

    Вызовы процедур и функций

    Вызовы процедур 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. Конечно, было бы лучше, если бы эта программа выдавала ошибки на этапе проверки синтаксиса.

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

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

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

    В предыдущих примерах фактические параметры вызова процедуры или функции располагались в том же порядке, что и формальные параметры в ее заголовке. Это не всегда удобно, особенно если некоторые аргументы необязательны ( 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

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

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

    Конструкция 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.

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

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

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

    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

    Прокомментируем работу функции 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 приводит к четвертому варианту и появлению на экране окна сообщений.

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

    Известно, что в 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.
    		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

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

  • Эта функция может (и будет) вызываться как из процедур 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. Тем не менее, все работает правильно.
  • Рекурсивные процедуры

    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

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

    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

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

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

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

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

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

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

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

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

    Класс TreeNode

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

    'Class TreeNode
    'Элемент дерева
    
    Public key As String
    Public info As String
    Public left As New BinTree
    Public right As New BinTree

    Класс BinTree

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

    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

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

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

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

    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

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

    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) Лексикографическое дерево, задающее словарь
    Вернуться к учебному плану