понедельник, 31 января 2011 г.

Введение в обобщенное программирование (Generics) - 2


Стек на основе обобщений (generics)

Итак, начнем создавать стек, основанный на обобщении (generics).
В название класса, добавим параметр типа, заключив его в “<”,”>”
                        
 type

   TGenericStack<T> = class

зарезервированный тип T может быть помещен в те-же самые места, как и любой стандартный тип Delphi: в конструктор, в методы класса, или в параметры функций.

type

 TGenericStack<T> = class

    genArray: Array of T;

    TopOfStack : Integer;

    constructor Create(length: Integer);

    procedure push(Value: T);

    function pop: T;

 end;


В секции IMPLEMENTATION в параметрах методов мы используем идентификатор типа T, а также добавляем параметр типа <T> после каждого названия метода. Например:
            
procedure TGenericStack<T>.push(Value: T);

begin

   . . .


Также меняем все параметры и типы переменных на T:

procedure TGenericStack<T>.push(Value: T);

begin

  if TopOfStack < Length(genArray) then

  begin

    genArray[TopOfStack] := Value;

    Inc(TopOfStack);

  end;

end;

Для того, что бы использовать новый класс, основанный на обобщении, мы должны определить с каким типом объектов мы будем работать в нашем стеке. Для этого в секции Var при объявлении переменной экземпляра класса, мы указываем конкретный тип между “<” и “>”:
     
var

  Stack: TGenericStack<Integer>;// стек для целых чисел

Соответственно указываем данный тип при вызове конструктора:

var

  Stack: TGenericStack<Integer>;

begin

  Stack := TGenericStack<Integer>.Create(5);

  with Stack do

  begin

    push(111);

    . . .
Итак, новый класс стека, использующий механизм обобщений выглядит так:
unit Generic_stack;

interface

type
TGenericStack = class
genArray: Array of T;
TopOfStack : Integer;
constructor Create(length: Integer);
procedure push(Value: T);
function pop: T;
end;

implementation

Uses SysUtils;

constructor TGenericStack.Create(length: Integer);
begin
Inherited Create;
SetLength(genArray, length);
end;

procedure TGenericStack.push(Value: T);
begin
 if TopOfStack < Length(genArray) then   
 begin     
   genArray[TopOfStack] := Value;     
   Inc(TopOfStack);   
 end; 
end;  

function TGenericStack.pop: T;
begin
 if TopOfStack >= 0 then
 begin
   Dec(TopOfStack);
   Result := genArray[TopOfStack];
 end
 else
   raise Exception.Create('empty');
end;

end.


Если мы хотим работать с типом Integer, то работу мы делаем так:

procedure UseIntegerStack;
var
  Stack: TGenericStack;
begin
  Stack := TGenericStack.Create(5);
  with Stack do
  begin
    push(111);
    push(222);
    // -- refused by the compiler
    // push('Abigail');
    writeln(pop);
    writeln(pop);
  end;
end;


а если хотим работать с собственным классом, то так

// -- person stack
type
  TPerson = Class
    FirstName: String;
    Constructor Create(vFirstName: String);
  end;

constructor TPerson.Create(vFirstName: String);
begin
  Inherited Create;
  FirstName := vFirstName;
end;

procedure UsePersonStack;
var
  Stack: TGenericStack;
begin
  Stack := TGenericStack.Create(5);
  with Stack do
  begin
    push(TPerson.Create('ann'));
    push(TPerson.Create('allistair'));
    // -- refused by the compiler
    // Push(111);
    writeln(pop.FirstName);
    writeln(pop.FirstName);
  end;
end;

Как видите, мы ушли от реализации двух классов для двух разных типов. Теперь один обобщенный класс умеет работать с разными типами. Также мы ушли от неявных приведений типов, теперь компилятор берет на себя явную проверку соответствия типов, что облегчает жизнь программисту.
Итак для того что бы двигаться дальше немного определимся с формулировками:
Идентификатор, расположенный после названия типа (T в нашем случае), называют "параметром типа" (или иногда "формальный параметр типа").
Реальный конкретный тип, который мы задаем, в секции var или в параметре (Integer нашем случае), называют "аргументом типа" (или иногда "фактическим параметром")
Для Т - безразличен любой тип языка Delphi. То есть любой тип Delphi является приемлемым. Для обобщенных классов часто в параметре типа используется только один символ – T, но в Delphi можно давать более длинные осмысленные имена, такие как measure или value что для языка тоже корректно, а для программиста более читабельно:
    type

     TCurrency<N, V>= Class

                       end;


     TDictionary<key, value>= Class

                               end;


     TCalculator<T_value, T_operation>= Class

                                         end;
Для длинных осмысленных названий мы будем использовать соглашение, где параметр типа начинает с верхнего регистра “T_” после чего идет сопровождаемое осмысленное название, как например T_Measure.
Компилятор отклонит определение второго класса с тем же самым именем, но разрешит определение класса с другими параметрами типа.
При этом возможно, как будет показано позже, использованию нескольких параметров типа. Если "подпись параметра" является другой, то мы можем использовать, то же самое название класса.
    type

      TStack<T_gen>= Class

                      end;


     // Так будет отклонено

     // т.к. второй класс с тем же самым именем :  
     // TStack<T_gen_2>= Class

     

     // А вот это корректно, так как есть другие параметры типа

     TStack<T_gen_1, T_gen_2>= Class

                                end;                                 
 продолжение следует

1 комментарий:

  1. во, гуд :) теперь привычно (:
    меня вот самого тут мысль недавно посещала сделать простенький общий класс (для общего развития), для, например, выполнения простых арифметических операций. Тогда для обычных integer/double это и так будет работать. но интересно было бы создать тип - запись, и перегрузить в ней операцию сложения и т.п.
    тогда обобщенный класс можно использовать и с подобными записями.
    т.е в одном примере объединить две весьма интересные и мощные возможности - дженерики и перегрузка операторов.

    ОтветитьУдалить