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

Семейство классов и наследование интерфейсов. Динамические классы

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

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

Семейство классов и реализация интерфейсов

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

Наследование

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

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

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

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

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

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

Вот пример семейства классов, связанных отношением наследования:

(рис 5.1) Дерево одиночного наследования

Множественное наследование описывается ациклическим графом:

(рис 5.2) Ациклический граф множественного наследования.

Мы уже говорили, что производный класс наследует состояние и поведение базового класса. Это значит, что все открытые ( Public ) элементы базового класса доступны в производном классе. Но главное, конечно, в том, что потомок может пойти дальше своего родителя. Производный класс расширяет свойства и поведение базового класса. В производном классе можно:

  • объявить новые переменные
  • объявить новые методы
  • переопределить (перегрузить, перекрыть) методы базового класса.
  • Переопределение метод а класса A в производном классе В, называемое также перегрузкой (overloading) или перекрытием (overriding), - это определение в классе B метода с именем, которое уже является именем некоторого метода класса А.

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

    Виртуальные методы и полиморфизм

    Как взаимодействуют между собой объекты разных классов, связанных отношением наследования? Возможны ли взаимные присваивания между объектами родительских классов и их потомками? Ответ на второй вопрос положителен лишь "наполовину". Совместимость по присваиванию обеспечивается лишь в одну сторону - от родителей к потомкам: объекту родительского класса может быть присвоено значение объекта-потомка - обратное недопустимо. Это и понятно: ведь потомок, сохраняя все свойства родителя, может иметь и другие свойства.

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

  • совместимость между формальным и фактическим параметром процедуры или функции. Формальный параметр может иметь тип базового класса, а фактический параметр - аргумент - при вызове функции может быть объектом любого производного класса.
  • совместимость указателей. Указатель может ссылаться на базовый класс, но при конструировании объекта в операторе " new " может быть вызван конструктор производного класса, что позволяет создать объект соответствующего класса. В более общей ситуации указателю базового класса может быть присвоен указатель производного класса.
  • Ниже мы покажем, что свойство совместимости по присваиванию в сочетании с возможностью определения виртуальных методов приводит к весьма полезным следствиям.

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

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

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

    Принципиально виртуальный метод от не виртуального отличается механизмом позднего связывания , применяемым для организации вызова виртуального метода. Во многих языках программирования методы могут быть как виртуальными, так и, в большинстве случаев, не виртуальными. В языке VBA это не так, здесь практически все методы виртуальные и для них применяется механизм позднего связывания. Это и понятно, поскольку VBA по существу интерпретируемый язык, а интерпретация подразумевает позднее связывание. Поэтому, чтобы Вы почувствовали разницу между виртуальными и не виртуальными методами, позвольте привести один пример на языке Visual C++. Рассмотрим следующий пример. Пусть имеются базовый класс Base и производный от него класс Derived, в которых определены виртуальный метод VirtMethod и невиртуальный метод NonVirtMethod. В базовом классе определена также полиморфная функция Test:

    class Base
    {
    public:
    virtual void VirtMethod ();	// печатает слово" Отец"
    void NonVirtMethod ();	// печатает слово" Мать"
    
    void Test ()
    {
    VirtMethod ();	// вызов виртуального метода
    NonVirtMethod ();	// вызов не виртуального метода
    }
    }
    
    class Derived: public Base
    {
    public:
    virtual void VirtMethod ();	// печатает слово" Сын"
    
    void NonVirtMethod ();	// печатает слово" Дочь"
    }

    Когда компилятор в базовом классе Base создает код для функции Test, то, анализируя вызов не виртуального метода NonVirtMethod(), он уже на этапе компиляции создаст обращение к методу NonVirtMethod класса Base.

    Совсем иначе он поступит, анализируя вызов виртуального метода VirtMethod. Для виртуального метода на этапе компиляции нельзя определить, метод какого класса следует вызывать. Это может быть метод класса Base или класса Derived. Возможно даже, что уже после того, как функция Test будет откомпилирована, программист определит новый класс - потомок класса Derived со своим виртуальным методом VirtMethod.

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

    // При создании объекта BaseItem вызывается конструктор класса Base.
    Base BaseItem;
    // При создании объекта DerivedItem вызывается конструктор класса Derived
    Derived DerivedItem;
    // Вызов функции Test
    BaseItem.Test();	// Будет напечатано "Отец" и "Мать"
    DerivedItem.Test();	// Будет напечатано "Сын" и "Мать".

    Функция Test является примером полиморфной функции. Заметьте: она имеет по умолчанию первым параметром указатель на объект базового класса ( this ). Продолжим рассмотрение нашего примера и покажем, как указатели обеспечивают полиморфизм:

    // Полиморфные указатели pItem1 и pItem2 ссылаются на базовый класс Base.
    // Вызывается конструктор и создается динамический объект класса Base.
    Base* pItem1 = new Base();
    // Вызывается конструктор и создается динамический объект класса Derived
    Base* pItem2 = new Derived();
    
    // Будет вызван метод класса Base и напечатано слово "Отец".
    pItem1 ->VirtMethod();
    // Будет вызван метод класса Derived и напечатано слово "Сын".
    pItem2 -> VirtMethod();

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

    Абстрактные классы

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

    Наследование и полиморфизм в Office 2000

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

    Наследование интерфейсов

    В Office 2000 на классах введено отношение "наследование интерфейса" . Здесь под интерфейсом понимается совокупность всех открытых ( Public ) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от "классического" наследования не транзитивно, - потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, - наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:

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

    Implements имя_родительского_класса

    При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все открытые методы, а для каждого открытого свойства, будет наследоваться пара процедур - свойств Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка " Implements ", в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.

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

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

  • Использовать обычную схему Copy - Paste, копируя реализацию родителя.
  • Реализовать наследование типичным для Office 2000 способом путем встраивания родительского объекта в класс - потомок. Тогда можно вызывать в нужном месте нужный метод родителя. Чуть позже мы рассмотрим проект " Люди и машины ", где используем этот прием.
  • Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения "наследование интерфейсов". Синтаксически, это определяется двумя обстоятельствами:

  • Заготовки методов, строящиеся автоматически, используют спецификатор Private. Это означает, что наследуемые методы не являются открытыми методами потомка и, следовательно, не входят в его интерфейс и не передаются далее внукам родителя.
  • Еще одна особенность состоит в том, что имена наследуемых методов содержат имя родителя. Это также означает, что наследуемый метод не становится собственным методом, который можно передать далее своим потомкам
  • Возникает естественный вопрос, если наследуемые методы не входят в интерфейс класса, а следовательно не могут быть вызваны объектами данного класса, то какой в них толк? Толк есть, поскольку эти методы могут быть все-таки вызваны объектами, правда, принадлежащими родительскому классу. Объект родительского класса, ссылающийся на своего потомка, может вызывать методы родителя, наследуемые потомком. Вот небольшой пример, где действую объекты трех классов Father, Son и GrandSon, связанные отношением наследования интерфейсов. Пусть определен абстрактный класс Father, интерфейс которого состоит из одного свойства, одного чистого метода и метода с заданной реализацией:

    'Class Father
    'Свойства класса
    Public MyProperty As String
    
    'Методы класса
    
    Public Sub MyPureMethod()
    'Чистый метод
    End Sub
    
    Public Sub MyRealMethod()
    		MsgBox ("It's the Father")
    End Sub

    Класс Son, наследующий интерфейс класса Father, по контракту должен реализовать две процедуры - свойства и два его метода. Мы дали только формальную реализацию, - в созданные автоматически заготовки добавили комментарии. Лишь в реализацию метода Father_MyPureMethod класса Son вставлена строка текста. Кроме того, в класс добавлен новый метод, определяющий собственный интерфейс этого класса. Вот описание этого класса:

    'Класс Son - Наследник класса Father
    Implements Father
    
    Private Property Let Father_MyProperty(ByVal RHS As String)
    'Реализация отложена
    
    End Property
    
    Private Property Get Father_MyProperty() As String
    'Реализация отложена
    
    End Property
    
    Private Sub Father_MyPureMethod()
    		MsgBox ("It's the Son")
    
    End Sub
    
    Private Sub Father_MyRealMethod()
    'Реализация отложена
    
    End Sub	
    
    Public Sub SonNewMethod()
    		MsgBox ("It's the Son of his Father")
    End Sub

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

    Приведем теперь описание класса GrandSon - потомка классов Son и Father. Заметьте, класс явно определяет обоих своих родителей. Опять-таки мы ограничились формальной реализацией, определив лишь реализацию метода Father_MyRealMethod:

    Option Explicit
    
    Implements Father
    Implements Son
    
    Private Property Let Father_MyProperty(ByVal RHS As String)
    'Реализация отложена
    
    End Property
    
    Private Property Get Father_MyProperty() As String
    'Реализация отложена
    
    End Property
    
    Private Sub Father_MyPureMethod()
    'Реализация отложена
    
    End Sub
    
    Private Sub Father_MyRealMethod()
    'Реализация отложена
    MsgBox ("It's the GrandSon")
    End Sub
    
    Private Sub Son_SonNewMethod()
    'Реализация отложена
    
    End Sub

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

    Public Sub Family()
    
    Dim F As New Father, S As New Son, GS As New GrandSon
    Dim Grand As Father, GrandS As Son
    
    Set Grand = F
    Grand.MyProperty = "Flat"
    Grand.MyRealMethod
    Grand.MyPureMethod
    Debug.Print Grand.MyProperty
    Set Grand = S
    Grand.MyProperty = "Flat"
    Grand.MyRealMethod
    Grand.MyPureMethod
    If TypeOf Grand Is Son Then
    		Set GrandS = Grand: GrandS.SonNewMethod
    End If
    Debug.Print Grand.MyProperty
    
    Set Grand = GS
    If TypeOf Grand Is GrandSon Then
    		Set GrandS = Grand: GrandS.SonNewMethod
    End If
    		Grand.MyProperty = "Flat"
    		Grand.MyRealMethod
    		Grand.MyPureMethod
    Debug.Print Grand.MyProperty
    
    End Sub

    В этом примере объект Grand класса Father связывается поочередно с объектами класса Father, Son, GrandSon и всякий раз вызываются методы родительского класса, унаследованные потомками. Обратите внимание на конструкцию TypeOf - Is, позволяющую определить текущий тип объекта. В процессе работы этой процедуры будут открываться диалоговые окна, уведомляющие, что мы встретились с объектами Father, Son, Son of his Father, GrandSon. При печати свойств лишь однажды будет напечатано значение Flat.

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

    Полиморфизм семейства классов

    Два фактора обеспечивают полиморфизм:

  • Присваивание снизу - вверх.
  • Механизм позднего связывания.
  • Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов X.VirtMethod, то по этому тексту ничего нельзя сказать о результате этого вызова, поскольку по первому свойству полиморфизма с X может быть связан объект любого из классов семейства, а благодаря второму свойству будет найден виртуальный метод именно этого класса.

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

    Наследование интерфейсов позволяет организовать присваивание снизу - вверх и, как следствие, позволяет работать с группой элементов, каждый из которых может принадлежать различным классам семейства. Поскольку позднее связывание и ранее было характерно для VBA, то полиморфизм в Office 2000 реализован в полном объеме.

    Итак, подводя некоторый итог, еще раз отметим, что полиморфизм обеспечивает два важных достоинства при работе с семейством классов:

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

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

    Давайте перейдем от слов к делу и посмотрим, как это все работает на более или менее содержательном примере.

    Проект "Люди и Машины"

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

    (рис 5.3) Схема семейства классов "Люди и Машины"

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

    Option Explicit
    'Класс Машина
    'Свойства класса
    Private Марка As String
    Private ДатаВыпуска As Date
    Private Цвет As String
    
    'Конструкторы класса
    
    Private Sub Class_Initialize()
    		Марка = "Форд"
    		ДатаВыпуска = "20.07.1925"
    		Цвет = "Вишневый"
    End Sub
    
    Public Sub НоваяМашина(M As String, D As Date, C As String)
    		Марка = M
    		ДатаВыпуска = D
    		Цвет = C
    End Sub
    
    'Методы класса
    Public Sub PrintDataCar()
    		Debug.Print "Марка = ", Марка
    		Debug.Print "ДатаВыпуска = ", ДатаВыпуска
    		Debug.Print "Цвет = ", Цвет
    		
    End Sub
    
    Public Property Get МаркаМашины() As String
    		МаркаМашины = Марка
    End Property
    
    Public Property Get ЦветМашины() As String
    		ЦветМашины = Цвет
    End Property
    
    Public Property Get ДатаВыпускаМашины() As Date
    		ДатаВыпускаМашины = ДатаВыпуска
    End Property

    У класса Машина имеется:

  • три закрытых свойства, обладающие статусом Read only,
  • два конструктора, - закрытый конструктор по умолчанию и открытый конструктор НоваяМашина,
  • один типичный для классов открытый метод, задающий печать свойств - PrintDataCar
  • Интерфейс класса составляют два открытых метода, они и будут наследоваться классом - потомком. Класс - потомок ВладелецМашины значительно больше унаследует от своего другого родителя - класса Личность. Вот определение класса ВладелецМашины:

    Option Explicit
    'Класс ВладелецМашины
    'Наследует интерфейсы классов Личность и Машина
    Implements Машина
    Implements Личность
    
    'Свойства класса
    Private Сам As Личность
    Private ЕгоМашина As Машина
    
    Private Sub Class_Initialize()
     Set Сам = New Личность
     Set ЕгоМашина = New Машина
    End Sub
    
    'Реализация интерфейсов класса Личность
    Private Sub Личность_CopyPerson(You As Личность)
    		Сам.CopyPerson (You)
    End Sub
    
    Private Sub Личность_InitPerson(ByVal FN As String, ByVal LN As String, ByVal DoB As Date)
     'Инициализация личности
    		Сам.InitPerson FN, LN, DoB
    End Sub
    
    Private Sub Личность_PrintPerson()
     'Печать в отладочном окне Immediate
    		Сам.PrintPerson
    End Sub
    
    Private Sub Личность_SayWhoIs()
    '	Вывод сообщения о поле и возрасте владельца машины
    		Dim StrMsg As String
    		StrMsg = "Думаю, Владелец машины марки: "  _
    		ЕгоМашина.МаркаМашины  " это - "
    		If Сам.WhoIs Then
    			If Year(Сам.ВашаДатаРождения) > 1967 Then
    				StrMsg = StrMsg  "молодая девушка!"
    			Else: StrMsg = StrMsg  "женщина!"
    			End If
    		Else
    			If Year(Сам.ВашаДатаРождения) > 1967 Then
    				StrMsg = StrMsg  "молодой человек!"
    			Else: StrMsg = StrMsg  "мужчина!"
    			End If
    		End If
    		MsgBox (StrMsg)
    End Sub
    
    Private Function Личность_WhoIs() As Boolean
    		Сам.WhoIs
    End Function
    
    Private Property Let Личность_ВашаДатаРождения(ByVal NewValue As Date)
    		Сам.ВашаДатаРождения = NewValue
    End Property
    
    Private Property Get Личность_ВашаДатаРождения() As Date
    		'Зажигает событие ДеньРождения
    		'в зависимости от значения текущей даты
    		Личность_ВашаДатаРождения = Сам.ВашаДатаРождения
    End Property
    
    Private Property Let Личность_ВашаФамилия(ByVal NewValue As String)
    	'Зажигает событие ИзменениеФамилии
    		Сам.ВашаФамилия = NewValue
    End Property
    
    Private Property Get Личность_ВашаФамилия() As String
    		Личность_ВашаФамилия = Сам.ВашаФамилия
    End Property
    
    Private Property Let Личность_ВашеИмя(ByVal NewValue As String)
    		Сам.ВашеИмя = NewValue
    End Property
    
    Private Property Get Личность_ВашеИмя() As String
    		Личность_ВашеИмя = Сам.ВашеИмя
    End Property
    
    Private Property Let Личность_ВашеОтчество(ByVal NewValue As String)
    	Сам.ВашеОтчество = NewValue
    End Property
    
    Private Property Get Личность_ВашеОтчество() As String
    		Личность_ВашеОтчество = Сам.ВашеОтчество
    End Property
    
    'Реализация интерфейсов класса Машина
    Private Property Get Машина_ДатаВыпускаМашины() As Date
    	Машина_ДатаВыпускаМашины = ЕгоМашина.ДатаВыпускаМашины
    End Property
    
    Private Property Get Машина_МаркаМашины() As String
    		Машина_МаркаМашины = ЕгоМашина.МаркаМашины
    End Property
    
    Private Property Get Машина_ЦветМашины() As String
    		Машина_ЦветМашины = ЕгоМашина.ЦветМашины
    End Property
    
    Private Sub Машина_PrintDataCar()
    		ЕгоМашина.PrintDataCar
     End Sub
    
    Private Sub Машина_НоваяМашина(M As String, D As Date, C As String)
     ЕгоМашина.НоваяМашина M, D, C
    End Sub
    
    'Собственный интерфейс класса ВладелецМашины
    'Public методы - интерфейс Владельца машины
    Public Sub InitCarOwner(FN As String, LN As String, DoB As Date, _
    		Marka As String, DB As Date, Color As String)
    		'Инициализация данных о хозяине и его машине
    		Личность_InitPerson FN, LN, DoB
    		Машина_НоваяМашина Marka, DB, Color
    End Sub
    
     Public Sub ConnectOwnerAndCar(pers As Личность, car As Машина)
    		'соединяет данные о хозяине и его новой машине
    		Сам.CopyPerson pers
    		Машина_НоваяМашина car.МаркаМашины, car.ДатаВыпускаМашины, _
    			car.ЦветМашины
    End Sub
    
    Public Sub PrintOwnerData()
    		Личность_PrintPerson
    		Debug.Print " владеет машиной: "
    		Машина_PrintDataCar
    End Sub

    Прокомментируем этот довольно длинный текст. Вот на какие моменты следует обратить внимание:

  • Для того, чтобы реализовать полноценное наследование свойств и поведения родителей, а не только наследование интерфейсов, используется встраивание, - в классе определены свойства Сам класса Личность и свойство ЕгоМашина класса Машина.
  • В конструкторе по умолчанию Class_Initialize, вызываемом при создании объекта класса ВладелецМашины, объекты Сам и ЕгоМашина инициализируются, - будут вызваны их конструкторы по умолчанию класса Личность и класса Машина.
  • При наследовании интерфейсов класса Личность почти для всех методов наследовалось поведение родительского класса. Реализуется это достаточно просто - вызывается соответствующий метод объекта Сам.
  • Для метода SayWhoIs поведение переопределено. В данном случае при вызове метод учитывает специфику класса и сообщает некоторые данные, как о владельце, так и о его машине. Так что при вызове этого метода у просто личностей и у личностей, являющихся владельцами машин, результат будет различным.
  • Поскольку родительский класс допускает события, то и у потомка при вызове методов зажигаются соответствующие события.
  • Интерфейс класса Машина реализуется подобным образом. Полностью наследуется поведение родительского класса.
  • Собственный интерфейс класса довольно прост. У объектов этого класса нет новых дополнительных свойств, - все спрятано в свойствах встроенных объектов Сам и ЕгоМашина. Интерфейс составляют два конструктора и метод, осуществляющий печать данных. Обратите внимание, один конструктор InitCarOwner позволяет сконструировать новый объект класса по терминальным данным, характеризующим личность и его машину. Второй конструктор ConnectOwnerAndCar в качестве параметров использует ранее созданные объекты классов Личность и Машина, соединяя эти два объекта в один объект класса ВладелецМашины.
  • На рисунке 5.4 отражен один из моментов проектирования класса. Можно видеть, что в окне кода список объектов содержит объекты Class, Личность и Машина. При выборе объекта Class правый раскрывающийся список покажет список стандартных событий класса, а при выборе объектов Личность и Машина будет раскрываться список наследуемых методов.

    (рис 5.4) Проектирование класса ВладелецМашины

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

    Option Explicit
    'Модуль Примеры
    Public FriendOne As New Личность
    Public FriendTwo As New Личность
    Public FriendThree As New Личность
    Public carOne As New Машина
    Public carTwo As New Машина
    Public carThree As New Машина
    Public OwnerOne As New ВладелецМашины
    Public OwnerTwo As New ВладелецМашины
    Public OwnerThree As New ВладелецМашины
    Public FOne As New Личности
    
    Public Sub Люди()
    'Вызывается конструктор с параметрами
    'и происходит знакомство с объектами
    FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _
    		DoB:="21.05.39"
    FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _
    		DoB:="22.03.79"
    FriendThree.InitPerson FN:="Остап", LN:="Бендер", DoB:="23.07.1910"
    FriendOne.PrintPerson
    FriendTwo.PrintPerson
    FriendOne.SayWhoIs
    FriendTwo.SayWhoIs
    'Связывание с двойниками.
    'Теперь объекты могут реагировать на события!
    FOne.Connect
    End Sub
    
    Public Sub Cars()
    'Вызывается конструктор с параметрами
    carOne.НоваяМашина "Антилопа", "12.12.12", "Неопределенный"
    carTwo.НоваяМашина "Москвич", "12.11.98", "Морская волна"
    carThree.НоваяМашина "Jeep", "23.05.97", "Orange"
    End Sub
    
    Public Sub CarOwners()
    		OwnerOne.ConnectOwnerAndCar FriendOne, carTwo
    		OwnerTwo.ConnectOwnerAndCar FriendThree, carOne
    		OwnerThree.InitCarOwner FN:="Юрий", LN:="Вегера", _
    		DoB:="21.08.34", Marka:="Газ69", DB:="20.01.76", Color:="Зеленый"
    		OwnerOne.PrintOwnerData
    		OwnerTwo.PrintOwnerData
    		OwnerThree.PrintOwnerData
    End Sub
    Public Sub CallEvents()
    		Dim DoB As Date
    		'Вызов методов приведет к возникновению событий!
    		'При замене фамилии возникнет событие ИзменениеФамилии
    		'Заметьте, не всегда фамилия будет изменена!
    		FriendOne.ВашаФамилия = "Фидотов"
    		FriendTwo.ВашаФамилия = "Волконская"
    		'При попытке узнать дату рождения
    		'может быть вызван обработчик события ДеньРождения.
    		DoB = FriendOne.ВашаДатаРождения
    		Debug.Print DoB
    		DoB = FriendTwo.ВашаДатаРождения
    		Debug.Print DoB
    		FriendOne.PrintPerson
    		FriendTwo.PrintPerson
    		
    	'События не наследуются
    		Set FriendOne = OwnerTwo
    		'Нельзя связать теперь объект FriendOne с двойником
    		'FOne.Connect
    		FriendOne.ВашаФамилия = "Воробьянинов"
    		FriendOne.PrintPerson
    
    End Sub
    Public Sub Группа()
    		Const SizeGroup = 6
    		Const SizeGarage = 6
    		Dim i As Byte
    		Dim Group(1 To SizeGroup) As Личность
    		Dim Гараж(1 To SizeGarage) As Машина
    		
    		Set Group(1) = FriendOne
    		Set Group(2) = FriendTwo
    		Set Group(3) = FriendThree
    		Set Group(4) = OwnerOne
    		Set Group(5) = OwnerTwo
    		Set Group(6) = OwnerThree
    		For i = 1 To SizeGroup
    			Group(i).SayWhoIs
    		Next i
    
    		Set Гараж(1) = carOne
    		Set Гараж(2) = carTwo
    		Set Гараж(3) = carThree
    		Set Гараж(4) = OwnerOne
    		Set Гараж(5) = OwnerTwo
    		Set Гараж(6) = OwnerThree
    		For i = 1 To SizeGarage
    			Гараж(i).PrintDataCar
    		Next i
    End Sub
    
    Public Sub ЛюдиИМашины()
    		Люди
    		Cars
    		CarOwners
    		Группа
    		PolyMorf FriendTwo
    		PolyMorf OwnerTwo
    End Sub
    
    Public Sub PolyMorf(One As Личность)
    		One.SayWhoIs
    End Sub

    Дадим теперь комментарии к этому тексту:

  • В модуле Примеры вначале объявляются по три объекта классов Личность, Машина и ВладелецМашины. В методах Люди, Cars, CarOwners происходит инициализация этих объектов. Некоторые из этих личностей уже встречались в предыдущих примерах, но появился Остап Бендер и его знаменитая машина.
  • Обратите внимание на создание объектов Owner класса ВладелецМашины, они создаются разными конструкторами.
  • Основную работу выполняет метод Группа. Здесь создается массив элементов Group из трех объектов Friend и трех объектов Owner, принадлежащих разным классам. Тем не менее все эти объекты обрабатываются в едином цикле и для них вызывается метод SayWhoIs, который, как мы говорили ранее по-разному работает для объектов класса Личность и класса ВладелецМашины. Так что простой цикл по элементам Group демонстрирует реализацию полиморфизма.
  • В методе Группа создана и продемонстрирована работа с еще одной группой элементов, - массивом Garage. Эту группу составляют объекты класса Машина и объекты класса ВладелецМашины. Обратите внимание, одни и те же объекты Owner входят в обе группы элементов и могут быть обработаны нужным образом.
  • В процессе обработки массива Garage вызывается метод PrintDataCar, который унаследован объектами класса ВладелецМашины.
  • В модуле Примеры приведена полиморфная функция PolyMorf(One As Личность ), с формальным параметром родительского класса Личность. Она дважды вызывается в процедуре ЛюдиИМашины с фактическими параметрами разных классов.
  • Процедура ЛюдиИМашины является процедурой, организующей всю работу, она поочередно запускает все перечисленные ранее процедуры. Сама она вызывается в обработчике события Click командной кнопки, встроенной в наш тестовый документ.
  • В процессе выполнения этой процедуры откроется серия диалоговых окон, где вначале будет сказано, кто такой Федотов и Катя, будет спрошено отчество Вегеры Юрия, затем опять будет рассказано о Федотове, Кате и Бендере, после чего появится информация о владельцах машин разных марок. На последнем этапе опять будет рассказано о Кате и владельце машины "Антилопа". Нам осталось привести результаты отладочной печати, появляющиеся при работе этой процедуры:

    Станислав                 Федотов            родился         21.05.39 
    Катя                      Павлова            родилась        22.03.79 
    Станислав                 Федотов            родился         21.05.39 
     владеет машиной: 
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Остап                     Бендер             родился         23.07.1910 
     владеет машиной: 
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Юрий            Алексеевич      Вегера       родился         21.08.34 
     владеет машиной: 
    Марка =			Газ69
    ДатаВыпуска =					20.01.76 
    Цвет =			Зеленый
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Марка =			Jeep
    ДатаВыпуска =					23.05.97 
    Цвет =			Orange
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Марка =			Газ69
    ДатаВыпуска =					20.01.76 
    Цвет =			Зеленый

    Как мы уже отмечали, родительский класс Личность реагирует на события. Это не мешает наследовать его интерфейсы, но не означает, что потомки будут наследовать события своего родителя. Чтобы убедиться, что зажигание событий не мешает работе потомков и не влияет на их работу, мы включили процедуру CallEvents. В процессе ее работы возникают события у объектов родительского класса и они правильно обрабатываются. Но, заметьте, после того как объект FriendOne был связан с объектом - потомком FriendOwner, при смене фамилии соответствующее событие уже не возникало, так что ничто не помешало Остапу Бендеру сменить свою фамилию. По ходу работы этой процедуры в появляющихся диалоговых окнах будет сообщено об отказе смены фамилии Федотову и появится поздравление Кате Павловой в связи с замужеством. Приведем результаты отладочной печати при работе этой процедуры:

    21.05.39 
    22.03.79 
    Станислав          Федотов              родился            21.05.39
    Катя               Волконская           родилась           22.03.79
    Остап              Воробьянинов         родился            23.07.1910

    Этот пример демонстрирует основные возможности работы с семейством классов, появившиеся в связи с введением механизма наследования интерфейсов.

    Создание динамических структур данных

    Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки, деки и очереди, деревья бинарные и сбалансированные. В современных языках программирования некоторые из таких структур стали такой же частью языка, как и массивы. Реализуется такая динамическая структура чаще всего в виде класса. Прекрасным примером является реализация самой системы Office 2000. Как мы не раз говорили, Office 2000 можно рассматривать как семейство классов объектов. Так вот, почти для каждого класса объектов, существует и класс, задающий коллекцию этих объектов. Классы - коллекции составляют почти половину классов Office 2000. Все они устроены похожим образом и имеют некоторый стандартный набор методов, позволяющих удалить или добавить новый элемент в коллекцию, получить элемент из коллекции, зная его порядковый номер или ключ элемента. Некоторые коллекции имеют специфические свойства и поведение, обусловленное спецификой самих элементов, хранящихся в коллекции. В ряде случаев, коллекции могут допускать хранение элементов разных классов. Обилие коллекций в Office 2000 первоначально раздражает, поскольку хотелось бы иметь единый класс со свойствами и методами, не зависящими от природы элементов, составляющих коллекцию. Но со временем ко всему привыкаешь и начинаешь принимать как должное, что у каждого класса объектов своя коллекция. Благодаря подсказкам по ходу написания программы и легко доступной справочной системе, не требуется держать в памяти специфику каждой коллекции.

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

    Встроенный динамический класс Collection

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

    У класса Collection одно свойство - Count и 3 метода: Add, Item, Remove. Рассмотрим их подробнее:

  • Свойство Count возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод Add (item, key, before, after) добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции.
  • Метод Remove(key) удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
  • Обратите внимание на мощность методов класса. Они позволяют реализовать различные классические динамические структуры. Конечно же, совсем просто реализуется обычный односвязный список с возможностью добавления элементов в начало или конец списка. Но можно реализовать и значительно более мощную структуру, называемую словарем. Словарь представляет собой связанную совокупность пар элементов. Первый элемент пары называется ключом, второй - информационным полем. Особенностью словарей является то, что, зная ключ элемента, можно получить доступ к информационному полю. При этом, обратите внимание, в классе Collection на ключ не накладывается никаких ограничений, - это обычная строка. Немаловажно, что есть и альтернативный способ прямого доступа к элементам коллекции по индексу, когда в качестве ключа выступает порядковый номер элемента в коллекции. Таким образом, коллекция соединяет в себе достоинства списков и массивов.

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

    Sub TestOfCollection()
    'Так объявляются объекты (переменные) типа Collection
    	Dim MyCollection As New Collection
    'Объявление обычных локальных переменных
    	Dim i As Integer
    	Dim N As Long
    'Оператор With позволяет избежать многократного указания имени объекта
    With MyCollection
    N =.Count
    Debug.Print" Число элементов пустой коллекции =", N
    ' Добавление элементов в конец списка. 
    'Элементы имеют индексы, но не имеют ключа.
    	.Add (2)
    	.Add (4)
    	.Add (6)
    'Добавление нечетных элементов на свои места.
    'Заметьте, как указывается позиция
    'добавления c использованием параметров - before и after
    ' Добавляемые элементы имеют строковый тип и обладают ключом
    	.Add" один"," first", 1	' before (перед первым элементом)
    	.Add" три"," third",, 2	'after (после второго)
    	.Add" пять"," fifth",, 4
    N =.Count
    Debug.Print" Число элементов после 6-и вызовов метода Add", N
    Debug.Print" Элементы коллекции:" 
    ' Отладочная печать созданной коллекции из шести элементов.
    	For i = 1 To MyCollection.Count
    		Debug.Print MyCollection(i)
    	Next
    ' Удаление 4-го и 5-го элементов по заданному индексу и ключу.
    	.Remove 4
    	.Remove" fifth" 
    N =.Count
    Debug.Print" Число элементов после двух вызовов метода Remove=", N
    Debug.Print" Элементы коллекции:" 
    'И снова печать коллекции, в которой теперь четыре элемента.
    For i = 1 To MyCollection.Count
    Debug.Print MyCollection(i)
    Next
    End With
    End Sub

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

    Число элементов пустой коллекции = 0
    Число элементов после 6-и вызовов метода Add = 6
    Элементы коллекции:	один	2	три	4	пять	6
    Число элементов после двух вызовов метода Remove = 4
    Элементы коллекции:	один	2	три	6

    Подчеркнем еще раз основные свойства класса Collection:

  • Класс позволяет объединять в коллекцию элементы разных типов, хотя чаще применяются однотипные коллекции.
  • Класс объединяет в себе свойства линейного списка, динамического массива и структуры, называемой словарем , или отображением (map).
  • Это список, поскольку определена операция Add, позволяющая динамически добавлять элементы в конец списка.
  • Это динамический массив, поскольку все элементы индексированы и к ним возможен прямой доступ по индексу. С другой стороны, размер массива не фиксируется и динамически изменяется при добавлении и удалении элементов.
  • Это словарь, поскольку добавляемые элементы могут иметь ключ. Прямой доступ к элементам возможен как по индексу, так и по ключу. Под "прямым" здесь понимается доступ, основанный на применении функций расстановки (хэширования). Конкретный вид этих функций, определяющий эффективность поиска, - "секрет фирмы".
  • В классе определены методы Add, Item, Remove и свойство Count.
  • Элементами коллекции могут быть данные любых типов, встроенных в VBA. Не менее важно, что допускается строить коллекцию из объектов, классы которых определены программистом. В предыдущих примерах этой лекции мы использовали для построения групп массивы только по той причине, что еще не ввели понятие коллекции. Более разумно было бы использование коллекций при работе с личностями, владельцами машин и самими машинами. Построим пример, в котором действует коллекция личностей:

    Public Sub Collection()
    		'Создание и работа с коллекцией личностей
    		Dim Личности As New Collection
    'Работа с коллекцией, как со списком
    		Dim Адам As New Личность
    		Адам.InitPerson "Адам", "Первый Человек", #1/1/100#
    		Личности.Add Адам
    		Dim Ной As New Личность
    		Ной.InitPerson "Ной", "Праведник", #1/1/100#
    		Личности.Add Ной
    'Работа с коллекцией, как с динамическим массивом
    		Dim Шекспир As New Личность
    		Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564#
    		Личности.Add Item:=Шекспир, After:=2
    		Dim Гомер As New Личность
    		Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100#
    		Личности.Add Item:=Гомер, Before:=3
    		Личности(4).SayWhoIs
    'Работа с коллекцией, как со словарем
    		Dim Пушкин As New Личность
    		Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799#
    		Личности.Add Item:=Пушкин, Key:="Гений"
    		Dim Булгаков As New Личность
    		Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891#
    		Личности.Add Item:=Булгаков, Key:="Мастер"
    		Debug.Print Личности("Гений").ВашаФамилия, " - это Гений!"
    		Debug.Print Личности("Мастер").ВашаФамилия, " - это Мастер!"
    'Печать всего списка
    		Dim I As Byte
    		For I = 1 To Личности.Count
    			Личности(I).PrintPerson
    		Next I
    End Sub

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

    Пушкин                - это Гений!
    Булгаков              - это Мастер!
    Адам                    Первый Человек           родился           01.01.100 
    Ной                     Праведник                родился           01.01.100 
    Гомер                   Великий Слепой           родился           01.01.100 
    Вильям                  Шекспир                  родился           23.04.1564 
    Александр               Пушкин                   родился           06.06.1799 
    Михаил                  Булгаков                 родился           23.01.1891

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

    Создание собственных динамических классов

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

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

  • информационную часть элемента списка;
  • структуру элемента списка;
  • сам список, в том числе и операции, над ним определенные.
  • Для определенности рассмотрим список, хранящий информацию о "личностях". Тогда можно считать первую часть нашей задачи решенной,- класс Личность уже создан, и мы его просто используем. Теперь создадим класс с именем ЭлементСпискаЛичностей, задающий структуру элемента (запись). Этот класс прост, поскольку элементы линейного односвязного списка состоят из двух полей - информационного и поля указателя соседнего элемента. Никакие методы над этим классом обычно не определяются. Его полное описание:

    'Класс ЭлементСпискаЛичностей содержит только два поля
    Public Сам As Личность
    Public Друг As ЭлементСпискаЛичностей

    В определении класса ЭлементСпискаЛичностей свойство Друг является объектом того же класса. VBA допускает такую рекурсию в определении - без нее списковую структуру не организовать.

    Осталось определить класс СписокЛичностей, задающий сам список. Главное на этом этапе - спроектировать операции над списком. Мы ограничимся набором традиционных методов:

  • AddFirst (F As Личность) - добавляет элемент в начало списка;
  • AddLast (F As Личность) - добавляет элемент в конец списка;
  • Initialize - конструктор по умолчанию, инициализирует список;
  • PrintList() - печатает содержимое элементов списка;
  • ClearList() - очищает список.
  • Методы диктуют и состав данных (свойств) класса СписокЛичностей. Нужны минимум два указателя на начало и конец списка: First и Last. Эти свойства разумно закрыть от внешнего использования. Мы добавим к ним открытую переменную Count, следящую за числом элементов списка. Приведем теперь описание свойств и методов этого класса:

    Option Explicit
    'Определение класса СписокЛичностей
    'Свойства
    		Private First As ЭлементСпискаЛичностей
    		Private Last As ЭлементСпискаЛичностей
    		Public Count As Integer
    'Методы
    Private Sub Class_Initialize()
    		Set First = Nothing
    		Set Last = Nothing
    		Count = 0
    End Sub
    
    Public Sub AddFirst(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = First
    		If First Is Nothing Then
    			Set Last = Elem
    		End If
    		Set First = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub PrintList()
    		Dim P As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			Q.PrintPerson
    			Set P = P.Друг
    		Wend
    End Sub
    
    Public Sub AddLast(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = Nothing
    		If First Is Nothing Then
    			Set First = Elem
    		Else
    			Set Last.Друг = Elem
    		End If
    		Set Last = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub ClearList()
    	'Попытка освободить память не достигает успеха из-за отсутствия
    	'соответствующего оператора.
    		Dim P As ЭлементСпискаЛичностей, R As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			'Unload Q
    			Set R = P
    			Set P = P.Друг
    			'Unload R
    		Wend
    	'Обнуление указателей
    		Set First = Nothing
    		Set Last = Nothing
    		Count = 0
    End Sub

    Теперь несколько замечаний по поводу реализации методов:

  • При добавлении нового элемента в методах AddFirst и AddLast вначале создается новый пустой элемент списка; поскольку в объявлении элемента используется спецификатор New. Создается также новый элемент для информационного поля, куда копируется информация, переданная при вызове методов Add, - здесь пригодился метод CopyPerson класса Личность. Заметьте, что в отличие от класса Collection, создается копия элемента, а не используется ссылка. Поэтому состояние элементов списка не зависит от внешних изменений. Изменять содержимое элементов списка можно только с помощью методов списка. Хотя мы и не спроектировали такие методы, понятно, что это нетрудно сделать.
  • При печати списка последовательно проходятся все его элементы, для каждого из них вызывается метод PrintPerson, определенный в классе Личность.
  • Особо остановимся на методе ClearList, в котором можно установить нулевые значения указателей ( Nothing ), но нельзя освободить память, занятую элементами списка. Мы специально делаем такую попытку, не приводящую к успеху, чтобы заострить Ваше внимание на этом вопросе. В большинстве языков программирования, позволяющих создавать динамические структуры данных, всегда есть пара взаимосвязанных операций над памятью: "Выделить память" и "Освободить память" ( New-Delete или, например, Get-Free ). Операции эти выполняются динамически, и выделение и освобождение памяти производится по требованию программиста при выполнении программы. В VBA дело обстоит не так,- здесь есть New, но нет Delete. Связано это, конечно, с особенностями VBA, который, не будучи классическим языком компиляторного типа, представляет нечто среднее между компилятором и интерпретатором. Поэтому здесь освобождение памяти происходит в соответствии с обычными для переменных правилами, определяющими их время жизни. Так что в VBA программист не должен заботиться об освобождении памяти, занятой его динамическими структурами, - это делается автоматически без его участия. Так не проходит освобождение динамической памяти с помощью метода Unload, выполняющего операцию освобождения памяти, но над объектами другого рода.
  • Приведем теперь достаточно простой пример, позволяющий построить список в процессе диалога с пользователем и в конце распечатать данные, хранящиеся в построенном списке:

    Public Sub ВводСписка()
    	'Создание списка в диалоге с пользователем
    	Dim Личности As New СписокЛичностей
    	Dim ЭтоЛичность As New Личность
    	Dim Имя As String, Фамилия As String, Дата As Date
    	If MsgBox("Начнем создавать список личностей?", vbYesNo) = vbYes Then
    		Do
    			ЭтоЛичность.ВашеИмя = InputBox("Введите имя личности")
    			ЭтоЛичность.ВашаФамилия = InputBox("Введите Фамилию")
    			ЭтоЛичность.ВашаДатаРождения = InputBox("Введите дату рождения ")
    			Личности.AddLast ЭтоЛичность
    		Loop Until MsgBox("Продолжим создавать список личностей?", vbYesNo) = vbNo
    	Личности.PrintList
    	End If
    End Sub

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

    Обертывание коллекции VBA

    Обратите внимание, наш только что приведенный пример ВводСписка, иллюстрирующий часто возникающую на практике задачу, невозможно реализовать, используя встроенный класс Collection.Причина этого уже пояснялась, - коллекция хранит ссылки на созданные вне нее элементы, она не обладает собственной памятью. Поэтому пользователь сам должен заботиться о создании и хранении элементов. Конечно, хотелось бы поручить эту задачу самой коллекции, как это делается в созданном динамическом классе СписокЛичностей. С другой стороны этот класс имеет значительно менее мощные методы AddFirst и AddLast в сравнении с методом Add класса Collection. Класс СписокЛичностей не позволяет создавать словари, он значительно менее эффективен, чем класс Collection в тех случаях, когда требуется обеспечить прямой доступ к элементам списка. Возникает вопрос, нельзя ли соединить достоинства двух подходов? Оказывается, можно. Для этого применяется стандартная при работе с классами технология обертывания. Суть ее в том, что стандартный объект, например, элемент управления встраивается в собственный класс. Методы класса могут вызывать методы объекта, сохраняя тем самым мощь стандартного объекта. С другой стороны можно добавить новые методы или расширить функциональность существующих. Мы продемонстрируем эту технологию, показав как можно строить собственные коллекции для объектов любого класса. Конечно, нам не удастся построить универсальный класс, расширяющий возможности класса Collection, но нам удастся показать, что для каждого собственного класса элементов, можно построить коллекцию, обладающую всеми свойствами обычной коллекции, но, вместе с тем, хранящую элементы в самой коллекции.

    Не будем изобретать новых примеров и построим класс КоллекцияЛичностей. Этот класс будет обладать всеми стандартными свойствами и методами класса Collection, дополненными методами класса СписокЛичностей. Он будет иметь:

  • Свойство Count - возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод AddPerson (item, key, before, after) - добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции. Заметьте, несмотря на внешнее сходство метод AddPerson отличается от метода Add класса Collection. Во-первых, параметр Item будет иметь строго фиксированный тип Личность. Во-вторых, есть различия в реализации. Коллекция будет хранить не ссылку на элемент Item класса Личность, а копию этого элемента.
  • Метод Remove(key) - удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) - возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
  • Метод PrintList() - печатает элементы списка в его текущем состоянии.
  • Метод PrintHystory - печатает историю создания списка, - все его элементы, даже если они и были удалены впоследствии.
  • Наряду с открытыми свойствами и методами наш класс будет содержать и закрытые свойства и методы:

  • Свойство First - указатель на начало списка, хранящего элементы коллекции.
  • Событие Initialize - конструктор по умолчанию, инициализирует список.
  • Метод AddFirst - создает копии элементов и добавляет их в список, представляющий внутреннюю память коллекции.
  • Вот полное описание класса КоллекцияЛичностей, обертывающего класс Collection. Класс сохранил все мощные свойства стандартной коллекции и, к тому же, приобрел новые привлекательные свойства:

    Option Explicit
    
    'Определение класса КоллекцияЛичностей
    'Свойства
    		Private First As ЭлементСпискаЛичностей
    		Private Count As Long
    		'Обратите внимание: встраиваем коллекцию
    		Private Persons As Collection
    		
    'Методы
    Private Sub Class_Initialize()
    		Set First = Nothing
    		Count = 0
    		'при создании объекта класса КоллекцияЛичностей создается
    		'и внутренняя коллекция для хранения его элементов
    		Set Persons = New Collection
    End Sub
    
    Private Sub AddFirst(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = First
    		Set First = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub PrintList()
    		'Печать текущего состояния списка
    		Dim i As Long
    		For i = 1 To Persons.Count
    			Persons(i).PrintPerson
    		Next i
    End Sub
    
    Public Sub AddPerson(Item As Личность, Optional key As String = "", _
    			Optional before As Long = 0, Optional after As Long = 0)
    		Dim P As Личность
    		'Вначале добавляем элемент в линейный список,
    		'используя внутренний метод AddFirst
    		AddFirst Item
    		
    		'Разбор случаев вызова
    		Set P = First.Сам
    		If key <> "" Then
    			Persons.Add P, key
    		ElseIf before <> 0 Then
    			Persons.Add P,, before
    		ElseIf after <> 0 Then
    			Persons.Add P,,, after
    		Else
    			Persons.Add P
    		End If
    		Count = Persons.Count
    End Sub
    Public Property Get Количество() As Long
    	Количество = Count
    End Property
    
    
    Public Sub Remove(key As Variant)
    		Persons.Remove key
    		Count = Persons.Count
    End Sub
    
    Public Function Item(key As Variant) As Личность
    		Set Item = Persons.Item(key)
    		
    End Function
    
    Public Sub PrintHystory()
    'Печать всех элементов в порядке, обратном их добавлению в список.
    'Печатаются все элементы, независимо от того, были ли они удалены.
    	Dim P As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			Q.PrintPerson
    			Set P = P.Друг
    		Wend
    End Sub

    Приведем комментарии к этому тексту:

  • Прежде всего, следует обратить внимание на то, что объект Persons класса Collection встроен, как свойство, в создаваемый класс. Этот объект инициализируется конструктором по умолчанию, - обработчиком события Initialize в тот момент, когда создается сам объект класса КоллекцияЛичностей. Благодаря этому встроенному объекту, становятся доступны все свойства и методы стандартного класса Collection.
  • Наиболее интересна реализация метода AddPerson. В этом методе вначале вызывается закрытый метод AddFirst, создающий копию добавляемого элемента и помещающий эту копию в линейный список, представляющий внутреннюю память коллекции. Следующим шагом является вызов метода Add встроенным объектом Persons. Этому предшествует разбор случаев вызова. Дело в том, что метод AddPerson, также как и его прародитель метод Add имеет возможные параметры, которые могут быть заданы или опущены в момент вызова. Так что приходится разбираться, какие из параметров были заданы при вызове.
  • Функция Item и метод Remove достаточно просты в реализации, они вызывают соответствующие методы встроенного объекта.
  • Свойство Count, доступное для чтения, соответствует значению этого свойства внутреннего объекта Persons.
  • В интерфейс класса добавлены два метода печати элементов списка. Один из них распечатывает текущее состояние коллекции, состав элементов которой может динамически изменяться. Другой метод распечатывает историю создания коллекции. Список, представляющий внутреннюю память коллекции, сохраняет эту историю
  • Приведем процедуру для работы с этой коллекцией:

    Public Sub Великие()
    		'Создание и работа с коллекцией личностей
    		Dim Личности As New КоллекцияЛичностей
    		Dim ЭтоЛичность As Личность
    'Работа с коллекцией, как со списком
    		Dim Адам As New Личность
    		Адам.InitPerson "Адам", "Первый Человек", #1/1/100#
    		Личности.AddPerson Адам, "Первый"
    		Dim Ной As New Личность
    		Ной.InitPerson "Ной", "Праведник", #1/1/100#
    		Личности.AddPerson Item:=Ной, after:=1
    	
    'Работа с коллекцией, как с динамическим массивом
    		Dim Шекспир As New Личность
    		Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564#
    		Личности.AddPerson Item:=Шекспир, after:=2
    		Dim Гомер As New Личность
    		Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100#
    		Личности.AddPerson Item:=Гомер, before:=3
    		
    'Работа с коллекцией, как со словарем
    		Dim Булгаков As New Личность
    		Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891#
    		Личности.AddPerson Item:=Булгаков, key:="Мастер"
    		Dim Пушкин As New Личность
    		Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799#
    		Личности.AddPerson Item:=Пушкин, key:="Гений"
    	
    'Печать всего списка
    		Личности.PrintList
    		Debug.Print Личности.Количество
     'Удаление элементов
    		Личности.Remove "Первый"
    		Личности.Remove 2
    'Печать после удаления
    		Личности.PrintList
    		Debug.Print Личности.Количество
    'Доступ к отдельным элементам по ключу
    		Set ЭтоЛичность = Личности.Item("Гений")
    		ЭтоЛичность.PrintPerson
    		Set ЭтоЛичность = Личности.Item(2)
    		ЭтоЛичность.PrintPerson
    End Sub

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

    Адам                Первый Человек            родился          01.01.100 
    Ной                 Праведник                 родился          01.01.100 
    Гомер               Великий Слепой            родился          01.01.100 
    Вильям              Шекспир                   родился          23.04.1564 
    Михаил              Булгаков                  родился          23.01.1891 
    Александр           Пушкин                    родился          06.06.1799 
     6 
    Ной                 Праведник                 родился          01.01.100 
    Вильям              Шекспир                   родился          23.04.1564 
    Михаил              Булгаков                  родился          23.01.1891 
    Александр           Пушкин                    родился          06.06.1799 
     4 
    Александр           Пушкин                    родился          06.06.1799 
    Вильям              Шекспир                   родился          23.04.1564

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

    Public Sub ВводКоллекции()
    	'Создание коллекции в диалоге с пользователем
    	Dim Личности As New КоллекцияЛичностей
    	Dim ЭтоЛичность As New Личность
    	Dim Имя As String, Фамилия As String, Дата As Date
    	If MsgBox("Начнем создавать список личностей?", vbYesNo) = vbYes Then
    		Do
    			ЭтоЛичность.ВашеИмя = InputBox("Введите имя личности")
    			ЭтоЛичность.ВашаФамилия = InputBox("Введите Фамилию")
    			ЭтоЛичность.ВашаДатаРождения = InputBox("Введите дату рождения ")
    			Личности.AddPerson ЭтоЛичность
    		Loop Until MsgBox("Продолжим создавать список личностей?", vbYesNo) = vbNo
    	Личности.PrintList
    	End If
    End Sub

    На этом мы и поставим точку в этой лекции.

    Страницы:

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

    Семейство классов и реализация интерфейсов

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

    Наследование

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

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

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

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

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

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

    Вот пример семейства классов, связанных отношением наследования:

    (рис 5.1) Дерево одиночного наследования

    Множественное наследование описывается ациклическим графом:

    (рис 5.2) Ациклический граф множественного наследования.

    Мы уже говорили, что производный класс наследует состояние и поведение базового класса. Это значит, что все открытые ( Public ) элементы базового класса доступны в производном классе. Но главное, конечно, в том, что потомок может пойти дальше своего родителя. Производный класс расширяет свойства и поведение базового класса. В производном классе можно:

  • объявить новые переменные
  • объявить новые методы
  • переопределить (перегрузить, перекрыть) методы базового класса.
  • Переопределение метод а класса A в производном классе В, называемое также перегрузкой (overloading) или перекрытием (overriding), - это определение в классе B метода с именем, которое уже является именем некоторого метода класса А.

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

    Виртуальные методы и полиморфизм

    Как взаимодействуют между собой объекты разных классов, связанных отношением наследования? Возможны ли взаимные присваивания между объектами родительских классов и их потомками? Ответ на второй вопрос положителен лишь "наполовину". Совместимость по присваиванию обеспечивается лишь в одну сторону - от родителей к потомкам: объекту родительского класса может быть присвоено значение объекта-потомка - обратное недопустимо. Это и понятно: ведь потомок, сохраняя все свойства родителя, может иметь и другие свойства.

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

  • совместимость между формальным и фактическим параметром процедуры или функции. Формальный параметр может иметь тип базового класса, а фактический параметр - аргумент - при вызове функции может быть объектом любого производного класса.
  • совместимость указателей. Указатель может ссылаться на базовый класс, но при конструировании объекта в операторе " new " может быть вызван конструктор производного класса, что позволяет создать объект соответствующего класса. В более общей ситуации указателю базового класса может быть присвоен указатель производного класса.
  • Ниже мы покажем, что свойство совместимости по присваиванию в сочетании с возможностью определения виртуальных методов приводит к весьма полезным следствиям.

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

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

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

    Принципиально виртуальный метод от не виртуального отличается механизмом позднего связывания , применяемым для организации вызова виртуального метода. Во многих языках программирования методы могут быть как виртуальными, так и, в большинстве случаев, не виртуальными. В языке VBA это не так, здесь практически все методы виртуальные и для них применяется механизм позднего связывания. Это и понятно, поскольку VBA по существу интерпретируемый язык, а интерпретация подразумевает позднее связывание. Поэтому, чтобы Вы почувствовали разницу между виртуальными и не виртуальными методами, позвольте привести один пример на языке Visual C++. Рассмотрим следующий пример. Пусть имеются базовый класс Base и производный от него класс Derived, в которых определены виртуальный метод VirtMethod и невиртуальный метод NonVirtMethod. В базовом классе определена также полиморфная функция Test:

    class Base
    {
    public:
    virtual void VirtMethod ();	// печатает слово" Отец"
    void NonVirtMethod ();	// печатает слово" Мать"
    
    void Test ()
    {
    VirtMethod ();	// вызов виртуального метода
    NonVirtMethod ();	// вызов не виртуального метода
    }
    }
    
    class Derived: public Base
    {
    public:
    virtual void VirtMethod ();	// печатает слово" Сын"
    
    void NonVirtMethod ();	// печатает слово" Дочь"
    }

    Когда компилятор в базовом классе Base создает код для функции Test, то, анализируя вызов не виртуального метода NonVirtMethod(), он уже на этапе компиляции создаст обращение к методу NonVirtMethod класса Base.

    Совсем иначе он поступит, анализируя вызов виртуального метода VirtMethod. Для виртуального метода на этапе компиляции нельзя определить, метод какого класса следует вызывать. Это может быть метод класса Base или класса Derived. Возможно даже, что уже после того, как функция Test будет откомпилирована, программист определит новый класс - потомок класса Derived со своим виртуальным методом VirtMethod.

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

    // При создании объекта BaseItem вызывается конструктор класса Base.
    Base BaseItem;
    // При создании объекта DerivedItem вызывается конструктор класса Derived
    Derived DerivedItem;
    // Вызов функции Test
    BaseItem.Test();	// Будет напечатано "Отец" и "Мать"
    DerivedItem.Test();	// Будет напечатано "Сын" и "Мать".

    Функция Test является примером полиморфной функции. Заметьте: она имеет по умолчанию первым параметром указатель на объект базового класса ( this ). Продолжим рассмотрение нашего примера и покажем, как указатели обеспечивают полиморфизм:

    // Полиморфные указатели pItem1 и pItem2 ссылаются на базовый класс Base.
    // Вызывается конструктор и создается динамический объект класса Base.
    Base* pItem1 = new Base();
    // Вызывается конструктор и создается динамический объект класса Derived
    Base* pItem2 = new Derived();
    
    // Будет вызван метод класса Base и напечатано слово "Отец".
    pItem1 ->VirtMethod();
    // Будет вызван метод класса Derived и напечатано слово "Сын".
    pItem2 -> VirtMethod();

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

    Абстрактные классы

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

    Наследование и полиморфизм в Office 2000

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

    Наследование интерфейсов

    В Office 2000 на классах введено отношение "наследование интерфейса" . Здесь под интерфейсом понимается совокупность всех открытых ( Public ) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от "классического" наследования не транзитивно, - потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, - наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:

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

    Implements имя_родительского_класса

    При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все открытые методы, а для каждого открытого свойства, будет наследоваться пара процедур - свойств Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка " Implements ", в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.

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

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

  • Использовать обычную схему Copy - Paste, копируя реализацию родителя.
  • Реализовать наследование типичным для Office 2000 способом путем встраивания родительского объекта в класс - потомок. Тогда можно вызывать в нужном месте нужный метод родителя. Чуть позже мы рассмотрим проект " Люди и машины ", где используем этот прием.
  • Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения "наследование интерфейсов". Синтаксически, это определяется двумя обстоятельствами:

  • Заготовки методов, строящиеся автоматически, используют спецификатор Private. Это означает, что наследуемые методы не являются открытыми методами потомка и, следовательно, не входят в его интерфейс и не передаются далее внукам родителя.
  • Еще одна особенность состоит в том, что имена наследуемых методов содержат имя родителя. Это также означает, что наследуемый метод не становится собственным методом, который можно передать далее своим потомкам
  • Возникает естественный вопрос, если наследуемые методы не входят в интерфейс класса, а следовательно не могут быть вызваны объектами данного класса, то какой в них толк? Толк есть, поскольку эти методы могут быть все-таки вызваны объектами, правда, принадлежащими родительскому классу. Объект родительского класса, ссылающийся на своего потомка, может вызывать методы родителя, наследуемые потомком. Вот небольшой пример, где действую объекты трех классов Father, Son и GrandSon, связанные отношением наследования интерфейсов. Пусть определен абстрактный класс Father, интерфейс которого состоит из одного свойства, одного чистого метода и метода с заданной реализацией:

    'Class Father
    'Свойства класса
    Public MyProperty As String
    
    'Методы класса
    
    Public Sub MyPureMethod()
    'Чистый метод
    End Sub
    
    Public Sub MyRealMethod()
    		MsgBox ("It's the Father")
    End Sub

    Класс Son, наследующий интерфейс класса Father, по контракту должен реализовать две процедуры - свойства и два его метода. Мы дали только формальную реализацию, - в созданные автоматически заготовки добавили комментарии. Лишь в реализацию метода Father_MyPureMethod класса Son вставлена строка текста. Кроме того, в класс добавлен новый метод, определяющий собственный интерфейс этого класса. Вот описание этого класса:

    'Класс Son - Наследник класса Father
    Implements Father
    
    Private Property Let Father_MyProperty(ByVal RHS As String)
    'Реализация отложена
    
    End Property
    
    Private Property Get Father_MyProperty() As String
    'Реализация отложена
    
    End Property
    
    Private Sub Father_MyPureMethod()
    		MsgBox ("It's the Son")
    
    End Sub
    
    Private Sub Father_MyRealMethod()
    'Реализация отложена
    
    End Sub	
    
    Public Sub SonNewMethod()
    		MsgBox ("It's the Son of his Father")
    End Sub

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

    Приведем теперь описание класса GrandSon - потомка классов Son и Father. Заметьте, класс явно определяет обоих своих родителей. Опять-таки мы ограничились формальной реализацией, определив лишь реализацию метода Father_MyRealMethod:

    Option Explicit
    
    Implements Father
    Implements Son
    
    Private Property Let Father_MyProperty(ByVal RHS As String)
    'Реализация отложена
    
    End Property
    
    Private Property Get Father_MyProperty() As String
    'Реализация отложена
    
    End Property
    
    Private Sub Father_MyPureMethod()
    'Реализация отложена
    
    End Sub
    
    Private Sub Father_MyRealMethod()
    'Реализация отложена
    MsgBox ("It's the GrandSon")
    End Sub
    
    Private Sub Son_SonNewMethod()
    'Реализация отложена
    
    End Sub

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

    Public Sub Family()
    
    Dim F As New Father, S As New Son, GS As New GrandSon
    Dim Grand As Father, GrandS As Son
    
    Set Grand = F
    Grand.MyProperty = "Flat"
    Grand.MyRealMethod
    Grand.MyPureMethod
    Debug.Print Grand.MyProperty
    Set Grand = S
    Grand.MyProperty = "Flat"
    Grand.MyRealMethod
    Grand.MyPureMethod
    If TypeOf Grand Is Son Then
    		Set GrandS = Grand: GrandS.SonNewMethod
    End If
    Debug.Print Grand.MyProperty
    
    Set Grand = GS
    If TypeOf Grand Is GrandSon Then
    		Set GrandS = Grand: GrandS.SonNewMethod
    End If
    		Grand.MyProperty = "Flat"
    		Grand.MyRealMethod
    		Grand.MyPureMethod
    Debug.Print Grand.MyProperty
    
    End Sub

    В этом примере объект Grand класса Father связывается поочередно с объектами класса Father, Son, GrandSon и всякий раз вызываются методы родительского класса, унаследованные потомками. Обратите внимание на конструкцию TypeOf - Is, позволяющую определить текущий тип объекта. В процессе работы этой процедуры будут открываться диалоговые окна, уведомляющие, что мы встретились с объектами Father, Son, Son of his Father, GrandSon. При печати свойств лишь однажды будет напечатано значение Flat.

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

    Полиморфизм семейства классов

    Два фактора обеспечивают полиморфизм:

  • Присваивание снизу - вверх.
  • Механизм позднего связывания.
  • Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов X.VirtMethod, то по этому тексту ничего нельзя сказать о результате этого вызова, поскольку по первому свойству полиморфизма с X может быть связан объект любого из классов семейства, а благодаря второму свойству будет найден виртуальный метод именно этого класса.

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

    Наследование интерфейсов позволяет организовать присваивание снизу - вверх и, как следствие, позволяет работать с группой элементов, каждый из которых может принадлежать различным классам семейства. Поскольку позднее связывание и ранее было характерно для VBA, то полиморфизм в Office 2000 реализован в полном объеме.

    Итак, подводя некоторый итог, еще раз отметим, что полиморфизм обеспечивает два важных достоинства при работе с семейством классов:

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

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

    Давайте перейдем от слов к делу и посмотрим, как это все работает на более или менее содержательном примере.

    Проект "Люди и Машины"

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

    (рис 5.3) Схема семейства классов "Люди и Машины"

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

    Option Explicit
    'Класс Машина
    'Свойства класса
    Private Марка As String
    Private ДатаВыпуска As Date
    Private Цвет As String
    
    'Конструкторы класса
    
    Private Sub Class_Initialize()
    		Марка = "Форд"
    		ДатаВыпуска = "20.07.1925"
    		Цвет = "Вишневый"
    End Sub
    
    Public Sub НоваяМашина(M As String, D As Date, C As String)
    		Марка = M
    		ДатаВыпуска = D
    		Цвет = C
    End Sub
    
    'Методы класса
    Public Sub PrintDataCar()
    		Debug.Print "Марка = ", Марка
    		Debug.Print "ДатаВыпуска = ", ДатаВыпуска
    		Debug.Print "Цвет = ", Цвет
    		
    End Sub
    
    Public Property Get МаркаМашины() As String
    		МаркаМашины = Марка
    End Property
    
    Public Property Get ЦветМашины() As String
    		ЦветМашины = Цвет
    End Property
    
    Public Property Get ДатаВыпускаМашины() As Date
    		ДатаВыпускаМашины = ДатаВыпуска
    End Property

    У класса Машина имеется:

  • три закрытых свойства, обладающие статусом Read only,
  • два конструктора, - закрытый конструктор по умолчанию и открытый конструктор НоваяМашина,
  • один типичный для классов открытый метод, задающий печать свойств - PrintDataCar
  • Интерфейс класса составляют два открытых метода, они и будут наследоваться классом - потомком. Класс - потомок ВладелецМашины значительно больше унаследует от своего другого родителя - класса Личность. Вот определение класса ВладелецМашины:

    Option Explicit
    'Класс ВладелецМашины
    'Наследует интерфейсы классов Личность и Машина
    Implements Машина
    Implements Личность
    
    'Свойства класса
    Private Сам As Личность
    Private ЕгоМашина As Машина
    
    Private Sub Class_Initialize()
     Set Сам = New Личность
     Set ЕгоМашина = New Машина
    End Sub
    
    'Реализация интерфейсов класса Личность
    Private Sub Личность_CopyPerson(You As Личность)
    		Сам.CopyPerson (You)
    End Sub
    
    Private Sub Личность_InitPerson(ByVal FN As String, ByVal LN As String, ByVal DoB As Date)
     'Инициализация личности
    		Сам.InitPerson FN, LN, DoB
    End Sub
    
    Private Sub Личность_PrintPerson()
     'Печать в отладочном окне Immediate
    		Сам.PrintPerson
    End Sub
    
    Private Sub Личность_SayWhoIs()
    '	Вывод сообщения о поле и возрасте владельца машины
    		Dim StrMsg As String
    		StrMsg = "Думаю, Владелец машины марки: "  _
    		ЕгоМашина.МаркаМашины  " это - "
    		If Сам.WhoIs Then
    			If Year(Сам.ВашаДатаРождения) > 1967 Then
    				StrMsg = StrMsg  "молодая девушка!"
    			Else: StrMsg = StrMsg  "женщина!"
    			End If
    		Else
    			If Year(Сам.ВашаДатаРождения) > 1967 Then
    				StrMsg = StrMsg  "молодой человек!"
    			Else: StrMsg = StrMsg  "мужчина!"
    			End If
    		End If
    		MsgBox (StrMsg)
    End Sub
    
    Private Function Личность_WhoIs() As Boolean
    		Сам.WhoIs
    End Function
    
    Private Property Let Личность_ВашаДатаРождения(ByVal NewValue As Date)
    		Сам.ВашаДатаРождения = NewValue
    End Property
    
    Private Property Get Личность_ВашаДатаРождения() As Date
    		'Зажигает событие ДеньРождения
    		'в зависимости от значения текущей даты
    		Личность_ВашаДатаРождения = Сам.ВашаДатаРождения
    End Property
    
    Private Property Let Личность_ВашаФамилия(ByVal NewValue As String)
    	'Зажигает событие ИзменениеФамилии
    		Сам.ВашаФамилия = NewValue
    End Property
    
    Private Property Get Личность_ВашаФамилия() As String
    		Личность_ВашаФамилия = Сам.ВашаФамилия
    End Property
    
    Private Property Let Личность_ВашеИмя(ByVal NewValue As String)
    		Сам.ВашеИмя = NewValue
    End Property
    
    Private Property Get Личность_ВашеИмя() As String
    		Личность_ВашеИмя = Сам.ВашеИмя
    End Property
    
    Private Property Let Личность_ВашеОтчество(ByVal NewValue As String)
    	Сам.ВашеОтчество = NewValue
    End Property
    
    Private Property Get Личность_ВашеОтчество() As String
    		Личность_ВашеОтчество = Сам.ВашеОтчество
    End Property
    
    'Реализация интерфейсов класса Машина
    Private Property Get Машина_ДатаВыпускаМашины() As Date
    	Машина_ДатаВыпускаМашины = ЕгоМашина.ДатаВыпускаМашины
    End Property
    
    Private Property Get Машина_МаркаМашины() As String
    		Машина_МаркаМашины = ЕгоМашина.МаркаМашины
    End Property
    
    Private Property Get Машина_ЦветМашины() As String
    		Машина_ЦветМашины = ЕгоМашина.ЦветМашины
    End Property
    
    Private Sub Машина_PrintDataCar()
    		ЕгоМашина.PrintDataCar
     End Sub
    
    Private Sub Машина_НоваяМашина(M As String, D As Date, C As String)
     ЕгоМашина.НоваяМашина M, D, C
    End Sub
    
    'Собственный интерфейс класса ВладелецМашины
    'Public методы - интерфейс Владельца машины
    Public Sub InitCarOwner(FN As String, LN As String, DoB As Date, _
    		Marka As String, DB As Date, Color As String)
    		'Инициализация данных о хозяине и его машине
    		Личность_InitPerson FN, LN, DoB
    		Машина_НоваяМашина Marka, DB, Color
    End Sub
    
     Public Sub ConnectOwnerAndCar(pers As Личность, car As Машина)
    		'соединяет данные о хозяине и его новой машине
    		Сам.CopyPerson pers
    		Машина_НоваяМашина car.МаркаМашины, car.ДатаВыпускаМашины, _
    			car.ЦветМашины
    End Sub
    
    Public Sub PrintOwnerData()
    		Личность_PrintPerson
    		Debug.Print " владеет машиной: "
    		Машина_PrintDataCar
    End Sub

    Прокомментируем этот довольно длинный текст. Вот на какие моменты следует обратить внимание:

  • Для того, чтобы реализовать полноценное наследование свойств и поведения родителей, а не только наследование интерфейсов, используется встраивание, - в классе определены свойства Сам класса Личность и свойство ЕгоМашина класса Машина.
  • В конструкторе по умолчанию Class_Initialize, вызываемом при создании объекта класса ВладелецМашины, объекты Сам и ЕгоМашина инициализируются, - будут вызваны их конструкторы по умолчанию класса Личность и класса Машина.
  • При наследовании интерфейсов класса Личность почти для всех методов наследовалось поведение родительского класса. Реализуется это достаточно просто - вызывается соответствующий метод объекта Сам.
  • Для метода SayWhoIs поведение переопределено. В данном случае при вызове метод учитывает специфику класса и сообщает некоторые данные, как о владельце, так и о его машине. Так что при вызове этого метода у просто личностей и у личностей, являющихся владельцами машин, результат будет различным.
  • Поскольку родительский класс допускает события, то и у потомка при вызове методов зажигаются соответствующие события.
  • Интерфейс класса Машина реализуется подобным образом. Полностью наследуется поведение родительского класса.
  • Собственный интерфейс класса довольно прост. У объектов этого класса нет новых дополнительных свойств, - все спрятано в свойствах встроенных объектов Сам и ЕгоМашина. Интерфейс составляют два конструктора и метод, осуществляющий печать данных. Обратите внимание, один конструктор InitCarOwner позволяет сконструировать новый объект класса по терминальным данным, характеризующим личность и его машину. Второй конструктор ConnectOwnerAndCar в качестве параметров использует ранее созданные объекты классов Личность и Машина, соединяя эти два объекта в один объект класса ВладелецМашины.
  • На рисунке 5.4 отражен один из моментов проектирования класса. Можно видеть, что в окне кода список объектов содержит объекты Class, Личность и Машина. При выборе объекта Class правый раскрывающийся список покажет список стандартных событий класса, а при выборе объектов Личность и Машина будет раскрываться список наследуемых методов.

    (рис 5.4) Проектирование класса ВладелецМашины

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

    Option Explicit
    'Модуль Примеры
    Public FriendOne As New Личность
    Public FriendTwo As New Личность
    Public FriendThree As New Личность
    Public carOne As New Машина
    Public carTwo As New Машина
    Public carThree As New Машина
    Public OwnerOne As New ВладелецМашины
    Public OwnerTwo As New ВладелецМашины
    Public OwnerThree As New ВладелецМашины
    Public FOne As New Личности
    
    Public Sub Люди()
    'Вызывается конструктор с параметрами
    'и происходит знакомство с объектами
    FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _
    		DoB:="21.05.39"
    FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _
    		DoB:="22.03.79"
    FriendThree.InitPerson FN:="Остап", LN:="Бендер", DoB:="23.07.1910"
    FriendOne.PrintPerson
    FriendTwo.PrintPerson
    FriendOne.SayWhoIs
    FriendTwo.SayWhoIs
    'Связывание с двойниками.
    'Теперь объекты могут реагировать на события!
    FOne.Connect
    End Sub
    
    Public Sub Cars()
    'Вызывается конструктор с параметрами
    carOne.НоваяМашина "Антилопа", "12.12.12", "Неопределенный"
    carTwo.НоваяМашина "Москвич", "12.11.98", "Морская волна"
    carThree.НоваяМашина "Jeep", "23.05.97", "Orange"
    End Sub
    
    Public Sub CarOwners()
    		OwnerOne.ConnectOwnerAndCar FriendOne, carTwo
    		OwnerTwo.ConnectOwnerAndCar FriendThree, carOne
    		OwnerThree.InitCarOwner FN:="Юрий", LN:="Вегера", _
    		DoB:="21.08.34", Marka:="Газ69", DB:="20.01.76", Color:="Зеленый"
    		OwnerOne.PrintOwnerData
    		OwnerTwo.PrintOwnerData
    		OwnerThree.PrintOwnerData
    End Sub
    Public Sub CallEvents()
    		Dim DoB As Date
    		'Вызов методов приведет к возникновению событий!
    		'При замене фамилии возникнет событие ИзменениеФамилии
    		'Заметьте, не всегда фамилия будет изменена!
    		FriendOne.ВашаФамилия = "Фидотов"
    		FriendTwo.ВашаФамилия = "Волконская"
    		'При попытке узнать дату рождения
    		'может быть вызван обработчик события ДеньРождения.
    		DoB = FriendOne.ВашаДатаРождения
    		Debug.Print DoB
    		DoB = FriendTwo.ВашаДатаРождения
    		Debug.Print DoB
    		FriendOne.PrintPerson
    		FriendTwo.PrintPerson
    		
    	'События не наследуются
    		Set FriendOne = OwnerTwo
    		'Нельзя связать теперь объект FriendOne с двойником
    		'FOne.Connect
    		FriendOne.ВашаФамилия = "Воробьянинов"
    		FriendOne.PrintPerson
    
    End Sub
    Public Sub Группа()
    		Const SizeGroup = 6
    		Const SizeGarage = 6
    		Dim i As Byte
    		Dim Group(1 To SizeGroup) As Личность
    		Dim Гараж(1 To SizeGarage) As Машина
    		
    		Set Group(1) = FriendOne
    		Set Group(2) = FriendTwo
    		Set Group(3) = FriendThree
    		Set Group(4) = OwnerOne
    		Set Group(5) = OwnerTwo
    		Set Group(6) = OwnerThree
    		For i = 1 To SizeGroup
    			Group(i).SayWhoIs
    		Next i
    
    		Set Гараж(1) = carOne
    		Set Гараж(2) = carTwo
    		Set Гараж(3) = carThree
    		Set Гараж(4) = OwnerOne
    		Set Гараж(5) = OwnerTwo
    		Set Гараж(6) = OwnerThree
    		For i = 1 To SizeGarage
    			Гараж(i).PrintDataCar
    		Next i
    End Sub
    
    Public Sub ЛюдиИМашины()
    		Люди
    		Cars
    		CarOwners
    		Группа
    		PolyMorf FriendTwo
    		PolyMorf OwnerTwo
    End Sub
    
    Public Sub PolyMorf(One As Личность)
    		One.SayWhoIs
    End Sub

    Дадим теперь комментарии к этому тексту:

  • В модуле Примеры вначале объявляются по три объекта классов Личность, Машина и ВладелецМашины. В методах Люди, Cars, CarOwners происходит инициализация этих объектов. Некоторые из этих личностей уже встречались в предыдущих примерах, но появился Остап Бендер и его знаменитая машина.
  • Обратите внимание на создание объектов Owner класса ВладелецМашины, они создаются разными конструкторами.
  • Основную работу выполняет метод Группа. Здесь создается массив элементов Group из трех объектов Friend и трех объектов Owner, принадлежащих разным классам. Тем не менее все эти объекты обрабатываются в едином цикле и для них вызывается метод SayWhoIs, который, как мы говорили ранее по-разному работает для объектов класса Личность и класса ВладелецМашины. Так что простой цикл по элементам Group демонстрирует реализацию полиморфизма.
  • В методе Группа создана и продемонстрирована работа с еще одной группой элементов, - массивом Garage. Эту группу составляют объекты класса Машина и объекты класса ВладелецМашины. Обратите внимание, одни и те же объекты Owner входят в обе группы элементов и могут быть обработаны нужным образом.
  • В процессе обработки массива Garage вызывается метод PrintDataCar, который унаследован объектами класса ВладелецМашины.
  • В модуле Примеры приведена полиморфная функция PolyMorf(One As Личность ), с формальным параметром родительского класса Личность. Она дважды вызывается в процедуре ЛюдиИМашины с фактическими параметрами разных классов.
  • Процедура ЛюдиИМашины является процедурой, организующей всю работу, она поочередно запускает все перечисленные ранее процедуры. Сама она вызывается в обработчике события Click командной кнопки, встроенной в наш тестовый документ.
  • В процессе выполнения этой процедуры откроется серия диалоговых окон, где вначале будет сказано, кто такой Федотов и Катя, будет спрошено отчество Вегеры Юрия, затем опять будет рассказано о Федотове, Кате и Бендере, после чего появится информация о владельцах машин разных марок. На последнем этапе опять будет рассказано о Кате и владельце машины "Антилопа". Нам осталось привести результаты отладочной печати, появляющиеся при работе этой процедуры:

    Станислав                 Федотов            родился         21.05.39 
    Катя                      Павлова            родилась        22.03.79 
    Станислав                 Федотов            родился         21.05.39 
     владеет машиной: 
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Остап                     Бендер             родился         23.07.1910 
     владеет машиной: 
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Юрий            Алексеевич      Вегера       родился         21.08.34 
     владеет машиной: 
    Марка =			Газ69
    ДатаВыпуска =					20.01.76 
    Цвет =			Зеленый
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Марка =			Jeep
    ДатаВыпуска =					23.05.97 
    Цвет =			Orange
    Марка =			Москвич
    ДатаВыпуска =					12.11.98 
    Цвет =			Морская волна
    Марка =			Антилопа
    ДатаВыпуска =					12.12.12 
    Цвет =			Неопределенный
    Марка =			Газ69
    ДатаВыпуска =					20.01.76 
    Цвет =			Зеленый

    Как мы уже отмечали, родительский класс Личность реагирует на события. Это не мешает наследовать его интерфейсы, но не означает, что потомки будут наследовать события своего родителя. Чтобы убедиться, что зажигание событий не мешает работе потомков и не влияет на их работу, мы включили процедуру CallEvents. В процессе ее работы возникают события у объектов родительского класса и они правильно обрабатываются. Но, заметьте, после того как объект FriendOne был связан с объектом - потомком FriendOwner, при смене фамилии соответствующее событие уже не возникало, так что ничто не помешало Остапу Бендеру сменить свою фамилию. По ходу работы этой процедуры в появляющихся диалоговых окнах будет сообщено об отказе смены фамилии Федотову и появится поздравление Кате Павловой в связи с замужеством. Приведем результаты отладочной печати при работе этой процедуры:

    21.05.39 
    22.03.79 
    Станислав          Федотов              родился            21.05.39
    Катя               Волконская           родилась           22.03.79
    Остап              Воробьянинов         родился            23.07.1910

    Этот пример демонстрирует основные возможности работы с семейством классов, появившиеся в связи с введением механизма наследования интерфейсов.

    Создание динамических структур данных

    Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки, деки и очереди, деревья бинарные и сбалансированные. В современных языках программирования некоторые из таких структур стали такой же частью языка, как и массивы. Реализуется такая динамическая структура чаще всего в виде класса. Прекрасным примером является реализация самой системы Office 2000. Как мы не раз говорили, Office 2000 можно рассматривать как семейство классов объектов. Так вот, почти для каждого класса объектов, существует и класс, задающий коллекцию этих объектов. Классы - коллекции составляют почти половину классов Office 2000. Все они устроены похожим образом и имеют некоторый стандартный набор методов, позволяющих удалить или добавить новый элемент в коллекцию, получить элемент из коллекции, зная его порядковый номер или ключ элемента. Некоторые коллекции имеют специфические свойства и поведение, обусловленное спецификой самих элементов, хранящихся в коллекции. В ряде случаев, коллекции могут допускать хранение элементов разных классов. Обилие коллекций в Office 2000 первоначально раздражает, поскольку хотелось бы иметь единый класс со свойствами и методами, не зависящими от природы элементов, составляющих коллекцию. Но со временем ко всему привыкаешь и начинаешь принимать как должное, что у каждого класса объектов своя коллекция. Благодаря подсказкам по ходу написания программы и легко доступной справочной системе, не требуется держать в памяти специфику каждой коллекции.

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

    Встроенный динамический класс Collection

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

    У класса Collection одно свойство - Count и 3 метода: Add, Item, Remove. Рассмотрим их подробнее:

  • Свойство Count возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод Add (item, key, before, after) добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции.
  • Метод Remove(key) удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
  • Обратите внимание на мощность методов класса. Они позволяют реализовать различные классические динамические структуры. Конечно же, совсем просто реализуется обычный односвязный список с возможностью добавления элементов в начало или конец списка. Но можно реализовать и значительно более мощную структуру, называемую словарем. Словарь представляет собой связанную совокупность пар элементов. Первый элемент пары называется ключом, второй - информационным полем. Особенностью словарей является то, что, зная ключ элемента, можно получить доступ к информационному полю. При этом, обратите внимание, в классе Collection на ключ не накладывается никаких ограничений, - это обычная строка. Немаловажно, что есть и альтернативный способ прямого доступа к элементам коллекции по индексу, когда в качестве ключа выступает порядковый номер элемента в коллекции. Таким образом, коллекция соединяет в себе достоинства списков и массивов.

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

    Sub TestOfCollection()
    'Так объявляются объекты (переменные) типа Collection
    	Dim MyCollection As New Collection
    'Объявление обычных локальных переменных
    	Dim i As Integer
    	Dim N As Long
    'Оператор With позволяет избежать многократного указания имени объекта
    With MyCollection
    N =.Count
    Debug.Print" Число элементов пустой коллекции =", N
    ' Добавление элементов в конец списка. 
    'Элементы имеют индексы, но не имеют ключа.
    	.Add (2)
    	.Add (4)
    	.Add (6)
    'Добавление нечетных элементов на свои места.
    'Заметьте, как указывается позиция
    'добавления c использованием параметров - before и after
    ' Добавляемые элементы имеют строковый тип и обладают ключом
    	.Add" один"," first", 1	' before (перед первым элементом)
    	.Add" три"," third",, 2	'after (после второго)
    	.Add" пять"," fifth",, 4
    N =.Count
    Debug.Print" Число элементов после 6-и вызовов метода Add", N
    Debug.Print" Элементы коллекции:" 
    ' Отладочная печать созданной коллекции из шести элементов.
    	For i = 1 To MyCollection.Count
    		Debug.Print MyCollection(i)
    	Next
    ' Удаление 4-го и 5-го элементов по заданному индексу и ключу.
    	.Remove 4
    	.Remove" fifth" 
    N =.Count
    Debug.Print" Число элементов после двух вызовов метода Remove=", N
    Debug.Print" Элементы коллекции:" 
    'И снова печать коллекции, в которой теперь четыре элемента.
    For i = 1 To MyCollection.Count
    Debug.Print MyCollection(i)
    Next
    End With
    End Sub

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

    Число элементов пустой коллекции = 0
    Число элементов после 6-и вызовов метода Add = 6
    Элементы коллекции:	один	2	три	4	пять	6
    Число элементов после двух вызовов метода Remove = 4
    Элементы коллекции:	один	2	три	6

    Подчеркнем еще раз основные свойства класса Collection:

  • Класс позволяет объединять в коллекцию элементы разных типов, хотя чаще применяются однотипные коллекции.
  • Класс объединяет в себе свойства линейного списка, динамического массива и структуры, называемой словарем , или отображением (map).
  • Это список, поскольку определена операция Add, позволяющая динамически добавлять элементы в конец списка.
  • Это динамический массив, поскольку все элементы индексированы и к ним возможен прямой доступ по индексу. С другой стороны, размер массива не фиксируется и динамически изменяется при добавлении и удалении элементов.
  • Это словарь, поскольку добавляемые элементы могут иметь ключ. Прямой доступ к элементам возможен как по индексу, так и по ключу. Под "прямым" здесь понимается доступ, основанный на применении функций расстановки (хэширования). Конкретный вид этих функций, определяющий эффективность поиска, - "секрет фирмы".
  • В классе определены методы Add, Item, Remove и свойство Count.
  • Элементами коллекции могут быть данные любых типов, встроенных в VBA. Не менее важно, что допускается строить коллекцию из объектов, классы которых определены программистом. В предыдущих примерах этой лекции мы использовали для построения групп массивы только по той причине, что еще не ввели понятие коллекции. Более разумно было бы использование коллекций при работе с личностями, владельцами машин и самими машинами. Построим пример, в котором действует коллекция личностей:

    Public Sub Collection()
    		'Создание и работа с коллекцией личностей
    		Dim Личности As New Collection
    'Работа с коллекцией, как со списком
    		Dim Адам As New Личность
    		Адам.InitPerson "Адам", "Первый Человек", #1/1/100#
    		Личности.Add Адам
    		Dim Ной As New Личность
    		Ной.InitPerson "Ной", "Праведник", #1/1/100#
    		Личности.Add Ной
    'Работа с коллекцией, как с динамическим массивом
    		Dim Шекспир As New Личность
    		Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564#
    		Личности.Add Item:=Шекспир, After:=2
    		Dim Гомер As New Личность
    		Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100#
    		Личности.Add Item:=Гомер, Before:=3
    		Личности(4).SayWhoIs
    'Работа с коллекцией, как со словарем
    		Dim Пушкин As New Личность
    		Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799#
    		Личности.Add Item:=Пушкин, Key:="Гений"
    		Dim Булгаков As New Личность
    		Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891#
    		Личности.Add Item:=Булгаков, Key:="Мастер"
    		Debug.Print Личности("Гений").ВашаФамилия, " - это Гений!"
    		Debug.Print Личности("Мастер").ВашаФамилия, " - это Мастер!"
    'Печать всего списка
    		Dim I As Byte
    		For I = 1 To Личности.Count
    			Личности(I).PrintPerson
    		Next I
    End Sub

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

    Пушкин                - это Гений!
    Булгаков              - это Мастер!
    Адам                    Первый Человек           родился           01.01.100 
    Ной                     Праведник                родился           01.01.100 
    Гомер                   Великий Слепой           родился           01.01.100 
    Вильям                  Шекспир                  родился           23.04.1564 
    Александр               Пушкин                   родился           06.06.1799 
    Михаил                  Булгаков                 родился           23.01.1891

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

    Создание собственных динамических классов

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

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

  • информационную часть элемента списка;
  • структуру элемента списка;
  • сам список, в том числе и операции, над ним определенные.
  • Для определенности рассмотрим список, хранящий информацию о "личностях". Тогда можно считать первую часть нашей задачи решенной,- класс Личность уже создан, и мы его просто используем. Теперь создадим класс с именем ЭлементСпискаЛичностей, задающий структуру элемента (запись). Этот класс прост, поскольку элементы линейного односвязного списка состоят из двух полей - информационного и поля указателя соседнего элемента. Никакие методы над этим классом обычно не определяются. Его полное описание:

    'Класс ЭлементСпискаЛичностей содержит только два поля
    Public Сам As Личность
    Public Друг As ЭлементСпискаЛичностей

    В определении класса ЭлементСпискаЛичностей свойство Друг является объектом того же класса. VBA допускает такую рекурсию в определении - без нее списковую структуру не организовать.

    Осталось определить класс СписокЛичностей, задающий сам список. Главное на этом этапе - спроектировать операции над списком. Мы ограничимся набором традиционных методов:

  • AddFirst (F As Личность) - добавляет элемент в начало списка;
  • AddLast (F As Личность) - добавляет элемент в конец списка;
  • Initialize - конструктор по умолчанию, инициализирует список;
  • PrintList() - печатает содержимое элементов списка;
  • ClearList() - очищает список.
  • Методы диктуют и состав данных (свойств) класса СписокЛичностей. Нужны минимум два указателя на начало и конец списка: First и Last. Эти свойства разумно закрыть от внешнего использования. Мы добавим к ним открытую переменную Count, следящую за числом элементов списка. Приведем теперь описание свойств и методов этого класса:

    Option Explicit
    'Определение класса СписокЛичностей
    'Свойства
    		Private First As ЭлементСпискаЛичностей
    		Private Last As ЭлементСпискаЛичностей
    		Public Count As Integer
    'Методы
    Private Sub Class_Initialize()
    		Set First = Nothing
    		Set Last = Nothing
    		Count = 0
    End Sub
    
    Public Sub AddFirst(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = First
    		If First Is Nothing Then
    			Set Last = Elem
    		End If
    		Set First = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub PrintList()
    		Dim P As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			Q.PrintPerson
    			Set P = P.Друг
    		Wend
    End Sub
    
    Public Sub AddLast(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = Nothing
    		If First Is Nothing Then
    			Set First = Elem
    		Else
    			Set Last.Друг = Elem
    		End If
    		Set Last = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub ClearList()
    	'Попытка освободить память не достигает успеха из-за отсутствия
    	'соответствующего оператора.
    		Dim P As ЭлементСпискаЛичностей, R As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			'Unload Q
    			Set R = P
    			Set P = P.Друг
    			'Unload R
    		Wend
    	'Обнуление указателей
    		Set First = Nothing
    		Set Last = Nothing
    		Count = 0
    End Sub

    Теперь несколько замечаний по поводу реализации методов:

  • При добавлении нового элемента в методах AddFirst и AddLast вначале создается новый пустой элемент списка; поскольку в объявлении элемента используется спецификатор New. Создается также новый элемент для информационного поля, куда копируется информация, переданная при вызове методов Add, - здесь пригодился метод CopyPerson класса Личность. Заметьте, что в отличие от класса Collection, создается копия элемента, а не используется ссылка. Поэтому состояние элементов списка не зависит от внешних изменений. Изменять содержимое элементов списка можно только с помощью методов списка. Хотя мы и не спроектировали такие методы, понятно, что это нетрудно сделать.
  • При печати списка последовательно проходятся все его элементы, для каждого из них вызывается метод PrintPerson, определенный в классе Личность.
  • Особо остановимся на методе ClearList, в котором можно установить нулевые значения указателей ( Nothing ), но нельзя освободить память, занятую элементами списка. Мы специально делаем такую попытку, не приводящую к успеху, чтобы заострить Ваше внимание на этом вопросе. В большинстве языков программирования, позволяющих создавать динамические структуры данных, всегда есть пара взаимосвязанных операций над памятью: "Выделить память" и "Освободить память" ( New-Delete или, например, Get-Free ). Операции эти выполняются динамически, и выделение и освобождение памяти производится по требованию программиста при выполнении программы. В VBA дело обстоит не так,- здесь есть New, но нет Delete. Связано это, конечно, с особенностями VBA, который, не будучи классическим языком компиляторного типа, представляет нечто среднее между компилятором и интерпретатором. Поэтому здесь освобождение памяти происходит в соответствии с обычными для переменных правилами, определяющими их время жизни. Так что в VBA программист не должен заботиться об освобождении памяти, занятой его динамическими структурами, - это делается автоматически без его участия. Так не проходит освобождение динамической памяти с помощью метода Unload, выполняющего операцию освобождения памяти, но над объектами другого рода.
  • Приведем теперь достаточно простой пример, позволяющий построить список в процессе диалога с пользователем и в конце распечатать данные, хранящиеся в построенном списке:

    Public Sub ВводСписка()
    	'Создание списка в диалоге с пользователем
    	Dim Личности As New СписокЛичностей
    	Dim ЭтоЛичность As New Личность
    	Dim Имя As String, Фамилия As String, Дата As Date
    	If MsgBox("Начнем создавать список личностей?", vbYesNo) = vbYes Then
    		Do
    			ЭтоЛичность.ВашеИмя = InputBox("Введите имя личности")
    			ЭтоЛичность.ВашаФамилия = InputBox("Введите Фамилию")
    			ЭтоЛичность.ВашаДатаРождения = InputBox("Введите дату рождения ")
    			Личности.AddLast ЭтоЛичность
    		Loop Until MsgBox("Продолжим создавать список личностей?", vbYesNo) = vbNo
    	Личности.PrintList
    	End If
    End Sub

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

    Обертывание коллекции VBA

    Обратите внимание, наш только что приведенный пример ВводСписка, иллюстрирующий часто возникающую на практике задачу, невозможно реализовать, используя встроенный класс Collection.Причина этого уже пояснялась, - коллекция хранит ссылки на созданные вне нее элементы, она не обладает собственной памятью. Поэтому пользователь сам должен заботиться о создании и хранении элементов. Конечно, хотелось бы поручить эту задачу самой коллекции, как это делается в созданном динамическом классе СписокЛичностей. С другой стороны этот класс имеет значительно менее мощные методы AddFirst и AddLast в сравнении с методом Add класса Collection. Класс СписокЛичностей не позволяет создавать словари, он значительно менее эффективен, чем класс Collection в тех случаях, когда требуется обеспечить прямой доступ к элементам списка. Возникает вопрос, нельзя ли соединить достоинства двух подходов? Оказывается, можно. Для этого применяется стандартная при работе с классами технология обертывания. Суть ее в том, что стандартный объект, например, элемент управления встраивается в собственный класс. Методы класса могут вызывать методы объекта, сохраняя тем самым мощь стандартного объекта. С другой стороны можно добавить новые методы или расширить функциональность существующих. Мы продемонстрируем эту технологию, показав как можно строить собственные коллекции для объектов любого класса. Конечно, нам не удастся построить универсальный класс, расширяющий возможности класса Collection, но нам удастся показать, что для каждого собственного класса элементов, можно построить коллекцию, обладающую всеми свойствами обычной коллекции, но, вместе с тем, хранящую элементы в самой коллекции.

    Не будем изобретать новых примеров и построим класс КоллекцияЛичностей. Этот класс будет обладать всеми стандартными свойствами и методами класса Collection, дополненными методами класса СписокЛичностей. Он будет иметь:

  • Свойство Count - возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод AddPerson (item, key, before, after) - добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции. Заметьте, несмотря на внешнее сходство метод AddPerson отличается от метода Add класса Collection. Во-первых, параметр Item будет иметь строго фиксированный тип Личность. Во-вторых, есть различия в реализации. Коллекция будет хранить не ссылку на элемент Item класса Личность, а копию этого элемента.
  • Метод Remove(key) - удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) - возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
  • Метод PrintList() - печатает элементы списка в его текущем состоянии.
  • Метод PrintHystory - печатает историю создания списка, - все его элементы, даже если они и были удалены впоследствии.
  • Наряду с открытыми свойствами и методами наш класс будет содержать и закрытые свойства и методы:

  • Свойство First - указатель на начало списка, хранящего элементы коллекции.
  • Событие Initialize - конструктор по умолчанию, инициализирует список.
  • Метод AddFirst - создает копии элементов и добавляет их в список, представляющий внутреннюю память коллекции.
  • Вот полное описание класса КоллекцияЛичностей, обертывающего класс Collection. Класс сохранил все мощные свойства стандартной коллекции и, к тому же, приобрел новые привлекательные свойства:

    Option Explicit
    
    'Определение класса КоллекцияЛичностей
    'Свойства
    		Private First As ЭлементСпискаЛичностей
    		Private Count As Long
    		'Обратите внимание: встраиваем коллекцию
    		Private Persons As Collection
    		
    'Методы
    Private Sub Class_Initialize()
    		Set First = Nothing
    		Count = 0
    		'при создании объекта класса КоллекцияЛичностей создается
    		'и внутренняя коллекция для хранения его элементов
    		Set Persons = New Collection
    End Sub
    
    Private Sub AddFirst(F As Личность)
    		Dim Elem As New ЭлементСпискаЛичностей
    		Dim Info As New Личность
    		'Создаем копию переменной F. В списке будем использовать копию, а не ссылку.
    		Info.CopyPerson F
    		Set Elem.Сам = Info
    		Set Elem.Друг = First
    		Set First = Elem
    		Count = Count + 1
    End Sub
    
    Public Sub PrintList()
    		'Печать текущего состояния списка
    		Dim i As Long
    		For i = 1 To Persons.Count
    			Persons(i).PrintPerson
    		Next i
    End Sub
    
    Public Sub AddPerson(Item As Личность, Optional key As String = "", _
    			Optional before As Long = 0, Optional after As Long = 0)
    		Dim P As Личность
    		'Вначале добавляем элемент в линейный список,
    		'используя внутренний метод AddFirst
    		AddFirst Item
    		
    		'Разбор случаев вызова
    		Set P = First.Сам
    		If key <> "" Then
    			Persons.Add P, key
    		ElseIf before <> 0 Then
    			Persons.Add P,, before
    		ElseIf after <> 0 Then
    			Persons.Add P,,, after
    		Else
    			Persons.Add P
    		End If
    		Count = Persons.Count
    End Sub
    Public Property Get Количество() As Long
    	Количество = Count
    End Property
    
    
    Public Sub Remove(key As Variant)
    		Persons.Remove key
    		Count = Persons.Count
    End Sub
    
    Public Function Item(key As Variant) As Личность
    		Set Item = Persons.Item(key)
    		
    End Function
    
    Public Sub PrintHystory()
    'Печать всех элементов в порядке, обратном их добавлению в список.
    'Печатаются все элементы, независимо от того, были ли они удалены.
    	Dim P As ЭлементСпискаЛичностей
    		Dim Q As Личность
    		Set P = First
    		While Not (P Is Nothing)
    			Set Q = P.Сам
    			Q.PrintPerson
    			Set P = P.Друг
    		Wend
    End Sub

    Приведем комментарии к этому тексту:

  • Прежде всего, следует обратить внимание на то, что объект Persons класса Collection встроен, как свойство, в создаваемый класс. Этот объект инициализируется конструктором по умолчанию, - обработчиком события Initialize в тот момент, когда создается сам объект класса КоллекцияЛичностей. Благодаря этому встроенному объекту, становятся доступны все свойства и методы стандартного класса Collection.
  • Наиболее интересна реализация метода AddPerson. В этом методе вначале вызывается закрытый метод AddFirst, создающий копию добавляемого элемента и помещающий эту копию в линейный список, представляющий внутреннюю память коллекции. Следующим шагом является вызов метода Add встроенным объектом Persons. Этому предшествует разбор случаев вызова. Дело в том, что метод AddPerson, также как и его прародитель метод Add имеет возможные параметры, которые могут быть заданы или опущены в момент вызова. Так что приходится разбираться, какие из параметров были заданы при вызове.
  • Функция Item и метод Remove достаточно просты в реализации, они вызывают соответствующие методы встроенного объекта.
  • Свойство Count, доступное для чтения, соответствует значению этого свойства внутреннего объекта Persons.
  • В интерфейс класса добавлены два метода печати элементов списка. Один из них распечатывает текущее состояние коллекции, состав элементов которой может динамически изменяться. Другой метод распечатывает историю создания коллекции. Список, представляющий внутреннюю память коллекции, сохраняет эту историю
  • Приведем процедуру для работы с этой коллекцией:

    Public Sub Великие()
    		'Создание и работа с коллекцией личностей
    		Dim Личности As New КоллекцияЛичностей
    		Dim ЭтоЛичность As Личность
    'Работа с коллекцией, как со списком
    		Dim Адам As New Личность
    		Адам.InitPerson "Адам", "Первый Человек", #1/1/100#
    		Личности.AddPerson Адам, "Первый"
    		Dim Ной As New Личность
    		Ной.InitPerson "Ной", "Праведник", #1/1/100#
    		Личности.AddPerson Item:=Ной, after:=1
    	
    'Работа с коллекцией, как с динамическим массивом
    		Dim Шекспир As New Личность
    		Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564#
    		Личности.AddPerson Item:=Шекспир, after:=2
    		Dim Гомер As New Личность
    		Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100#
    		Личности.AddPerson Item:=Гомер, before:=3
    		
    'Работа с коллекцией, как со словарем
    		Dim Булгаков As New Личность
    		Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891#
    		Личности.AddPerson Item:=Булгаков, key:="Мастер"
    		Dim Пушкин As New Личность
    		Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799#
    		Личности.AddPerson Item:=Пушкин, key:="Гений"
    	
    'Печать всего списка
    		Личности.PrintList
    		Debug.Print Личности.Количество
     'Удаление элементов
    		Личности.Remove "Первый"
    		Личности.Remove 2
    'Печать после удаления
    		Личности.PrintList
    		Debug.Print Личности.Количество
    'Доступ к отдельным элементам по ключу
    		Set ЭтоЛичность = Личности.Item("Гений")
    		ЭтоЛичность.PrintPerson
    		Set ЭтоЛичность = Личности.Item(2)
    		ЭтоЛичность.PrintPerson
    End Sub

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

    Адам                Первый Человек            родился          01.01.100 
    Ной                 Праведник                 родился          01.01.100 
    Гомер               Великий Слепой            родился          01.01.100 
    Вильям              Шекспир                   родился          23.04.1564 
    Михаил              Булгаков                  родился          23.01.1891 
    Александр           Пушкин                    родился          06.06.1799 
     6 
    Ной                 Праведник                 родился          01.01.100 
    Вильям              Шекспир                   родился          23.04.1564 
    Михаил              Булгаков                  родился          23.01.1891 
    Александр           Пушкин                    родился          06.06.1799 
     4 
    Александр           Пушкин                    родился          06.06.1799 
    Вильям              Шекспир                   родился          23.04.1564

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

    Public Sub ВводКоллекции()
    	'Создание коллекции в диалоге с пользователем
    	Dim Личности As New КоллекцияЛичностей
    	Dim ЭтоЛичность As New Личность
    	Dim Имя As String, Фамилия As String, Дата As Date
    	If MsgBox("Начнем создавать список личностей?", vbYesNo) = vbYes Then
    		Do
    			ЭтоЛичность.ВашеИмя = InputBox("Введите имя личности")
    			ЭтоЛичность.ВашаФамилия = InputBox("Введите Фамилию")
    			ЭтоЛичность.ВашаДатаРождения = InputBox("Введите дату рождения ")
    			Личности.AddPerson ЭтоЛичность
    		Loop Until MsgBox("Продолжим создавать список личностей?", vbYesNo) = vbNo
    	Личности.PrintList
    	End If
    End Sub

    На этом мы и поставим точку в этой лекции.

    Вернуться к учебному плану