Обобщения aka Generics |
28.06.2007 Николай Лабинский |
Разработчикам, использующим объектно-ориентированное программирование, хорошо известны его преимущества. Одно из ключевых преимуществ — возможность повторно использовать код, т.е. создавать производный класс, наследующий все возможности базового класса. В производном классе можно просто переопределить виртуальные методы или добавить новые, чтобы изменить унаследованные характеристики для решения новых задач. Обобщения (Generics) — еще один новый (начиная с версии 2.2.х) механизм повторного использования кода, а именно повторным использованием алгоритма.
По сути, разработчик определяет алгоритм, например сортировку, поиск, замену,
преобразование и т.д., но не указывает конкретный тип данных, с которым работает
алгоритм. Именно поэтому алгоритм можно обобщенно применять к объектам разных
типов. Используя готовый алгоритм, другой разработчик просто указывает
конкретный тип, например для сортировки — Integer
,
String
или даже Record
и Class
.
В FPC обобщения реализованы как своего рода макросы для компилятора, которые он выполняет при специализации (specialize), т.е. при их непосредственном использовании при указании конкретного типа. Именно поэтому описание и использование обобщений происходит за два этапа:
Описание обобщения по сути описывает новый тип: макрос, который впоследствии может выполнять компилятор.
Специализация обобщения — создание нового специализированного класса из обобщения, путем исполнения компилятором макроса из прошлого этапа.
Рассмотрим, как же описываются обобщения в FPC на простом примере списка:
type generic GList<_T> = class type public // Область типов (публичная) // Тип функции для метода ForEach TForEachProc = procedure(item: _T); var private // Область полей (приватная) Arr : array of _T; // В основе списка лежит динамический массив Len : integer; // Длина массива public // Область публичных методов function Add(item : _T): integer; procedure DeleteAt(p : integer); procedure ForEach(p : TForEachProc); procedure Clear; constructor Create; destructor Destroy; override; end;
Ну и реализация методов:
function GList.Add(item : _T): integer; begin SetLength(Arr,Len+1); Arr[Len] := item; Result := Len; inc(Len); end { Add }; procedure GList.DeleteAt(p : integer); var i : integer; begin if (p >= 0) and (p < Len) then begin for i := p to Len-2 do Arr[i] := Arr[i+1]; dec(Len); SetLength(Arr,Len); end; end { DeleteAt }; procedure GList.ForEach(p : TForEachProc); var i : integer; begin for i:= Low(Arr) to High(Arr) do p(Arr[i]); end { ForEach }; procedure GList.Clear; begin Arr := nil; Len := 0; end { Clear }; constructor GList.Create; begin inherited; Len := 0; end { Create }; destructor GList.Destroy; begin Clear; inherited; end { Destroy };
Как видно из примера, описание обобщений очень похоже не описание обычного класса за исключением локальных блоков описаний типов и переменных как в модулях или подпрограммах.
Рассмотрим некоторые особенности описания и реализации:
Тип _T
своего рода шаблон, вместо которого на этапе специализации
будет подставлен конкретный тип, заранее неизвестный. Кроме того,
идентификатор _T
не может быть использован ни для чего иного
кроме шаблона т.е.
procedure GList.ForEach(p : TForEachProc); var i : integer; _t : integer; // ошибка! begin ... end { ForEach };
Локальный блок описаний типов (в примере) содержит тип
TForEachProc
. Обратите внимание, конкретный тип неизвестен при
описании обобщения: описание содержит ссылку на шаблон _T
. Все
другие ссылки на идентификаторы должны быть известны при описании обобщения,
т.е. еще до специализации.
Локальный блок переменных, введенный для удобства и повышения «читабельности» кода полностью эквивалентен:
private Arr : array of _T; // В основе списка лежит динамический массив Len : integer; // Длина массива public // Область публичных методов function Add(item : _T): integer; ...
Оба локальных блока типов и переменных могут имеют необязательный спецификатор видимости. При его отсутствии используется текущая видимость.
Рассмотрим теперь специализацию обобщений.
Однажды описанное обобщение может быть использовано для генерации других классов: это похоже на повторение описания класса только уже с шаблонами, указывающими на конкретные типы данных.
Специализация возможна только в блоках type
и выглядит следующим
образом:
type TGL_int = specialize GList<integer>; TGL_str = specialize GList<string>;
Описание же переменных с использованием специализации запрещено:
var TGL_smpl : specialize GList<integer>; // Ошибка
Кроме того, тип специализации (тот что в угловых скобках) должен быть известен. Рассмотрим пример:
type Generic TMyFirstType= Class(TMyObject); Generic TMySecondType = Class(TMyOtherObject); ... type TMySpecialType = specialize TMySecondType<TMyFirstType>; // Ошибка!
Ошибка возникает потому, что тип TMyFirstType
лишь обобщение а
не полностью определенный тип. Однако, следующий трюк вполне работоспособен:
type TA = specialize TMyFirstType<Atype>; TB = specialize TMySecondType<TA>;
потому что TA
— полностью определенный, специализированный
тип.
Но стоит заметить, что две одинаковые специализации одного и того же шаблона нельзя присваивать друг другу что само собой вытекает из правил эквивалентности типов… Эти 2 типа просто не эквивалентны, только поэтому (Generic-и тут ни при чем) нельзя присваивать друг другу переменные разных типов (спасибо volvo877). Например тут:
type TA = specialize GList<integer>; TB = specialize GList<integer>; var A : TA; B : TB; begin A := B; // Ошибка!
присвоение В
к А
вызывает ошибку.
Ну и в конце — пример использования:
{$mode objfpc} uses GnrcLst; type TGL_int = specialize GList<integer>; TGL_str = specialize GList<string>; var l1 : TGL_int; l2 : TGL_str; procedure ForEach_int(item : integer); begin WriteLn(item) end { ForEach_int }; procedure ForEach_str(item : string); begin WriteLn(item) end { ForEach_int }; begin l1 := TGL_int.Create; l1.Add(3); l1.Add(7); l1.Add(15); Writeln('Список integer''ов:'); l1.ForEach(@ForEach_int); l1.DeleteAt(1); Writeln('Список integer''ов после удаления 1го элемента:'); l1.ForEach(@ForEach_int); l1.Free; WriteLn; l2 := TGL_str.Create; l2.Add('1th'); l2.Add('2th'); l2.Add('3th'); Writeln('Список string''ов:'); l2.ForEach(@ForEach_str); l2.DeleteAt(1); Writeln('Список string''ов после удаления 1го элемента:'); l2.ForEach(@ForEach_str); l2.Free; end.
И его результаты работы:
Running "d:ppworkt_gnrclst.exe " Список integer'ов: 3 7 15 Список integer'ов после удаления 1го элемента: 3 15 Список string'ов: 1th 2th 3th Список string'ов после удаления 1го элемента: 1th 3th
Перечислю пару плюсов/минусов обобщений:
[+] Безопасность типов. Когда обобщенный алгоритм
специализируется компилятор понимает это и не допускает работу с другими типами.
Так, вы не сможете в GList<MyClass1>
добавить элемент типа
MyClass2
несмотря на то что у них есть общий родитель
TObject
чего не скажешь о стандартном классе TList
,
который работает с указателями.
[+] Более простой и понятный код. Поскольку компилятор обеспечивает безопасность типов, в исходном коде нужно меньше приведений типов. И как следствие, такой код проще писать и поддерживать.
[-] «Распухание» кода. Компилятор будет генерировать машинный код для каждого сочетания «обобщение + специализация», что в итоге может привести к увеличению размера приложения.
[-] Новизна. В FPC обобщения только-только появляются и многие возможности пока еще не реализованы. К ним относится и отсутствие поддержки Generic-ов в процедурах/функциях что привносит некоторые неудобства…
P.S. Все исходники можно найти в аттаче.
2007 © Nikolay Labinskiy aka e-moe
При написании использовались:
Оригинальная документация к FPC
CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#. Мастер-класс. / Пер. с англ. — М.: Издательство «РУсская редакция»; СПб.: Питер, 2007. — 656 стр. : ил.