Visual2000 · Архив статей А.Колесова & О.Павловой

Советы тем, кто программирует на VB & VBA

Андрей Колесов, Ольга Павлова

© 2000, Андрей Колесов, Ольга Павлова
Авторский вариант. Статья была опубликована c незначительной литературной правкой в журнале "КомпьютерПресс" N 01/2000, компакт-диск.


Совет 229. Используйте реентерабельные и рекурсивные процедуры. Но осторожно!

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

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

Подобный случай возможен и внутри одной программы, работающей на обычном ПК. Например, вы написали процедуру Integral для вычисления определенного интеграла для функции, которая задается в виде указателя (для VB какой режим стал в принципе возможен с появлением в VB 5.0 функции AddressOf). Если же вам нужно взять двойной интеграл, то может легко получиться, что подынтегральная функция в свою очередь еще раз обратится к Intergal.

Идея реентерабельности процедур основывается на реализации двух постулатов:

  1. Программный код процедур не должен модифицироваться во время выполнения. В счастью, в языках высокого уровня (в том числе и в VB) такое в принципе невозможно, а вот на ассемблере такие штуки делаются легко. Более того, в старые добрые времена подобная коррекция кода порой предлагалась даже в качестве стандартных приемом "изысканного" программирования.

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

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

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

Приведем несколько примеров использования рекурсивных процедур.

Пример 1. Процедура реверсивного преобразования строковой переменной (перестановка символов задом наперед). Такая встроенная функция появилась в VB 6.0 (SrtReverse) и разные способы ее реализации традиционными средствами Basic мы уже приводили (см. "Особенности работы со строковыми переменными в VB", часть 2). Еще один вариант с помощью рекурсивной функции выглядит следующим образом:

Public Function ReverseRecursion$(s$)
  '
  ' инверсия строковой переменной с помощью
  ' рекурсивной функции
  Dim c$
  c$ = Left$(s$, 1)
  If c$ = "" Then
    ' исходная строка пустая, завершаем рекурсию
    ReverseRecursion$ = ""
  Else
    ' не пустая строка - продолжаем рекурсию
    ReverseRecursion$ = ReverseRecursion$(Mid$(s$, 2)) & c$
  End If
End Function

Данный вариант выглядит не сложнее, чем традиционный алгоритм:

Function ReverseString$ (Source$)
  Dim sReverse$, i%
  For i = Len(Source$) To 1 Step -1
          sReverse = sReverse & Mid$(Source, i, 1)
  Next I
  ReverseString$ = sReverse
End Function

Однако при этом рекурсивный алгоритм работает примерно в 3,5-4 раза медленнее. Обратите внимание, что все внутренние переменные ReverseRecursion (в данном случае это одна переменная c$) являются динамическими. Если же поставить ключевое слово Static в описании функции, то она будет просто неверно работать. (Попробуете сами — будет выдаваться пустая строка! Это объясняется просто — рекурсия заканчивается, когда переменная с$="", и именно она участвует в "обратном" формировании строки.)

Пример 2. Вычисление факториала N! = N * (N-1) *... 1 Это является классическим примером рекурсивного соотношения, которое записывается следующим образом:

N! = N * (N-1)!
0!=1

Такое вычисление также может быть реализовано в виде двух алгоритмов:

Код в рекурсивной функции заметно короче, но работает в 4 раза медленнее.

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

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

В начало статьи

Совет 230. Используйте рекурсию для передвижения по файлам

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

PathName$ = "d:\qb_lib\" ' стартовый каталог
FileName$ = "*.bas" ' шаблон имени файла
FileCounter% = 0 ' начальное значение счетчика найденных файлов
DirCounter% = 0  ' начальное значение счетчика просмотренных
						' подкаталогов
Call HowManyFilesInThisDirectory(PathName$, FileName$, FileCounter%)

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

  1. Результат работы процедуры формируется в двух переменных: FileCounter и DirCounter. Однако первая передается в качестве параметра функции (и постоянно модифицируется при этом), а вторая является глобальной, статической переменной рекурсивной функции.

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

    MyDir$ = Dir(PathName$, vbDirectory)
    Do While MyDir$ <> ""
      If MyDir$ <> "." And MyDir$ <> ".." Then
        If GetAttr(PathName$ + MyDir$) = vbDirectory Then
          ' найден каталог
          DirCounter% = MyDirCounter% + 1
          NewPathName$ = PathName$ + MyDir$(i) + "\"
          Call HowManyFilesInThisDirectory(NewPathName$, _
            FileName$, FileCounter%)
        End If
      End If
      MyDir$ = Dir ' следующий поиск
    Loop
    

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

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

  3. В процедуре формирования списка элементов каталога в массиве мы используем конструкцию:

    If MyDirCount% > UBound(arrPath$) Then
      ReDim Preserve arrPath$(UBound(arrPath$) + 100)
    End If
    

    Это обеспечивает автоматическое увеличение начального размера динамического массива с сохранением ранее записанной информации.

  4. Здесь с сожалением можно отметить, что в VB не допускается передача адресов функций в качестве параметров VB-процедуры (AddressOf можно использовать только при обращении к DLL). Если бы это было возможно, то мы могли бы использовать одну и ту же процедуру HowManyFilesInThisDirectory для выполнения разнообразных операций с найденными файлами (например, контекстного поиска в них). А пока нам придется брать исходный код этой процедуры и "вшивать" нужные операции, создавая новые варианты программ.

В начало статьи

Листинг 230. Перемещение по дереву файловой системы

Public DirCounter% ' текущее число просмотренных каталогов

Public Sub HowManyFilesInThisDirectory(PathName$, FileName$, FileCounter%)
'
'  подсчет числа найден файлов по заданному шаблону
'  PathName$ - каталог
'  FileName$ - шаблон имени файла
'  FileCounter% - текущее значение счетчика найденых файлов
'==================================================
   Dim MyFile$, MyDirCount%
   Dim NewPathName$, i%
   
   DirCounter% = DirCounter% + 1 ' смотрим очередной каталог
   '
   ' подсчет файлов в данном каталоге:
    MyFile$ = Dir(PathName$ + FileName$) ' первый поиск
    Do While MyFile$ <> ""
      FileCounter% = FileCounter% + 1
      ' тут можно выполнить какую-нибуль операцию с файлом
      MyFile$ = Dir  ' следующий поиск
    Loop
    '
    ' определяем состав подкаталогов в данном каталоге
    ReDim arrPath$(100)  ' для списка подкаталов
    Call CurrentDirCounter(PathName$, "", MyDirCount%, arrPath$(), vbDirectory)
    '
    If MyDirCount% > 0 Then 'есть подкаталоги
      For i% = 1 To MyDirCount%
        ' !! рекурсивное обращение к САМОЙ СЕБЕ!!
        NewPathName$ = PathName$ + arrPath$(i) + "\"
        Call HowManyFilesInThisDirectory(NewPathName$, FileName$, FileCounter%)
      Next
    End If
End Sub

Public Sub CurrentDirCounter(PathName$, FileName$, MyDirCount%,
arrPath$(), attr%)
'
'  Формирование списка имен элементов (attr% задает тип)
'  в текущем каталоге
'
    Dim MyDir$
    
    MyDirCount% = 0   'счетчик подкаталов в текущем каталоге
    MyDir$ = Dir(PathName$ + FileName$, attr%) 'первый поиск подкаталогов
    Do While MyDir$ <> ""
      If MyDir$ <> "." And MyDir$ <> ".." Then
        If GetAttr(PathName$ + MyDir$) = attr% Then  ' найден каталог
          MyDirCount% = MyDirCount% + 1
          If MyDirCount% > UBound(arrPath$) Then
            ' увеличивает размер массива с сохранением старой информации
            ReDim Preserve arrPath$(UBound(arrPath$) + 100)
          End If
          arrPath$(MyDirCount%) = MyDir$
         ' Debug.Print MyDir$; GetAttr(PathName$ + MyDir$)
        End If
      End If
      MyDir$ = Dir  ' следующий поиск
    Loop
    '
End Sub

В начало статьи

Совет 231. Преобразование имен файлов в определенный регистр

Традиционно системы DOS/Windows являются нечувствительными к регистру символов в именах файлов и каталогов: имена FileName, FiLENAME и т.д. являются идентичными. Однако в Unix-системах это не так, и мы, например, столкнулись с этой проблемой, когда начали заниматься созданием Web-узла, переписывая файлы с нашего локального компьютера на сервер.

Мы приняли однозначное правило — имена элементов оглавления пишутся только строчными буквами. Однако, если соблюдение этого правила в тексте HTML-файла не вызывает никаких сложностей — там все видно, то управление физическими названиями в оглавлении оказалось не так просто. Так, вид имени файла в окне Windows Explorer довольно часто совершенно не соответствует тому, каким он является на самом деле (не вполне понятно, почему).

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

Работа с отдельным файлов выглядит достаточно просто:

' PathName$ - имя каталога
' MyFile$ - исходный файл
NewFile$ = LCase$(MyFile$)
If NewFile$ <> MyName$ Then ' нужна замена имени
  Name PathName$ + MyName$ As PathName$ + NewFile$
End If

А как выполнить такую операцию для всех файлов по заданному шаблону? Казалось бы, это можно сделать так:

MyFile$ = Dir(PathName$ + FileName$) ' первый поиск
Do While MyFile$ <> ""
  '  анализ имени и переименование (см. код выше)
  MyDir$ = Dir ' следующий поиск
Loop

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

ReDim arrPath$(100) ' для списка файлов
Call CurrentDirCounter(PathName$, MyDirCount%, _
  arrPath$(), vbNormal)
'
If MyDirCount% > 0 Then ' есть подкаталоги
  For i% = 1 To MyDirCount%
    ' анализ имени и переименование (см. код выше)
  Next
End If

В начало статьи

Совет 232. Используйте Preserve для сохранения содержимого массива

При работе с массивами довольно часто встречается ситуация, когда оказывается, что его размеров не хватает для реального объема данных и нужно увеличить эти размеры с сохранением уже записанной информации. Типичным примером является запись в массив текстового файла, число строк которого неизвестна (другой случай, связанный с формированием списка элементов каталога, приведен в Совете 230, процедура HowManyFilesInThisDirectory). В этом случае следует использовать ключевое слово Preserve в операторе Redim:

StartSize = 100 ' начальное значение массива
ReDim arrStrings$(1 To StartSize)
AddSize = 20 ' дискретность увеличения длины
Open FileName$ For Input As #1
iLen = 0 ' текущая позиция для записи
While Not EOF(1)
  iLen = iLen + 1
  If iLen > UBound(arrStrings$) Then
     ReDim Preserve arrStrings$(UBound(arrStrings$) + AddSize)
  End If
  Line Input #1, arrStrings$(iLen)
Wend

Close #1
'
' Если хотите, то после окончания ввода можно уменьшить
' размеры массива до реального числа введенных элементов
ReDim Preserve arrStrings$(iLen)

Коррекция длины массива с использование Preserve возможна и для многомерных массивов, но при этом допускается изменение только последнего индекса (что связано с порядком хранения многомерных данных — по строкам). Например:

ReDim SomeArray(10, 10, 10)
ReDim Preserve SomeArray(10, 10, 15) ' допустимо
ReDim Preserve SomeArray(10, 15, 10) ' недопустимо

В начало статьи

Совет 233. Используйте FreeFile для получения номера файла. Но будьте при этом бдительны!

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

FileNumber = FreeFile
Open FileName$ For Input As #FileNumber
Line Input #FileNumber, a$

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

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

В начало статьи