Использование Generic-ов (FPC) | |
Еще о Generic-ах: Часть 2 | |
Я уже приводил несколько способов написания модулей обработки данных, независимо от их типа...
Например, здесь описывался метод, при котором части Interface и Implementation модуля реализуются
в отдельных файлах, и включаются в собственно модуль директивами {$I <файл_описания_интерфейса>} и
{$I <файл_описания_реализации>}. Там же приведены и недостатки этого метода.
Немного более эффективным является метод, работающий при использовании компилятора FPC, описанный
вот тут (с перегрузкой операторов)... Этот метод уже лучше, но все равно не лишен недостатков, в
частности, все так же нельзя в одной и той же программе использовать структуру с разными типами данных.
Был, наконец, описан и способ с использованием наследования для реализации подобных структур. Он,
в отличие от предыдущих двух способов, позволяет использование нескольких структур с разными типами хранимых данных, но
программа получается более сложной и для ее использования необходимо проделать бОльшую работу.
С выходом в свет версии 2.2.0 компилятора FPC для решения поставленной задачи можно пользоваться дженериками (Generics), которые
представляют собой нечто вроде макросов для компилятора, которые "раскрываются" при специализации
дженерика.
Попробуем с помощью дженериков реализовать ту же самую задачу, которая приводилась как тест класса TQueue
здесь :
"Каким бы образом сделать так, чтобы на вывод шёл не только первый встреченный искомый элемент с его местоположением (ну например максимальный), но и другие равные ему, с их родными индексами?"
type generic tqueue<T> = object // В этой секции будут описываться типы, которые специфичны для данного класса (локальные типы). // Ими можно будет пользоваться только внутри класса, извне они недоступны... Кроме этого, для // типов можно задать область видимости, которая будет соблюдаться при наследовании. (* Что касается поставленной задачи - поскольку тип Titem используется исключительно внутри класса, я задал его как локальный тип, из основной программы к нему нельзя будет обратиться. Все действия над элементами очереди будут производиться посредством вызова процедуры ForEach, в которой это действие и будет определено для отдельного элемента. *) type public TForEach = procedure(value: T); PTitem = ^Titem; Titem = record info: T; next: PTitem; end; // То же самое - для локальных переменных... (* В данном случае - локальные переменные head и tail объявлены частными, т.е., они не будут доступны даже наследникам класса TQueue (принцип упрятывания информации) *) var private head, tail: ptitem; public (* Здесь все осталось, как и было раньше, за исключением того, что добавлена процедура ForEach *) constructor init; destructor done; procedure put(x: T); function get(var value: T): boolean; procedure clear; function empty: boolean; procedure ForEach(p : TForEach); end;Примечание:
// Получаем процедуру, которая будет применяться к каждому элементу очереди ... procedure tqueue.ForEach(p : TForEach); var pt: ptitem; begin // ... и для всех элементов - вызываем переданную процедуру pt := head; while assigned(pt) do begin p(pt^.info); // <--- передавая ей сам элемент в качестве параметра. // Пускай она и разбирается, что с ним делать... pt := pt^.next end; end;
type // Определим сам тип Pair pair = record one, two: integer; end; // ... функцию для удобной инициализации переменной этого типа // (для того, чтобы можно было ее использовать непосредственно в выражениях) ... function _pair(a, b: integer): pair; begin result.one := a; result.two := b; end; // ... ну и процедуру, которая, собственно, и будет использоваться // для вывода на экран содержимого очереди procedure print_pair(value: pair); begin write('<', value.one, ':', value.two, '> '); end;
// Указываем компилятору тот тип данных, с которым будет использоваться очередь типа pairQueue ... type pairQueue = specialize TQueue<pair>; // ... и объявляем переменную этого типа ... var q: pairQueue; ...Объявление типа pairQueue сделано не случайно - дело в том, что специализировать Generic-и можно только в разделе Type, но нельзя конструировать новые типы при описании переменных. Это чем-то напоминает запрет на подобные описания:
// Нельзя !!! procedure P(var arr: array[1 .. 20] of integer); // Хотя вот это вполне допустимо: type Tarr = array[1 .. 20] of integer; procedure P(var arr: Tarr);За подобным ограничением стоит не что иное, как правила эквивалентности типов: два описанных в разных местах типа (даже совершенно одинаковых с точки зрения программиста) считаются компилятором разными:
type TA = array[1 .. 10] of integer; TB = array[1 .. 10] of integer; var A: TA; B: TB; begin A := B; // <--- Нельзя, типы разные !!! end.При специализации действует то же правило. Если бы можно было написать:
var qa: TQueue<integer>; qb: TQueue<integer>; ... qa := qb; // <--- здесь была бы ошибкаВышеуказанное ограничение (на специализацию только в разделе type) как раз и призвано уменьшить количество подобных ошибок.
type pairQueue = specialize TQueue<pair>; var q: pairQueue; max, i, j: integer; begin // При инициализации очереди теперь нет необходимости "запоминать" тип элементов, // с которым она работает - попытки записать в очередь элемент другого типа будут // отловлены еще на этапе компиляции q.init; max := - maxint; for i := 1 to n do begin for j := 1 to n do begin if arr[i, j] > max then begin max := arr[i, j]; q.clear; q.put(_pair(i, j)); end else if arr[i, j] = max then q.put(_pair(i, j)); end; end; q.ForEach(@print_pair); // Вот так теперь распечатывается очередь q.done; end.