Delphi10 泛型

@[TOC]

第十章 泛型

泛型举例

function TSample<T>.GetF1: T;
begin
  Result := self.F1;
end;

procedure TSample<T>.SetF1(value: T);
begin
  self.F1 := i;
end;

var
  IntegerObj: TSample<Integer>;
  StringObj: TSample<String>;
begin
  IntegerObj.P1 := 89;
  StringObj.P1 := 'Delphi';
  Writeln(IntegerObj.P1);
  Writeln(StringObj.P1);
  Readln;
end.

类的类型参数只能用于当前类或派生类中,但是类中使用的类型参数却不一定是类名称后指定的参数;

类型参数的有效范围等同于当前范围中声明的局部变量;

从泛型类派生子类时,父类可以有两种形式:带有形参的泛型类,带有实参泛型类;

父类

type
  TSample<T> = class
  private
  type
    TInnerRec<R> = record
      i: R;
    end;
    TQux<T> = class
      X: Integer;
    end;
  public
    FSample: T;
    function fun<F>(value: F): F;
  end;

子类

type
  T1<T> = class(TObject)
    function M1: T;
  end;
  T2<T> = class(T1<T>)//or T2<A> = class(T1<A>) whatever
  end;
  T3<T> = class(T1<Integer>)
  end;

T2<T>的父类是一个带有形参的类T1<T>;

T3<T>的父类则是一个带有实参的类T1<Integer>;

注释中的A可以更换,但是两者必须一致;

泛型接口和泛型记录的声明举例

type
  R1<T> = record
    F: T;
    function M1<M>(v1: M; v2: T): M;
  end;
  I1<T> = Interface
  End;
  I2<T> = Interface(I1<T>)
  End;
  I2<T> = Interface(I1<Integer>)
  End;

泛型例程指针的声明举例

type
  fun<T> = function(s: T): T;
  MyType = Integer;
function M1(value: Integer): MyType;
begin
end;
function M2(value: Integer): Integer;
begin
end;
procedure M3(value: Integer);
begin
end;
var
  pF:fun<Integer>;
begin
  pF := M2;
  pF := M1;
  pF := M3;//wrong
end.

注:类型参数可以出现在方法或例程的参数列表中,也可以作为返回值的类型;

泛型例程指针所指向的例程必须:

1.参数与返回值的类型结构必须相同,有返回值的函数指针不能指向没有返回值的过程,反之亦然;

2.参数列表必须与泛型例程指针声明时的形式一一对应,参数类型必须有相同的内部结构,二者相应参数的传参方式必须相同;

泛型类的实例化四条规则:

1.由于实例化发生在编译期间,运行时相当于调用了不同的类,所以如果在泛型类中定义了一个类变量,不同版本的类变量会保存各自的值;

2.如果一个泛型类中套嵌了另一个类,在实例化这个类时,需要实例化母类;

3.在实例化泛型类的子泛型类时,基类自动类型化;

type
  T1<Tk> = class
    i: Tk;
  end;
  T2<T> = class(T1<T>)
  end;
var
  obj: T2<String>;
begin
  obj.i := 'Delphi';
end.

在使用String实例化T2后,编译器自动产生了String版本的T1;

类型参数的标识符如T不作为类识别的依据,在声明T2时,我们将T1中的Tk变成T,

这完全没有任何关系,只要基类和派生类的类型参数标识符相同即可;

4.类中的方法没有得到明确的类型值时,可根据实际的参数值进行推测,从而实例化整个类;

type
  T1 = class
    procedure test;
    procedure M<Y>(S: Y);
  end;
procedure T1.M<Y>(S: Y);
begin
end;
procedure T1.test;
begin
  self.M<String>('wanginn');
  self.M('waggg');
  self.M(23);
end;

10.3泛型方法重载

重载方法的泛型版本和非泛型版本之间,编译器有限选择非泛型版本;

10.4泛型类型兼容

举例

type
  GArray<T> = array [0..3] of T;
  FourArray = GArray<Integer>;
var
  v1: GArray<Integer>;
  v2: FourArray;
begin
  v1 := v2;
end.

编译会通过,因为泛型中不是通过数据类型的名称来判断,而是根据实际结构;

即使是在实例化时,依然检查类型参数所指向的最终类型是否相同;

举例

type
  GArray<T> = array [0..3] of T;
  Int = Integer;          //new code
  FourArray = GArray<Int>;
var
  v1: GArray<Integer>;
  v2: FourArray;
begin
  v1 := v2;
end.

依然通过编译;

10.5泛型的限定

泛型的限定:通过某种方法可以限制泛型在实例化时能够接受的类型参数;

五种方案:接口限定,类名称限定,constructor关键词限定,class关键词限定,record关键词限定

接口限定举例

type
  T1 = Interface(IInterface)
  end;
  T1 = class(TInterfaceObject, I1)
  end;
  TCon<T: IInterface> = class
  end;
var
  obj: Tcon<I1>;
begin
end.

注:在参数类型后加上冒号,后面接一个或多个接口名称,这样就限定了这个类型必须是一个类;

这个类实现了多少其他的接口无关紧要,关键是这个类或者这个类的祖先类,直接实现了指定的所有接口本身;

类名称限定举例

type
  T1 = class
  end;
  T2 = class(T1)
  end;
  TCon<T: T1> = class
  end;
var
  obj: TCon(T2;)
begin
end.

在实例化TCon时,T的值只能是一个类或nil,如果是一个类,则必须是T1或T1的派生类;

但是Delphi不允许直接使用TObject来进行限定;

constructor关键词限定举例

type
  TCon<T: constructor> = class
    procedure test;
  end;
procedure TCon<T>.test;
var
  obj: T;
begin
  obj := T.Create;
end.

这种限定一般很少使用,使用这种限定可以直接调用T的默认构造函数及无参数的Create函数而无需知道T到底什么类型;

当然T肯定是一个类,只有类才有默认构造函数;

class关键词限定举例

type
  TCon<T: class> = class
  end;
var
  obj: TCon<TObject>;

这种方法可以限定前文提到的TObject;

此关键词可以与constructor联用,虽然没有什么意义;

record关键词限定举例

type
  T1 = class
  end;
  TCon<T: record> = class
  end;
var
  o1: TCon<T1>; //wrong
  o2: TCon<Integer>;

record限定了用于实例化的类型必须是值类型,即所有的简单类型加上记录类型;

class或record关键词不能和类名称联合限定;

record关键词也不能和class或constructor的任何一个联用;

10.6 TList类

TList类似于动态数组,是一个连续的队列,允许插队;

add添加到队尾;

insert插入指定位置;

List.Insert(3, 78)把78插入list作为第4个成员;

注:TList中成员序数绝对是从0开始,用户无法改变这点;

添加成员时,务必保证这个成员在原来的队列中存在;

AddRange和InsertRange,将数组添加到队列;

举例

program Project3;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

var
  ListArray: array [0..3] of Integer;
  I: Integer;
  List: TList<Integer>;
begin
  List := TList<Integer>.Create;
  for I := 0 to 3 do
    ListArray[I] := I + 101;
  for I := 0 to 4 do
    List.Add(I + 1);

  List.InsertRange(3, ListArray);
  for I := 0 to List.Count - 1 do
    Writeln(List[i]);//1,2,3,101,102,103,104,4,5
  Readln;
end.

五种删除成员的方法:

Remove,Delete,DeleteRange,Extract,Clear;

remove删除值为value的成员并返回序数,如果很多个value,删除最小序数的,不存在则返回-1;

如2 7 3 7 5删除7,变成2 3 7 5,返回1;

Extract和Remove非常相似,区别是Extract返回被删除的成员的值;

不存在则返回T类型的默认值,如0,nil等

Delete直接返回指定序数的成员;

DeleteRange举例

1,2,3,4,5 >>> list.DeleteRange(1, 2) >>> 1, 4, 5

Clear清空所有成员;

Exchange将Index1和Index2指定的成员的值互换;

Move将CurIndex指定的值移动到NewIndex指定的位置,这两个成员要存在;

Sort将所有成员的值顺序排序,默认由小到大;

program Project4;
{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

var
  ListArray: array [0..3] of Integer;
  I: Integer;
  List: TList<Integer>;

begin
  List := TList<Integer>.Create;
  for I := 0 to 4 do
    List.Add(I + 1);
  List.Exchange(0, 4);
  for I := 0 to List.Count - 1 do
  begin
    Write(List[I]);
    Write(' ');
  end;
  Writeln;
  List.Sort;
  for I := 0 to List.Count - 1 do
  begin
    Write(List[I]);
    Write(' ');
  end;
  Writeln;
  List.Move(1, 3);
  for I := 0 to List.Count - 1 do
  begin
    Write(List[I]);
    Write(' ');
  end;
  Writeln;
  Readln;
end.

查询队列中是否含有某个值的三种方法:Contains,IndexOf,LastIndexOf;

Contains用于查询队列中是否存在某个值与指定值相同,相同时可以用IndexOf返回此成员的序数;

如果存在多个这样的成员,IndexOf返回第一个成员的序数,LastIndexOf返回最后一个这样的成员的序数;

不存在时,都返回-1;

TList的属性Count:当前TList对象中的成员数目;

Capacity表示当前对象的容量,即能够存储的最大成员数目,设置这个属性值类似设置动态数组的成员数量;

Item索引,list.Item[2],可以省略写成list[2];

这里item是默认属性,所以可以省略