Семейство классов и наследование интерфейсов. Динамические классы
Создание динамических структур данных
Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки, деки и очереди, деревья бинарные и сбалансированные. В современных языках программирования некоторые из таких структур стали такой же частью языка, как и массивы. Реализуется такая динамическая структура чаще всего в виде класса. Прекрасным примером является реализация самой системы Office 2000. Как мы не раз говорили, Office 2000 можно рассматривать как семейство классов объектов. Так вот, почти для каждого класса объектов, существует и класс, задающий коллекцию этих объектов. Классы - коллекции составляют почти половину классов Office 2000. Все они устроены похожим образом и имеют некоторый стандартный набор методов, позволяющих удалить или добавить новый элемент в коллекцию, получить элемент из коллекции, зная его порядковый номер или ключ элемента. Некоторые коллекции имеют специфические свойства и поведение, обусловленное спецификой самих элементов, хранящихся в коллекции. В ряде случаев, коллекции могут допускать хранение элементов разных классов. Обилие коллекций в Office 2000 первоначально раздражает, поскольку хотелось бы иметь единый класс со свойствами и методами, не зависящими от природы элементов, составляющих коллекцию. Но со временем ко всему привыкаешь и начинаешь принимать как должное, что у каждого класса объектов своя коллекция. Благодаря подсказкам по ходу написания программы и легко доступной справочной системе, не требуется держать в памяти специфику каждой коллекции.
Частью языка VBA является класс Collection. Он существенно облегчает работу с динамическими структурами данных во многих типичных ситуациях. Он позволяет хранить в коллекции данные разных типов и имеет хорошо продуманные свойства и методы. Не менее важно и то, что VBA позволяет создавать и собственные динамические структуры данных сколь угодно сложно организованные. К их рассмотрению мы сейчас и приступаем.
Встроенный динамический класс Collection
Коллекция в VBA - упорядоченная совокупность элементов, вообще говоря, разного типа. Этим коллекция отличается от массива, где объединяются только однотипные элементы. Все элементы индексированы, но некоторые могут иметь и ключ, связанный с элементом. Размер коллекции заранее не фиксируется и может динамически изменяться. Любой существующий элемент коллекции может быть удален, при этом не возникает "дыр" - элементы перенумеровываются, и непрерывность последовательности индексов не нарушается. Неприятным следствием этого факта, приводящего иногда к ошибке, является то, что индекс в отличие от ключа не является постоянной характеристикой элемента и может изменяться в процессе работы с коллекцией. Новый элемент может быть добавлен в произвольное место коллекции, - как перед некоторым элементом, так и после любого из элементов.
У класса Collection одно свойство - Count и 3 метода: Add, Item, Remove. Рассмотрим их подробнее:
- Свойство Count возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
- Метод Add (item, key, before, after) добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции.
- Метод Remove(key) удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
- Метод Item(key) возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
Обратите внимание на мощность методов класса. Они позволяют реализовать различные классические динамические структуры. Конечно же, совсем просто реализуется обычный односвязный список с возможностью добавления элементов в начало или конец списка. Но можно реализовать и значительно более мощную структуру, называемую словарем. Словарь представляет собой связанную совокупность пар элементов. Первый элемент пары называется ключом, второй - информационным полем. Особенностью словарей является то, что, зная ключ элемента, можно получить доступ к информационному полю. При этом, обратите внимание, в классе Collection на ключ не накладывается никаких ограничений, - это обычная строка. Немаловажно, что есть и альтернативный способ прямого доступа к элементам коллекции по индексу, когда в качестве ключа выступает порядковый номер элемента в коллекции. Таким образом, коллекция соединяет в себе достоинства списков и массивов.
Приведем теперь пример процедуры, подробнее демонстрирующий работу с коллекцией. Наша коллекция будет включать данные двух типов: целочисленные и строковые. Часть элементов будет иметь ключ, остальные - только индекс. Элементы будут добавляться в заданную позицию и удаляться. Отладочная печать позволит проследить за этим процессом:
Sub TestOfCollection() 'Так объявляются объекты (переменные) типа Collection Dim MyCollection As New Collection 'Объявление обычных локальных переменных Dim i As Integer Dim N As Long 'Оператор With позволяет избежать многократного указания имени объекта With MyCollection N =.Count Debug.Print" Число элементов пустой коллекции =", N ' Добавление элементов в конец списка. 'Элементы имеют индексы, но не имеют ключа. .Add (2) .Add (4) .Add (6) 'Добавление нечетных элементов на свои места. 'Заметьте, как указывается позиция 'добавления c использованием параметров - before и after ' Добавляемые элементы имеют строковый тип и обладают ключом .Add" один"," first", 1 ' before (перед первым элементом) .Add" три"," third",, 2 'after (после второго) .Add" пять"," fifth",, 4 N =.Count Debug.Print" Число элементов после 6-и вызовов метода Add", N Debug.Print" Элементы коллекции:" ' Отладочная печать созданной коллекции из шести элементов. For i = 1 To MyCollection.Count Debug.Print MyCollection(i) Next ' Удаление 4-го и 5-го элементов по заданному индексу и ключу. .Remove 4 .Remove" fifth" N =.Count Debug.Print" Число элементов после двух вызовов метода Remove=", N Debug.Print" Элементы коллекции:" 'И снова печать коллекции, в которой теперь четыре элемента. For i = 1 To MyCollection.Count Debug.Print MyCollection(i) Next End With End Sub5.8.
Приведем теперь результаты отладочной печати:
Число элементов пустой коллекции = 0 Число элементов после 6-и вызовов метода Add = 6 Элементы коллекции: один 2 три 4 пять 6 Число элементов после двух вызовов метода Remove = 4 Элементы коллекции: один 2 три 6
Подчеркнем еще раз основные свойства класса Collection:
- Класс позволяет объединять в коллекцию элементы разных типов, хотя чаще применяются однотипные коллекции.
- Класс объединяет в себе свойства линейного списка, динамического массива и структуры, называемой словарем , или отображением (map).
- Это список, поскольку определена операция Add, позволяющая динамически добавлять элементы в конец списка.
- Это динамический массив, поскольку все элементы индексированы и к ним возможен прямой доступ по индексу. С другой стороны, размер массива не фиксируется и динамически изменяется при добавлении и удалении элементов.
- Это словарь, поскольку добавляемые элементы могут иметь ключ. Прямой доступ к элементам возможен как по индексу, так и по ключу. Под "прямым" здесь понимается доступ, основанный на применении функций расстановки (хэширования). Конкретный вид этих функций, определяющий эффективность поиска, - "секрет фирмы".
- В классе определены методы Add, Item, Remove и свойство Count.
Элементами коллекции могут быть данные любых типов, встроенных в VBA. Не менее важно, что допускается строить коллекцию из объектов, классы которых определены программистом. В предыдущих примерах этой лекции мы использовали для построения групп массивы только по той причине, что еще не ввели понятие коллекции. Более разумно было бы использование коллекций при работе с личностями, владельцами машин и самими машинами. Построим пример, в котором действует коллекция личностей:
Public Sub Collection() 'Создание и работа с коллекцией личностей Dim Личности As New Collection 'Работа с коллекцией, как со списком Dim Адам As New Личность Адам.InitPerson "Адам", "Первый Человек", #1/1/100# Личности.Add Адам Dim Ной As New Личность Ной.InitPerson "Ной", "Праведник", #1/1/100# Личности.Add Ной 'Работа с коллекцией, как с динамическим массивом Dim Шекспир As New Личность Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564# Личности.Add Item:=Шекспир, After:=2 Dim Гомер As New Личность Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100# Личности.Add Item:=Гомер, Before:=3 Личности(4).SayWhoIs 'Работа с коллекцией, как со словарем Dim Пушкин As New Личность Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799# Личности.Add Item:=Пушкин, Key:="Гений" Dim Булгаков As New Личность Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891# Личности.Add Item:=Булгаков, Key:="Мастер" Debug.Print Личности("Гений").ВашаФамилия, " - это Гений!" Debug.Print Личности("Мастер").ВашаФамилия, " - это Мастер!" 'Печать всего списка Dim I As Byte For I = 1 To Личности.Count Личности(I).PrintPerson Next I End Sub5.9.
В процессе работы этой процедуры в диалоговом окне появится сообщение о Шекспире, а в отладочном окне 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 Sub5.10.
Теперь несколько замечаний по поводу реализации методов:
- При добавлении нового элемента в методах 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 и собственного динамического класса.