Обобщения 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 стр. : ил.
| FPC | 3.2.2 | release |
| Lazarus | 3.2 | release |
| MSE | 5.10.0 | release |
| fpGUI | 1.4.1 | release |