Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: DocOne5, DocTwo5.
В предыдущей лекции мы уже говорили об одной из важных новинок Office 2000, направленной на превращение VBA в полноценный объектно-ориентированный язык. Возможность создания событий для классов, определенных программистом, существенно расширяет их возможности. Теперь такие классы обладают всеми тремя компонентами, присущими "настоящим" классам, - свойствами, методами и событиями, теперь и их объекты могут индивидуально реагировать на происходящие события. Сейчас мы расскажем еще об одном важном нововведении - возможности реализовать в одном классе интерфейс другого класса. Эта новинка направлена на создание полноценного семейства классов, связанного наследованием интерфейсов и реализующего полиморфизм методов семейства классов. Нужно сразу сказать, что сделано только пол шага в нужном направлении, - можно говорить о реализации полиморфизма и частичной реализации наследования в его классическом понимании. Можно ожидать развития этого направления в последующих версиях Office 2000. Но и то, что уже сделано, представляет несомненный интерес. Понимание этого материала требует от читателя знания основ ООП, и, в первую очередь, понимание наследования и полиморфизма. Минимально необходимые сведения мы сейчас приведем.
Объектно-ориентированное программирование дает возможность не только определить класс, задающий состояние и поведение объектов, но и организовать семейство классов с помощью важного отношения между классами, называемого отношением наследования.
Суть наследования в том, что новый класс в момент его создания можно объявить наследником одного или нескольких базовых классов. Пусть уже определен класс A, и при определении новый класс В объявлен наследником класса А. В этом случае класс В наследует все открытые для потомков свойства и поведение класса А, - наследует его интерфейс. Это означает, что в классе В автоматически определены переменные и методы класса А, задающие интерфейс.
Класс А называется базовым (родительским) по отношению к производному (порожденному) классу В. Очень часто, говоря о наследовании, используют терминологию " родительский класс " и " ". Ввиду транзитивности наследования (транзитивность означает, что из утверждений " А родитель В " и " В родитель С " следует " А родитель С ") каждый класс может иметь множество родителей (предков) и множество потомков, среди которых выделяют " непосредственных родителей " и " непосредственных потомков ". Потомок транзитивно наследует свойства и поведение всех своих предков.
В производном классе можно определить новые свойства и новое поведение, задав новые переменные и новые методы. Более того, в производном классе можно переопределить существующий метод базового класса.
Наследование называется множественным, если производный класс может быть наследником нескольких базовых классов, т.е. иметь более одного непосредственного родителя. При обычном (не множественном) наследовании каждый производный класс наследует свойства только одного базового класса, т.е. имеет одного непосредственного родителя.
Отношение наследования обычно представляют в виде графа, узлы которого соответствуют классам, и из узла A в узел B ведет дуга, если класс B является непосредственным наследником класса A. Этот граф изображает структуру совокупности классов с точки зрения наследования. Если наследование не множественное, то соответствующий граф является деревом. В корне дерева находится прародитель - класс, для которого все остальные классы являются его потомками-наследниками.
Вот пример семейства классов, связанных отношением наследования:
(рис 5.1) Дерево одиночного наследованияМножественное наследование описывается
(рис 5.2) Ациклический граф множественного наследования.Мы уже говорили, что производный класс наследует состояние и поведение базового класса. Это значит, что все открытые ( Public ) элементы базового класса доступны в производном классе. Но главное, конечно, в том, что потомок может пойти дальше своего родителя. Производный класс расширяет свойства и поведение базового класса. В производном классе можно:
Переопределение метод а класса A в производном классе В, называемое также перегрузкой (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 на классах введено отношение "наследование интерфейса" . Здесь под интерфейсом понимается совокупность всех открытых ( Public ) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от "классического" наследования не транзитивно, - потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, - наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:
Синтаксически, объявить о том, что класс наследует интерфейс другого класса, достаточно просто. Для этого достаточно в объявление класса поместить одну строчку:
Implements имя_родительского_класса
При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка " Implements ", в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.
Важно понимать, что наследование интерфейсов не означает наследование реализации методов. Это просто некоторый контракт между родителем и потомком. Потомок обязуется реализовать все
Если соответствующий метод родительского класса является чистым, то понятно, что потомок обязан дать собственную реализацию метода. Собственная реализация необходима и в том случае, если потомок хочет переопределить существующий метод родителя. Во многих случаях хотелось бы просто наследовать метод родителя, но, заметьте, автоматически это не делается. Здесь есть две возможности:
Copy - Paste, копируя реализацию родителя.Люди и машины ", где используем этот прием.Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения "наследование интерфейсов". Синтаксически, это определяется двумя обстоятельствами:
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.
Уже этот пример показывает, что наследование интерфейсов мало что дает с точки зрения наследования свойств и методов, поскольку ни свойства напрямую не наследуются, ни реализация не наследуется. Наследование интерфейсов представляет несомненный интерес и весьма полезно по другой причине, - оно позволяет реализовать в полной мере
Два фактора обеспечивают полиморфизм:
Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов 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
Этот пример демонстрирует основные возможности работы с семейством классов, появившиеся в связи с введением механизма наследования интерфейсов.
Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки,
Частью языка VBA является класс Collection. Он существенно облегчает работу с динамическими структурами данных во многих типичных ситуациях. Он позволяет хранить в коллекции данные разных типов и имеет хорошо продуманные свойства и методы. Не менее важно и то, что VBA позволяет создавать и собственные динамические структуры данных сколь угодно сложно организованные. К их рассмотрению мы сейчас и приступаем.
Коллекция в 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:
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 и собственного динамического класса.
Обратите внимание, наш только что приведенный пример ВводСписка, иллюстрирующий часто возникающую на практике задачу, невозможно реализовать, используя встроенный класс 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 в производном классе В, называемое также перегрузкой (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 на классах введено отношение "наследование интерфейса" . Здесь под интерфейсом понимается совокупность всех открытых ( Public ) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от "классического" наследования не транзитивно, - потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, - наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:
Синтаксически, объявить о том, что класс наследует интерфейс другого класса, достаточно просто. Для этого достаточно в объявление класса поместить одну строчку:
Implements имя_родительского_класса
При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка " Implements ", в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.
Важно понимать, что наследование интерфейсов не означает наследование реализации методов. Это просто некоторый контракт между родителем и потомком. Потомок обязуется реализовать все
Если соответствующий метод родительского класса является чистым, то понятно, что потомок обязан дать собственную реализацию метода. Собственная реализация необходима и в том случае, если потомок хочет переопределить существующий метод родителя. Во многих случаях хотелось бы просто наследовать метод родителя, но, заметьте, автоматически это не делается. Здесь есть две возможности:
Copy - Paste, копируя реализацию родителя.Люди и машины ", где используем этот прием.Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения "наследование интерфейсов". Синтаксически, это определяется двумя обстоятельствами:
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.
Уже этот пример показывает, что наследование интерфейсов мало что дает с точки зрения наследования свойств и методов, поскольку ни свойства напрямую не наследуются, ни реализация не наследуется. Наследование интерфейсов представляет несомненный интерес и весьма полезно по другой причине, - оно позволяет реализовать в полной мере
Два фактора обеспечивают полиморфизм:
Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов 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
Этот пример демонстрирует основные возможности работы с семейством классов, появившиеся в связи с введением механизма наследования интерфейсов.
Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки,
Частью языка VBA является класс Collection. Он существенно облегчает работу с динамическими структурами данных во многих типичных ситуациях. Он позволяет хранить в коллекции данные разных типов и имеет хорошо продуманные свойства и методы. Не менее важно и то, что VBA позволяет создавать и собственные динамические структуры данных сколь угодно сложно организованные. К их рассмотрению мы сейчас и приступаем.
Коллекция в 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:
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 и собственного динамического класса.
Обратите внимание, наш только что приведенный пример ВводСписка, иллюстрирующий часто возникающую на практике задачу, невозможно реализовать, используя встроенный класс 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
На этом мы и поставим точку в этой лекции.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.