delphi D11编程语言手册 学习笔记(P424-477) 泛型
这本书可以在 Delphi研习社②群 256456744 的群文件里找到. 书名: Delphi 11 Alexandria Edition.pdf
泛型在C++中叫做类型模板(template classes),单从字面上理解,模板是将一个事物的结构规律予以固定化、标准化的成果,它体现的是结构形式的标准化
也就是说泛型中的类是不确定的,把指定的类型放进出,出来的就是什么样的类型,避免同样格式的类写N遍!像类和接口一样,泛型也有个不成文的约定,就是以字母T来表示泛型.
泛型大多数情况下是用来处理集合对象的.这也是泛型的最基础用法.
TKeyValue<T> = class //定义泛型
private
FKey: string;
FValue: T;
procedure SetKey(const Value: string);
procedure SetValue(const Value: T);
public
property Key: string read FKey write SetKey;
property Value: T read FValue write SetValue;
end;
{ TKeyValue<T> }
procedure TKeyValue<T>.SetKey(const Value: string);
begin
FKey := Value;
end;
procedure TKeyValue<T>.SetValue(const Value: T);
begin
FValue := Value;
end;
//调用
var
//这里把泛型声明为string类型,你也可以声明为更多类型
//但是一旦确定了这个类型,你就不再传入不兼容的值了,比如 test.setvalue(100)就会出错
test: TKeyValue<string>;
begin
test := TKeyValue<string>.Create;
test.SetValue('abc'); //设置参数要与声明的类型一致,不然会产生错误
ShowMessage(test.FValue); //show abc
test.Free;
end;
内联变量使用泛型:
begin
var kvi:=TkeyValue<integer>.Create;
end;
除了类,泛型还可以用于定义数组,结构,返回值,方法和参数
TArr<T> = array[0..9] of T; {定义一个泛型数组}
TRecord<T> = record // 或者是 TRecord = record 都可以
class procedure ArrayAdd(var Arr: TRecord<T>; const item: T); static; //结构中的 class 方法必须是 static 的
end;
下面这个例子叫泛型函数,仅限于告知各位,泛型可以这么用,但是极不推荐大家用,也是由书上例子引伸开来的,我极度怀疑是作者在装B.虽然在type时完全合法合规,但是很难处理各类型间的运算问题!
type
TTese<TInput1, TInput2, TRenturn>= class
public
function Fa(value1: TInput1; value2: TInput2): TRenturn;
end;
{ TTese<TInput1, TInput2, TRenturn> }
{ 首先是这参数就不能直接使用,然后就是TReturn的类型也没有确定下来.根本就是在自找苦吃 }
function TTese<TInput1, TInput2, TRenturn>.Fa(value1: TInput1; value2: TInput2): TRenturn;
begin
Result := value1 + value2; //E2015 运算符不适用于此运算对象类型
end;
泛型类型的兼容性规则
下面的代码会因为类型不兼容导致出错,尽管它们的结构是模一样的
type
TArrayOf10 = array[1..10] of Integer;
procedure TForm1.Button1Click(Sender: TObject);
var
Array1: TArrayOf10;
Array2: TArrayOf10;
Array3, array4: array[1..10] of Integer;
begin
Array1 := Array2;
Array2 := Array3; // Error,一个是类数组,一个是数组
Array3 := array4;
array4 := Array1; // Error
end;
解决的方案是用泛型进行定义.以获得类型兼容.
type
TIntGenericArray<T>= class
arr: array[1..10] of T;// 定义成泛型,而不是显类数组
end;
TGenericArray = TIntGenericArray<Integer>;// 定义为整形类数组
procedure TForm1.Button1Click(Sender: TObject);
var
Array1: TIntGenericArray<Integer>; //整形类数组
Array2: TIntGenericArray<Integer>; //整形类数组
Array3, array4: TGenericArray; //整形类数组
begin
Array1 := TIntGenericArray<Integer>.Create;
Array2 := Array1;
Array3 := Array2;
array4 := Array3;
Array1 := array4;
end;
通过继承泛型来进行兼容
type
TIntGenericArray<T>= class
arr: array[1..10] of T;
end;
TGenericArray = class(TIntGenericArray<Integer>);//继承
procedure TForm1.Button1Click(Sender: TObject);
var
Array1: TIntGenericArray<Integer>;
Array2: TIntGenericArray<Integer>;
Array3, array4: TGenericArray;
begin
Array1 := TIntGenericArray<Integer>.Create;
Array3 := TGenericArray.Create;
Array1 := Array3; //两者兼容
end;
泛型类型每实例化一次,编译器就会产生一个新的实体,每个实体之间都是独立存在,不会共享源码的. 甚至于在跨单元引用泛型时,编译器也会被迫在目标单元再生成一份相同的源码
这会导致程序体积增大!解决的方案是把泛型里的方法定义在一个非泛型的类里面,然后在目标单元,再定义一个泛型来继承这个普通类.书上是这么说,但是我感觉不太对劲,方法都在普通类里写完了,我还要你这泛型做什么啊?不理解
泛型类型的函数
1.Default(T):如果我们传入一个非泛型的类型,它会返回空值,或0,或nil.
2.TypeInfo(T): 返回当前泛型实例的指针.常作: GetTypeName(.TypeInfo(T));
3.SizeOf(T):返回内存地址的大小字节.在32位系统返回4Bytes,64位则返回8Bytes;
4.IsManagedType(T)返回一个布尔值.表示该类型在内存中是否受管控,如果是字符串或者动态数组的话,会返回True;
5.HasWeakRef(T)这个函数是跟支持ARC的编译器相关的,用来判断内存引用是否为弱引用 .需要特定的内存管理支持.
6.GetTypeKind(T)与TypeInfo(T)类似,但要比TypeInfo(T)更全面.
type
TSampleClass <T> = class
private
FData: T;
public
procedure Zero;
function GetDataSize: Integer;
function GetDataName: string;
end;
function TSampleClass<T>.GetDataSize: Integer;
begin
Result := SizeOf (T);
end;
function TSampleClass<T>.GetDataName: string;
begin
Result := GetTypeName (TypeInfo (T));
end;
procedure TSampleClass<T>.Zero;
begin
FData := Default (T);
end;
泛型约束(不建议深入)
在泛型类型中,我们能对泛型类型的值所做的处理很少 ,通过下面的几个约束,我们能让泛型值做更多的事件.
1.类型限制(Class Constraints)
该约束要求T必须是一个类型:<T: class> 这样做的好处是,可以让泛型使用TObject的任何方法了,包括虚拟方法.
type
TSampleClass <T: class> = class
private
FData: T;
public
procedure One;
function ReadT: T;
procedure SetT (t: T);
end;
procedure TSampleClass<T>.One;
begin
if Assigned (FData) then
begin
Form30.Show ('ClassName: ' + FData.ClassName);//使用TObject的方法
Form30.Show ('Size: ' + IntToStr (FData.InstanceSize));//使用TObject的方法
Form30.Show ('ToString: ' + FData.ToString);//使用TObject的方法
end;
end;
//其他略
//调用
sample1: TSampleClass<TButton>;
sample2: TSampleClass<TStrings>;
sample3: TSampleClass<Integer>; // E2511 Type parameter 'T' must be a class type
你也可以定义成:<T: Record>,但这跟普通的记录类型并没有多大不同,意义不大
2.特定的类型约束
以下面的举例为例,这种泛型类型只支持组件类型,也就是TComponent的衍生类.用处太少(基本没什么用)
type
TCompClass <T: TComponent> = class
演示的代码很长,不过能看到这里的朋友应该能看得懂,先说结论吧
普通接口在使用时,有接口计数器介入,而在泛型接口里是没有接口计数器介入的.
泛型接口可能有多重约束,比如构造函数的约束,以及不同类型的泛型函数的约束.说人话就是使用具有接口约束的泛型类型,具有接口的优点,而去掉了接口的缺点.
<然后我发现,网上居然有文章,是跟书上的一模一样,所以我就直接搬过来吧>
通过接受一个限定的参数,这个参数是实现某个接口的类,比较起直接接受泛型,而限制这个泛型的类要更加灵活。也就是通常所说的面向接口式的编程。这样可以达到调用实现了这个接口的各种泛型的实例。这种对泛型使用接口约束的应用,在.net框架中有很广泛的应用.
4.预设构造函数约束<T: class, constructor>
注意这里的构造函数是没有参数的.
type
TConstrClass <T: class, constructor> = class
private
FVal: T;
public
constructor Create;
function Get: T;
end;
constructor TConstrClass<T>.Create;
begin
FVal := T.Create;
end;
泛型容器(STL)
编译器自带的泛型容器,可以通过引用System.Generics.Collections单元调用.
type
TList<T> = class
TQueue<T> = class
TStack<T> = class
TDictionary<TKey,TValue> = class
TObjectList<T: class> = class(TList<T>)
TObjectQueue<T: class> = class(TQueue<T>)
TObjectStack<T: class> = class(TStack<T>)
TObjectDictionary<TKey,TValue> = class(TDictionary<TKey,TValue>)
1.TList<T>列表容器
它包含了所有原有的方法,像是 Add, Insert, Remove, 以及 IndexOf(LastIndexOf 是从后面找; 也可用 List.Contains(str) 判断是否包含 str)。同时也提供了 Capacity跟 Count 属性。只是 Items 变成了 Item,而且是默认属性(可以直接用变量名称加上方括号来存取,不用透过属性名称),不过我们不常直接用这种方式存取。
泛型容器单元(Generics.Collections)[1]: TList<T>
TQueue队列列表, 先进先出(从头开始删除):参考仓库准则,先进先出
TQueue 主要有三个方法、一个属性: Enqueue(入列)、Dequeue(出列)、Peek(查看下一个要出列的元素); Count(元素总数). 泛型容器单元(Generics.Collections)[2]: TQueue<T>
TStack堆栈列表,后进先出(从后面开始删除):像弹匣一样,后压入的子弹会被最先打出去.
TStack 主要有三个方法、一个属性:Push(压栈)、Pop(出栈)、Peek(查看下一个要出栈的元素);Count(元素总数). 泛型容器单元(Generics.Collections)[3]: TStack<T>
TDictionary 类似哈希表.是所有泛型容器中最值得我们花时间来学习的一种类型.用来存储对象时,TDictionary 的效率要比TStringList高得多. 泛型容器单元(Generics.Collections)[4]: TDictionary<T>
uses Generics.Collections;
procedure TForm1.Button1Click(Sender: TObject);
var
Dictionary: TDictionary<string,Integer>;
b: Boolean;
T: Integer;
begin
Dictionary := TDictionary<string,Integer>.Create();
{添加}
Dictionary.Add('n1', 111);
Dictionary.Add('n2', 222);
Dictionary.Add('n3', 333);
{判断指定的 Key 是否存在}
b := Dictionary.ContainsKey('n1');
ShowMessage(BoolToStr(b, True)); {True}
b := Dictionary.ContainsKey('n4');
ShowMessage(BoolToStr(b, True)); {False}
{判断指定的 Value 是否存在}
b := Dictionary.ContainsValue(111);
ShowMessage(BoolToStr(b, True)); {True}
b := Dictionary.ContainsValue(999);
ShowMessage(BoolToStr(b, True)); {False}
{使用 AddOrSetValue 时, 如果 Key 存在则替换值; 此时如果用 Add 将发生异常}
Dictionary.AddOrSetValue('n1', 123);
ShowMessage(IntToStr(Dictionary['n1'])); {123}
{使用 AddOrSetValue 时, 如果 Key 不存在则同 Add}
Dictionary.AddOrSetValue('n4', 444);
ShowMessage(IntToStr(Dictionary['n4'])); {444}
{尝试取值}
if Dictionary.TryGetValue('n2', T) then
ShowMessage(IntToStr(T)); {222}
Dictionary.Free;
end;
2.对象容器,包括
type TObjectList<T: class> = class(TList<T>) TObjectQueue<T: class> = class(TQueue<T>) TObjectStack<T: class> = class(TStack<T>)
一旦对象被从列表中删除,对象即被释放.
已经有了: TList<T>、TQueue<T>、TStack<T>、TDictionary<TKey,TValue>
为什么还有: TObjectList<T>、TObjectQueue<T>、TObjectStack<T>、TObjectDictionary<TKey,TValue> ?
还记得 Classes.TList 和 Contnrs.TObjectList 的主要区别吗?
如果元素是对象, Contnrs.TObjectList 在删除元素时会同时释放对象, 而 Classes.TList 不会.
同样在这里, Generics.Collections.TObjectList<T> 会同时释放对象, 而 Generics.Collections.TList<T> 不会.
其他也是一样.
泛型容器单元(Generics.Collections)[5]: TObject...<T> 系列
再谈泛型接口
在泛型接口中,不需要GUID作为该接口的标识(或称IID).编译器会在泛型接口实例化时自动建立一个IID,算是隐式定义吧.
type
IGetValue<T>= interface
function GetValue: T;
procedure SetValue(Value: T);
end;
TGetValue<T> = class(TInterfacedObject, IGetValue<T>)
private
FValue: T;
public
constructor Create(Value: T);
destructor Destroy; override;
function GetValue: T;
procedure SetValue(Value: T);
end;
{ TTest<T> }
constructor TGetValue<T>.Create(Value: T);
begin
Form1.Memo1.Lines.Add('TTest<T>.Create');
end;
destructor TGetValue<T>.Destroy;
begin
Form1.Memo1.Lines.Add('TTest<T>.Destroy');
inherited;
end;
function TGetValue<T>.GetValue: T;
begin
Result := FValue;
end;
procedure TGetValue<T>.SetValue(Value: T);
begin
FValue := Value;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
AVal: TGetValue<string>; {也可以定义为接口AVal:IGetValue<string>;因为和标准接口一样,接口变量也是可以直接用被调用的类来赋值的 }
begin
AVal := TGetValue<string>.Create(Caption); { Caption: vcl.controls.TCaption }
try
Form1.Memo1.Lines.Add('TGetValue value: ' + AVal.GetValue);
finally
AVal.Free;
end;
end;
预先定义的泛型接口
Generics.Default 单元文件定义了两个用来比较泛型的接口:
1. IComparer<T>拥有一个 Compare 方法
2. IEqualityComparer<T>则拥有 Equals 跟 GetHashCode 方法
type TComparer<T> = class(TInterfacedObject, IComparer<T>) TEqualityComparer<T> = class(TInterfacedObject, IEqualityComparer<T>) TCustomComparer<T> = class(TSingletonImplementation,IComparer<T>, IEqualityComparer<T>) TStringComparer = class(TCustomComparer<string>)
智能指针(Smart Pointer ) :在构造函数中传入要管理的堆对象的引用,在析构函数里FreeAndNil这个堆对象的引用
智能指针的概念是从C++编程语言而来的.
我们知道,栈对象的声明周期由后台管理,栈对象在声明时进行构造,当方法退出或者类被销毁时(此时栈对象为类的成员变量),栈对象的生命周期也会随着结束,后台自动会调用它们的析构函数并释放栈空间。
而堆对象必须由程序员手动的释放,如果一个方法只有一两个堆对象我们还能应付的过来,但是当堆对象非常多,而且堆对象一般都要经过多个方法的传递、赋值,传递到最后,非常容易忘了delete,造成内存泄露。
能不能让后台也去自动管理堆对象的释放呢?前辈们想到一个办法,就是让一个栈对象包含一个堆对象的引用,当栈对象被后台自动释放时,会调用栈对象的析构函数,于是,在栈对象的析构函数里写下delete堆对象指针的语句。这样,就完成了后台间接管理堆对象
Delphi中的interface
从智能指针的简介中我们可以了解到,要使用智能指针,我们必须得捕获到栈对象的构造函数,将堆对象的指针传入栈对象,由栈对象保存堆对象的指针;还必须捕获到栈对象的析构函数,在栈对象的析构函数里进行对构造函数所传入堆对象指针delete。在c++很容易做到这一点,但是经上面分析,我们无法对Delphi的栈对象进行构造和析构的捕获。 我们可以换一种角度思考,不一定非要是栈对象,只要在Delphi中能有一种东西,只要出了它的作用域,它就能自动析构!