Программисты, работающие на языке Паскаль, обычно тратят очень много времени на создание кода по манипулированию и обеспечению структур данных, таких как связанные списки и массивы с динамической установкой размеров. И очень часто один и тот же код имеет тенденцию к повторному переписыванию и отладке.
    Что касается традиционного языка Паскаль, он лишь предоставляет вам встроенные типы записи и массива. Все другие структуры остаются на ваше усмотрение. Например, если вы собираетесь хранить данные в массиве, то обычно вам нужно написать код для создания массива, импорта данных в массив, получение данных массива для обработки, и, возможно, вывода данных на устройство ввода-вывода. Позднее, когда потребуется новый тип элемента массива, вы начинаете все сначала.
    Было бы замечательно, если бы тип массива поставлялся вместе с кодом, обрабатывающего бы многие из тех операций, которые обычно выполняются с массивом. Это был бы тип массива, который можно было бы расширять без нарушения первоначального кода. Все это является целью создания типа ObjectWindows TCollection. Это объект, который хранит наборы указателей и обладает набором методов по манипулированию ими.
    Будучи объектами и тем самым имея встроенные методы, наборы
обладают двумя дополнительными чертами, которые имеют отношение к
обычным массивам языка Паскаль - это динамическое установка
размеров и полиморфизм.
    Размер стандартного массива в стандартном Паскале
фиксируется во время компиляции. Хорошо, если вы точно знаете, какой
размер должен иметь ваш массив, но это может быть не столь хорошо к
тому моменту, когда кто-нибудь будет запускать на вашу программу.
Изменение размера массива требует изменения исходного кода и
перекомпиляции.
    Однако, для наборов вы устанавливаете только их начальный
размер, который динамически увеличивается в процессе работы
программы, для размещения в нем всех нужных данных. Это делает ваше
приложение в его скомпилированном виде значительно более гибким.
Тем не менее, следует иметь в виду, что набор не может сжиматься,
поэтому следует быть аккуратным и не делать его неоправданно
большим.
    Второй аспект, по которому массивы могут ограничивать ваше
приложение, состоит в том, что каждый элемент массива должен
иметь один и тот же тип, и этот тип должен быть определен при
компиляции кода.
    Наборы обходят это ограничение использованием
нетипизированных указателей. Это сказывается не только на быстроте и
эффективности, но наборы могут состоять из объектов (и даже не из
объектов) разного типа и размера. Набору не нужно знать что-либо об
объектах, которые он обрабатывает. Он просто организует связь с
ними в случае необходимости.
    Наборы ограничивают традиционную мощную проверку типа языка
Паскаль. Это означает, что можете поместить нечто в набор и когда
запрашиваете это назад, компилятор уже не может проверить ваших
предположений относительно объекта. Вы можете поместить нечто как
PHedgehog (еж), а прочитать назад как PSheep (овца), и набор
никак не сможет насторожить вас.
    Как программист, работающий на языке Паскаль, вы вполне
оправданно будете нервничать по поводу результатов. Проверка типов
языка Паскаль в конце концов сберегает несколько часов при поиске
некоторых достаточно иллюзорных ошибок. Поэтому вы должны принять
во внимание следующее предупреждение. Вы даже представить себе не
можете насколько трудно бывает искать ошибки несоответствия типа,
поскольку обычно эту работу за вас выполнял компилятор! Однако,
если вы обнаружите, что ваша программа сбивается или
зацикливается, тщательно проверьте хранимые типы объектов и считываемые из
наборов.
    Вы даже можете добавить в набор нечто, что вообще не
является объектом, но это также может явиться серьезным предметом
озабоченности. Наборы ожидают получения нетипизированных указателей
незаданного типа на нечто. Но некоторые методы TCollection
предназначены специально для работы с наборами элементов, производных
от TObject. Это касается методов доступа к потоку PutItem и
GetItem, и стандартной процедуры FreeItem.
    Например, это означает, что вы можете хранить PChar в
наборе, но при попытке послать этот набор в поток, результаты будут
не столь успешными, если вы не перепишете стандартные методы
набора GetItem и PutItem. Аналогично, при попытке освобождения
набора будет сделана попытка удаления каждого элемента с помощью
FreeItem. Например, это делает TStrCollection.
    Если вам удастся преодолеть все эти трудности, вы
обнаружите, что наборы (и построенные вами производные наборов) являются
быстрыми, гибкими и надежными структурами данных.
    Создание набора столь же просто, как и создание типа данных,
которые вы хотите в нем хранить. Предположим, что вы -
консультант, и вам нужно хранить и искать номер счета, фамилию и номер
телефона каждого из ваших клиентов. Сначала определим тип объекта
клиента (TClient), который будет хранится в наборе (не забудьте
определить тип указателя для каждого нового типа объекта):
    Затем реализуем методы Init и Done для размещения и удаления
данных о клиенте и метод Print для отображения данных о клиенте в
виде таблицы. Обратите внимание, что поля объекта имеют тип
PChar, поэтому память выделяется только для той части строки,
которая действительно используется. Функции StrNew и StrDispose
очень эффективно обрабатывают динамические строки.
    TClient.Done будет автоматически вызываться для каждого
клиента при удалении всего набора. Сейчас вы просто инициируете
набор для хранения ваших клиентов и вставляете в него записи о
клиентах. Головное тело программы (COLLECT1.PAS) будет выглядеть
следующим образом:
    Примечание: Процедуры PrintAll и SearchPhone будут
рассмотрены позднее.
    Обратите внимание, насколько просто было построить набор.
Первый оператор размещает новый экземпляр TCollection с именем
ClientList с начальным размером на 10 клиентов. В случае
необходимости размещения более 10 клиентов в ClientList, его размер
будет увеличиваться каждый раз на 5 клиентов. Следующие два
оператора создают новый объект клиента и вставляют его в набор. Вызов
Dispose в конце операции освобождает весь набор клиентов.
    Нигде не нужно было сообщать набору, какой вид данных
предполагается хранить - для этого просто используется указатель.
    Вставка и удаление элемента не являются единственными общими
операторами набора. Очень часто вы будете писать циклы for для
просмотра всех объектов набора с целью отображения данных или
выполнения некоторых вычислений. В других случаях вы будете искать
первый или последний элемент набора, который удовлетворяет
некоторому критерию поиска. Для этих целей у наборов имеется три
метода итератора: ForEach, FirstThat и LastThat. Каждый из них
воспринимает указатель на процедуру или функцию в качестве своего
единственного параметра.
    ForEach воспринимает указатель на процедуру. Процедура имеет
один параметр, который является указателем на хранимый в наборе
элемент. Для каждого элемента набора ForEach вызывает процедуру
один раз, в той последовательности, в которой элементы появляются
в наборе. Процедура PrintAll в Collect1 показывает пример
итератора FoeEach.
    Для каждого элемента набора, переданного в качестве
параметра в PrintAll, вызывается вложенная процедура CallPrint.
CallPrint просто распечатывает информацию об объекте клиента в
отформатированных колонках.
    Примечание: Итераторы должны вызывать локальные
процедуры far.
    Вам нужно быть аккуратным с сортировкой процедур, которые вы
вызываете итераторами. Для того, чтобы быть вызванной итератором,
процедура (в данном примере, CallPrint) должна:
    Кроме возможности приложения процедуры к каждому элементу
набора, часто бывает очень нужно найти конкретный элемент набора
на основании некоторого критерия. Это является предназначением
итераторов FirstThat и LastThat. Как это следует из их имен, они
просматривают набор в противоположных направлениях до момента
нахождения первого элемента набора, который удовлетворяет критерию
булевской функции, переданной в качестве элемента.
    FirstThat и LastThat возвращают указатель на первый (или
последний) элемент, который удовлетворяет условию поиска.
Предположим, что в приведенном ранее примере списка клиентов, вы не
можете вспомнить номер счета клиента или не помните точно написание
имени клиента. К счастью, вы точно помните, что это был ваш
первый клиент из штата Монтана. Следовательно, вы можете
организовать поиск первого клиента с кодом штата 406 (поскольку ваш
список клиентов ведется хронологически). Данная процедура использует
метод FirstThat, который и сделает всю работу:
    Снова обратите внимание на то, что PhoneMatch вложена и
использует удаленную модель вызова. В этом случае эта функция
возвращает True только при совпадении номера телефона клиента и
заданного образца поиска. Если в наборе нет объекта, который
соответствовал бы критерию поиска, FirstThat возвращает указатель
nil.
    Запомните: ForEach вызывает определенную пользователем
процедуру, а FirstThat и LastThat каждая вызывает определенную
пользователем булевскую функцию. В любом случае определенная
пользователем процедура или функция передают указатель на объект
набора.
    Иногда вам бывает нужно, чтобы ваши данные были определенным
образом отсортированы. ObjectWindows имеет специальный тип
набора, который позволяет вам упорядочить ваши данные произвольным
образом. Это тип TSortedCollection.
    TSortedCollection - это набор абстрактного типа. Для его
использования вы должны сначала решить, какой тип данных вы
собираетесь собирать и определить два метода, отвечающих вашим
конкретным требованиям сортировки. Для этого вам нужно создать новый
тип, производный от TSortedCollection. В данном случае назовем
его TClientCollection. Ваш TClientCollection уже знает, как
делать всю реальную работу с набором. Он может вставить (Insert)
запись о новом клиенте и удалять (Delete) существующие записи
он унаследовал эти основные черты поведения от TCollection. Все
что нужно сделать - это научить TClientCollection, какое поле
использовать в качестве ключа сортировки и как сравнивать двух
клиентов при решении вопроса о том, какой из них должен стоять в
наборе выше другого. Это делается переписыванием методов KeyOf и
Compare и реализации их следующим образом:
    Примечание: Так как ключи являются нетипизированными
указателями, для них нужно выполнять приведение типа.
    KeyOf определяет, какое поле или поля используются в
качестве ключей сортировки. В данном случае это поле клиента Account.
Compare воспринимает два ключа сортировки и определяет, какой из
них должен идти первым в соответствии с правилами сортировки.
Compare возвращает -1, 0 или 1 в зависимости от того, Key1
меньше, равен или больше Key2, соответственно. В данном примере
используется сортировка по алфавиту (для букв верхнего и нижнего
регистра) ключевой строки (Account) путем вызова модуля Strings
функции StrIComp. Вы можете легко сортировать набор по именам,
вместо номера счета, если замените возвращаемое KeyOf поле на
Name.
    Обратите внимание на то, что ключи, возвращаемые KeyOf и
передаваемые в Compare являются нетипизированными указателями,
поэтому до их разыменования и передачи в StrIComp в данном примере
вы должны привести их тип к PChar.
    Это практически все, что вам нужно определить! Теперь, если
вы переопределите ClientList как PClientCollection вместо
PCollection (сменив объявление var и вызов New), то легко сможете
распечатать ваших клиентов в алфавитном порядке (COLLECT2.PAS):
    Обратите внимание и на то, как легко будет сменить
сортировку списка клиентов по номеру счета на сортировку по имени. Все
что вам нужно сделать, это сменить метод KeyOf на возврат поля
Account на поле Name.
    Многим программам требуется работать с отсортированными
строками. Для этих целей ObjectWindows предоставляет набор
специального назначения TStrCollection (он совпадает с типом
TStringCollection, определенным для хранения строк Паскаля).
Обратите внимание, что элементы TStrCollection - это не объекты.
Они представляют собой указатели на строки, заканчивающиеся
нулем. Поскольку наборы строк происходят от TSortedCollection,
можно хранить и дублированные строки.
    Использовать наборы строк несложно. Просто определяется
переменная указателя для хранения набора строк. Разместим набор,
задав его начальный размер и приращение для роста при добавлении
новых строк (см. COLLECT3.PAS):
    WordList первоначально рассчитан для хранения 10 строк с
последующим приращением по 5 строк. Все что вам нужно сделать
это вставить несколько строк в набор. В данном примере слова
считываются из текстового файла и вставляются в набор:
    Обратите внимание, что функция StrNew используется для
копирования считанных слов, и адрес скопированной строки передается в
набор. При использовании набора вы всегда передаете ему контроль
над данными набора. Он позаботится об освобождении данных после
работы. Он при этом делает то, что происходит при вызове Dispose:
удаляется каждый элемент набора, и затем удаляется сам набор
WordList.
    Метод ForEach просматривает весь набор, элемент за
элементом, и выполняет над каждым из них заданную процедуру. В
предыдущем примере процедуре PrintWord передавался указатель строки для
ее отображения. Обратите внимание, что процедура PrintWord
вложенная (или локальная). Она работает в другой процедуре, Print,
которой передается указатель на TstrCollection. Print использует
метод итератора ForEach для передачи каждого элемента своего
набора в процедуру PrintWord.
    PrintWord должен выглядеть как уже знакомая процедура. Она
просто берет указатель строки и передает его значение Writeln.
Обратите внимание на директиву far после описания PrintWord.
PrintWord не может быть методом, это просто процедура. Кроме того
это должна быть вложенная процедура. Print надо рассматривать как
некую оболочку вокруг процедуры, которая выполняет некоторую
работу над каждым элементом набора (может быть отображает или
модифицирует данные). Вы можете иметь несколько аналогичных PrintWord
процедур, но каждая из них должна быть вложена в Print и должна
быть дальней процедурой (использовать директиву far или {$F+}).
    Отсортированные наборы (и следовательно наборы строк) имеют
метод Search, который возвращает индекс элемента с конкретным
значением ключа. Но как найти элемент в неотсортированном наборе?
Или когда критерий поиска не использует сам ключ? Конечно же,
следует использовать FirstThat и LastThat. Вы просто определяете
булевскую функцию для проверки нужного вам критерия и вызываете
FirstThat.
Объекты наборов
Динамическая установка размеров наборов
Полиморфизм наборов
Проверка типа и наборы
Объединение в набор элементов, не являющихся объектами
Создание набора
type
PClient=^TClient;
TClient=object(TObject)
Account, Name, Phone: PChar;
constructor Init(NewAccount, NewName, NewPhone: PChar);
destructor Done; virtual;
procedure Print; virtual;
end;
constructor TClient.Init(NewAccount, NewName,
NewPhone: PChar);
begin
Account := StrNew(NewAccount);
Name := StrNew(NewName);
Phone := StrNew(NewPhone);
end;
destructor TClientDone;
begin
StrDispose(Account);
StrDispose(Name);
StrDispose(Phone);
end;
procedure TClient.Print;
begin
Writeln( ' ',
Account, '':10 - StrLen(Account),
Name, '':20 - StrLen(Name),
Phone, '':16 - StrLen(Phone));
end;
var
ClientList: PCollection;
begin
ClientList:=New(PCollection, Init(10,5));
with ClientList^ do
begin
Insert(New(PClient, Init('91-100', 'Anders, Smitty',
'(406) 111-2222')));
Insert(New(PClient, Init('90-167', 'Smith, Zelda',
'(800) 555-1212')));
Insert(New(PClient, Init('90-177', 'Smitty, John',
'(406) 987-4321')));
Insert(New(PClient, Init('90-160', 'Johnson, Agatha',
'(302) 139-8913')));
end;
PrintAll(ClientList);
SearchPhone(ClientList, '(406)');
Dispose(ClientList, Done);
end.
Методы итератора
Итератор ForEach
procedure PrintAll(C: PCollection);
procedure CallPrint(P: PClient); far;
begin
P^.Print; {Вызов метода Print}
end;
begin {Print}
Writeln;
Writeln;
Writeln('Client list:');
C^.ForEach(@CallPrint); { распечатка для каждого клиента }
end;
Итераторы FirstThat и LastThat
procedure SearchPhone(C: PCollection; PhoneToFind: PChar);
function PhoneMatch(Client: PClient: PClient): Boolean;
far;
begin
PhoneMatch := StrPos(Client^.Phone, PhoneToFind) <> nil;
end;
var
FoundClient: PClient;
begin { SearchPhone }
Writeln;
FoundClient := C^.FirstThat(@PhoneMatch);
if FoundClient = nil then
Writeln('Такому требованию не отвечает ни один клиент')
else
begin
Writeln('Найден клиент:');
FoundClient^.Print;
end;
end;
Отсортированные наборы
TSortedCollection является производным от TCollection и
автоматически сортирует задаваемые ему объекты. При добавлении
нового элемента он автоматически проверяет набор на дублирование
ключей. Булевское поле Duplicates контролирует разрешение
дублирования ключей. Если для поля Duplicates установлено значение
False (по умолчанию), то новый элемент добавляется к набору,
заменяя существующий член с тем же самым ключом. Если Duplicates
имеет значение True, то новый член просто вставляется в набор.
PClientCollection = ^TClientCollection;
TClientCollection = object(TSortedCollection)
function KeyOf(Item: Pointer): Pointer; virtual;
function Compare(Key1, Key2: Pointer): Integer; virtual;
end;
function TClientCollection.KeyOf(Item: Pointer): Pointer;
begin
KeyOf := PClient(Item)^.Account;
end;
function TClientCollection.Compare(Key1, Key2: Pointer):
Integer;
begin
Compare := StrIComp(PChar(Key1), PChar(Key2));
end;
var
ClientList: PClientCollection;
.
.
begin
ClientList:=New(PClientCollection, Init(10,5));
.
.
end.
Наборы строк
var
WordList: PCollection;
WordRead: PChar;
.
.
.
begin
WordList:=New(PStrCollection, Init(10,5));
.
.
.
repeat
.
.
.
if GetWord(WordRead, WordFile)^ <> #0 then
WordList^.Insert(StrNew(WordRead));
.
.
.
until WordRead[0]=#0;
.
.
.
Dispose(WordList, Done);
Пересмотренные итераторы
procedure Print(C: PCollection);
procedure PrintWord(P: PChar); far;
begin
Writeln(P); { вывести строку }
end;
begin {Print}
Writeln;
Writeln;
C^.ForEach(@PrintWord); { вызов PrintWord }
end;