Публикации FreePascal

Обобщения aka Generics

28.06.2007
Николай Лабинский

Разработчикам, использующим объектно-ориентированное программирование, хорошо известны его преимущества. Одно из ключевых преимуществ — возможность повторно использовать код, т.е. создавать производный класс, наследующий все возможности базового класса. В производном классе можно просто переопределить виртуальные методы или добавить новые, чтобы изменить унаследованные характеристики для решения новых задач. Обобщения (Generics) — еще один новый (начиная с версии 2.2.х) механизм повторного использования кода, а именно повторным использованием алгоритма.

По сути, разработчик определяет алгоритм, например сортировку, поиск, замену, преобразование и т.д., но не указывает конкретный тип данных, с которым работает алгоритм. Именно поэтому алгоритм можно обобщенно применять к объектам разных типов. Используя готовый алгоритм, другой разработчик просто указывает конкретный тип, например для сортировки — Integer, String или даже Record и Class.

В FPC обобщения реализованы как своего рода макросы для компилятора, которые он выполняет при специализации (specialize), т.е. при их непосредственном использовании при указании конкретного типа. Именно поэтому описание и использование обобщений происходит за два этапа:

  1. Описание обобщения по сути описывает новый тип: макрос, который впоследствии может выполнять компилятор.

  2. Специализация обобщения — создание нового специализированного класса из обобщения, путем исполнения компилятором макроса из прошлого этапа.

Рассмотрим, как же описываются обобщения в 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 };

Как видно из примера, описание обобщений очень похоже не описание обычного класса за исключением локальных блоков описаний типов и переменных как в модулях или подпрограммах.

Рассмотрим некоторые особенности описания и реализации:

  1. Тип _T своего рода шаблон, вместо которого на этапе специализации будет подставлен конкретный тип, заранее неизвестный. Кроме того, идентификатор _T не может быть использован ни для чего иного кроме шаблона т.е.

    procedure GList.ForEach(p : TForEachProc);
    var
       i : integer;
      _t : integer; // ошибка!
    
    begin
      ...
    end { ForEach };
  2. Локальный блок описаний типов (в примере) содержит тип TForEachProc. Обратите внимание, конкретный тип неизвестен при описании обобщения: описание содержит ссылку на шаблон _T. Все другие ссылки на идентификаторы должны быть известны при описании обобщения, т.е. еще до специализации.

  3. Локальный блок переменных, введенный для удобства и повышения «читабельности» кода полностью эквивалентен:

    private
      Arr : array of _T;        // В основе списка лежит динамический массив
      Len : integer;            // Длина массива
    
    public              // Область публичных методов
      function Add(item : _T): integer;
      ...
  4. Оба локальных блока типов и переменных могут имеют необязательный спецификатор видимости. При его отсутствии используется текущая видимость.

Рассмотрим теперь специализацию обобщений.

Однажды описанное обобщение может быть использовано для генерации других классов: это похоже на повторение описания класса только уже с шаблонами, указывающими на конкретные типы данных.

Специализация возможна только в блоках 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 стр. : ил.

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links