Стек на основе обобщений (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;
В секции 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;
продолжение следует
во, гуд :) теперь привычно (:
ОтветитьУдалитьменя вот самого тут мысль недавно посещала сделать простенький общий класс (для общего развития), для, например, выполнения простых арифметических операций. Тогда для обычных integer/double это и так будет работать. но интересно было бы создать тип - запись, и перегрузить в ней операцию сложения и т.п.
тогда обобщенный класс можно использовать и с подобными записями.
т.е в одном примере объединить две весьма интересные и мощные возможности - дженерики и перегрузка операторов.