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

Особенности технологий раннего и позднего связывания в Visual Basic

Андрей Колесов

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


В классических процедурных языках...

В классических процедурных языках (например, в DOS-овских версиях MS Basic) основополагающим принципом было использования технологии раннего связывания. А позднее связывание впервые было реализовано в системах-интерпретаторах, так что парадокс, возможно, заключается в том, что примитивный язык для начинающих под названием Бейсик был прообразом "крутых" ООП-систем.

Система Visual Basic базируется на сочетании механизмов раннего и позднего связывания программного кода, что является одной их характерный черт современных объектноориентированных языков. Каждая из этих технологий имеет свои достоинства и недостатки, которые нужно иметь в виду при разработке приложений.

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

В чем принципиальные отличия

Попробуем сформулировать определения.

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

  2. Ранее связвывание — установка таких связей до начала выполнения программы. Обычно под этим понимается связывание к процессе компиляции исходных модулей и компоновки испольныемого модуля из объектных. Однако к этому же относится процедура проверки наличия всех библиотек времени выполнения (Run-Time module) при запуске приложения.

  3. Позднее связывание — установка связей в процессе выполнения программы. Речь идет обычно либо о динамических связях (когда только в ходе работы приложения определяется какик объекты будет нужны) либо о формировании таких объектов в время работы.

Примечание. Многие VB-программисты вообще не очень хорошо представляют себе, что процедура формирования исполняемого модуля (или запуска программы в среде VB) состоит из компиляции отдельных модулей и последующей их компоновки в загрузочный. Дело в том, что VB не позволяет подключать внешние объектные модули, поэтому Microsoft решила не детализировать этот процесс, назвав его компиляцией. Отметим, что это не является характеристикой языка Basic, а исключительно желание Microsoft. Например, во времена MS Basic/DOS были отдельные компилятор и компоновщик, которые можно было использовать автономно, вне среды разработки.

Чтобы начать разобираться в этом, напишите такой простой программный код:

Sub Main()
  Dim MyVal%
  MyVal = 13 
  If MyVal Mod 5 = 0 Then  ' если нацело делится на 5
    Call MyProc  , то обращение к функции
  End If
End Sub

Sub MyProc()
  Call HisProc  ' обращение к какой-то процедуре
End Sub

При работе, например с QuickBasic, еще в момент запуска программы в среде интерпретатора сразу будет выдана сообщение об ошибке — "Не определена процедура HisProc". Дело в том, что в QB стал одним из первых массовых инструментов разработки, в котором был реализован принцип компилирующего интерпретора.

Напомним, что классическая схема интерпретатора предполагает, что проверка синтаксиса оператора — в том числе разрешенность ссылок (фактического наличия указанной ссылки — переменной, процедуры и пр.) — осуществляется только в момент его выполнения. Т.е. только в момент обращения к данному оператору производится синтаксический разбор текстовой записи оператора и его исполнение. Например, если вы напишете на GWBasic (мне удалось с трудом найти на сохранившемся дистрибутиве MS-DOS 4.0) такой код:

100 GOTO 200
110 a$ = MID$("asdf",1,1,1,1)
200 PRINT "Привет"

то ошибка в операторе 110 (явно неверная запись оператора) появится, только если мы уберем оператор 100.

Компилирующий интерпретатор QB при отладке в среде сначала проводил полный синтаксический контроль кода всей программы, включая связывание всех идентификаторов, и преобразование ее текстового кода во внутренний p-Code. А уже потом уже выполнял этот внутренний код в режиме интерпретации. Кроме этого транслятора QB имел настоящий компилятор, который создал объектные двоичные модули, которые потом объединялись компоновщиком в программы на машинном коде.

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

При работе в VB и VBA реализованы два режима компиляции кода при работе в среде, которые определяются состоянием флажка Compile On Demand на вкладке General диалогового окна Tools|Options.

  1. Если флажок установлен (этот режим определяется по умолчанию при инсталляции VB или Office 2000), то компиляция исходного кода будет выполняться только в момент его выполнения. В этом случае при запуске приведенного выше примера в среде VB сообщение об ошибки появится только в случае, если d будет делить нацело на 5 (например, MyVal =15). Т.е. проверка разрешенности ссылки на HisProc будет выполняться только в момент выполнения процедуры MyProc, что является явным признаком механизма позднего связывания.

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

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

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

Совет для VBA-разработчиков. При отладке проекта работайте только со вторым вариантом режима компиляции кода. Это поможет вам избежать множества проблем, которые могут быть выявлены автоматически транслятором. Если вас волнует проблема скорости запуска программы (большой проект), то можно установить Compile On Demand, но на завершающем этапе отладки все же сбросьте его. После завершения отладки (в том числе в режиме опытной эксплуатации) можно установить первый режим — запуск программы будет выполняться быстрее.

Совет для VB-разработчиков. Он формулируется не столь категорично. Установка режима Compile On Demand не столько чревата проблемами, так как при создании EXE-модуля все равно будет выполнена полная компиляция проекта. Тем не менее, тут можно дать два "подсовета".

  1. При работе с небольшими проектами сбросьте Compile On Demand.

  2. Если время запуска программы в среде является критичным, то можно установить Compile On Demand. Однако время от времени для тестового запуска проекта в среде VB используйте вместо команды Run|Start (F5 или соответствующей кнопки на панели инструментов) команду Run|Start With Full Compile (Ctrl+F5), которая производит обязательную компиляцию всего проекта, независимо от установки Compile On Demand.

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

Как работает механизм связывания в VB

Но все же механизм позднего связывания VB в режиме Compile On Demand несколько отличается от "классического" варианта, реализованного в GWBasic. Дело в том, что VB в этом случае выполняет синтаксический контроль не по-операторно, а по-процедурно, т.е. производится трансляция всего кода процедуры в момент обращения к ней.

Чтобы убедить в этом, для процедуры MyProc напишите такой код:

Sub MyProc()
  Dim a$
  Exit Sub '  выход из процедуры
  ' эти операторы содержат ошибки:
  D = 1   ' переменная не определена
  A$ = Mid$("asdr",1,1,1,1)  ' неверный синтаксис функции Mid
End Sub

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

Здесь мне хотелось бы сделать замечание относительно того, что, несмотря на многие достоинства интеллектуального редактора VB/VBE, по некоторым функциям (очень важным для разработчика) он заметно уступает тому, что было реализовано той же Microsoft почти полтора десятка лет назад в QB.

Например, редактор VB совершенно спокойно реагирует на ввод такой явно ошибочной строки

A$= Mid$("asdr",1,1,1,1)  ' неверный синтаксис функции Mid

QB выдал бы сообщение об ошибке синтаксиса непосредственно при вводе кода.

Еще одни пример подобного спокойствия VB:

A$ = TextBox.test

Тут очевидно, что программист ошибся, введя "test" вместо "text". VB отлично знает, что у текстового поля нет свойства test (он привел список допустимых свойств в своей подсказке), но при этом ничего не сообщает разработчику, ограничившись тем, что не "поднял" первую букву идентификатора.

Совет: Используйьте в идентификаторах переменных и процедур прописные буквы и вводите код только с использованием строчных букв нижнего регистра клавиатуры. Тогда вы по реакции редактора (он должен "поднять" нужные символы) сразу увидите — определен идентификатор или нет (поворчим немного: хороший редактор мог бы сообщить об этом в явном виде).

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

Зачем нужно позднее связывание

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

Простой пример использования этого механизма — использование внешних DLL и, в частности, функций Win API.

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

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

Dim MyObject As Object
...
' тут объект должен быть создан, например
If SomeVar = 0 Then ' объект = ссылка
  Set MyObject = ActiveDocument.VBProject.References.Item(1) 
Else
  Set MyObject =  ActiveDocument.VBProject.  ' объект = проект 
активного документа
End If
...
a$ = MyObject.FullPath ' это будет работать только если SomeVar = 0

С точки зрения традиционного компилятора с механизмом раннего связывания в последнем операторе имеется явная ошибка, так как непонятно, что за объект будет реально создан и будет ли он обладать свойством FullPath — это станет известным только в момент выполнения программы. "Хороший" компилятор вполне может отследить тип присваиваемого объекта при линейном алгоритме, но в нашем случае ветвления он будет также бессилен.

Вот еще один пример на эту же тему. Очевидно, что такая процедура

Sub MyProc(cntControl As Control)
    MsgBox cntControl.Text
End Sub

в зависимости от передаваемого в нее конкретного элемента управления будет работать (Text) или не работать (Label).

Рассмотрим еще одну любопытную ситуацию на примере связи двух VBA-проектов. Если мы хотим из нашего активного проекта обратиться к процедуре OtherProcedure документа OtherDoc.doc, то можно сначала сделать ссылку на этот документ в окне Tools|References и написать такой простой код:

Call OtherProcedure

(Мы считаем, что имя процедуры является уникальными, поэтому не указываем полный путь к ней — Call OtherDoc.OtherModule.OtherProcedure.)

Если же мы захотим выполнить установку динамической ссылки на документ, то можно реализовать следующую конструкцию:

Sub MyProcedure
  ' установка ссылки программным образом:
  ActiveDocument.VBProject.References.AddFromFile "OtherDoc.doc"
  ' обращение к процедуре:
  Call OtherProcedure
End If

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

Sub MyProcedure
  ' установка ссылки программным образом:
  ActiveDocument.VBProject.References.AddFromFile "OtherDoc.doc"
  ' обращение к промежуточной процедуре:
  Call MyOtherProcedure
End If

Sub MyOtherProcedure
  ' к этому моменту имя OtherProcedure будет уже определено
  Call OtherProcedure
End If

Но такая конструкция будет работать только в режиме Compile On Demand. Радикальное же решение проблемы заключается в использовании в данном случае свойства Run для обращения к процедурам подключаемых проектов (подробнее см. статью "Программное взаимодействие проектов Office 2000").

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

Связывание с внешними объектами

Достаточно типичной задачей является является использование в приложении неких внешних ActiveX-объектов (приложений или объектов). Например, вы хотите обратиться к приложению Word. В этом случае можно выбрать один из вариантов — с ранним или поздним связыванием:

Sub EarlyBinding()
  ' Пример раннего связывания
  ' с внешним объектом Word
  Dim oWord As New Word.Application  'создаем конкретный объект
  ' Можно создать объект таким образом:
  ' Dim oWord As Word.Application  ' описываем конкретный
  ' Set oWord = New Word.Application  ' создаем новый экземпляр
  
  oWord.Visible = True
     MsgBox "Раннее связывание"
  oWord.Quit
  Set oWord = Nothing
  
End Sub

Sub LateBinding()
  ' Пример позднего связывания
  ' с внешним объектом Word
  Dim oWord As Object  ' неопределенный объект
  
  Set oWord = CreateObject("Word.Application")
  oWord.Visible = True
    MsgBox "Позднее связывание"
  oWord.Quit
  Set oWord = Nothing
End Sub

Достоинство раннего связывания: работают подсказки и синтаксический контроль при создании исполняемого модуля. Но этот вариант (точнее вся программа, независимо от того было или не было обращение к EarlyBinding) будет работать только в случае наличия ссылки (Project|References) на реально существующее приложение. При этом имеется два вариант создания объекта:

Dim oWord As New Word.Application  'создаем конкретный объект

и

Dim oWord As Word.Application  ' описываем конкретный
Set oWord = New Word.Application  ' создаем новый экземпляр

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

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

В случае позднего связывания можно было бы динамически менять имя открываемого объекта, например таким образом:

Sub LateBinding(AppName$)
  ' Пример позднего связывания
  ' с внешним объектом AppName$
  Dim oApp As Object  ' неопределенный объект
  
  Set oApp = CreateObject(AppName$)
  oApp.Visible = True
  oApp.Quit
  Set oApp = Nothing
End Sub

Разумеется, тут нужно быть уверенным, что написанные методы действительно могут работать с объектом. Но в данном простом примере можно использовать практически любое приложение, использующее ActiveX-интерфейс.

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

Используйте раннее связывание, где это возможно

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

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

Dim ActiveRef As Object
For Each ActiveRef In ActiveDocument.VBProject.References
    ' имя проекта или библиотеки
  Debug.Print "Имя проекта = " & ActiveRef.name   
    ' полное имя файла 
  Debug.Print "Полное имя файла = " & ActiveRef.fullpath
Next

Недостаток этой конструкции с точки зрения разработчика очевиден: в операторе For Each выполняет динамическое определение объекта ActiveRef в качестве ссылки. Поэтому редактор ничего не знает о том, как будет произведена эта установка и в последующих операторах Print не может показать список допустимых свойств для объекта ActiveRef (Именно это мы хотели подчеркнуть, написав свойства Name и FullPath строчными буквами — редактор также "не поднимет" буквы в этих именах).

Оригинальное решение этой проблемы приводит Владимир Биллиг в своей статье "Документы Office 2000 и их проекты". Он предлагает в процессе ввода кода описать ActiveRef в виде конкретного объекта, в данном случае как Dim ActiveRef As Reference. При этом, как утверждается, будет работать синстаксис-подсказка. Но при запуске кода на выполнения такое определение объекта окажется недействительным, поэтому нужно будет написать определение Dim ActiveRef As Object. (Т.е. "As Reference" используется только для ввода кода, а потом для отладки и выполнения эта строка меняется на "As Object").

К сожалению, мои попытки воспользоваться этим советом не увенчались успехом. Возможно, причина заключается в использовании разных релизов продукта или в каких-то тонких настройках. Но здесь можно констатировать только одно — механизм нетривиального определения объектов в Office 97/2000 выглядит пока довольно сырым. Например, Владимир Биллиг отмечает, что идентичные программные конструкции в одних приложениях работают, а в других нет. В нашем случае видны явные противоречия, когда редактор "поднимает" название типа в строке

Dim MyRef As Reference

показывая, что ключевое слово "Reference" знакомо ему, но при запуске программы сообщает, что этот тип объекта ему неизвестен.

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

With ActiveDocument.VBProject.References
  For i = 1 To .Count
    Debug.Print "Имя проекта = " & .Item(i).Name
    Debug.Print "Полное имя файла = " & .Item(i).FullPath
  Next
End With

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

For i = 0 To .Count-1

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

Экспериментируйте

Из всего сказанного выше можно сделать два вывода:

  1. VB предоставляет достаточно гибкие возможности по управлением процессом "связывания" кода.

  2. Механизм связывания объектов в VB находится в затянувшейся стадии становления, тут видно много подводных камней.

  3. Возможно, установка сервисных пакетов обновления поможет устранить некоторые из отмеченных проблем и противоречий.

Так или иначе — будьте внимательны при написании программ и выборе тех или иных конструкций. И еще — экспериментируйте для поиска наиболее удобных для вас вариантов.

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