P344-365 接口
"接口" 的概念和 "类" 特别是 "抽象类" 近似, Delphi 之初并没有接口, 后来(Delphi 3)为了支持 COM 引入了接口, 再后来发展成为 Delphi 重要的语言特性.
使用 COM 步骤可能是这样的:
1.程序在使用组件之初, 先联系 "接口"; 这应该是从注册表中查询.
2.找到后, 如果此接口还没有被实现, 马上调用 "类厂" 建立起对象, 并同时给接口计数器加 1.
3.可能会不止一个程序在使用同一个接口, 但每有使用则 "计数器+1", 每次用完则 "计数器-1".
4.当接口的使用计数为 0 时, 系统自动释放类厂为该接口建立的对象(垃圾回收机制).
下面用代码来理解这段话:
Type IFace = interface //声明一个接口 procedure proc; end; TAType = class(TInterfacedObject, IFace) //调用接口 constructor Create; //构造函数 destructor Destroy; override; //解析函数 procedure proc; //在存在构造函数或解析函数时,方法属性一律写在最后,否则出错 end; var Form2: TForm2; implementation {$R *.dfm} { TAType } constructor TAType.Create; begin inherited; Form2.memo1.lines.add('TAType Constructor!'); end; destructor TAType.Destroy; begin inherited; Form2.memo1.lines.add('TAType Destroy!'); end; procedure TAType.proc; begin Form2.memo1.lines.add('TAType.proc!'); end; procedure TForm2.Button1Click(Sender: TObject); var A: IFace; //注意这里,这里声明的是一个接口 begin A:= TAType.Create ; //然而这里去是用的类的构造函数,这里是接口的一个妙用.接口变量可以接一个实现了该接口的类 A.proc; //A:=nil; //主动释放内存,同样的,拥有它的类也会被释放掉! Form2.memo1.lines.add('---end---'); end;
与类相比,接口侧重于封装,并提供与类之间一种比继承更宽松一点的连接.
除了宣告抽象类(拥有抽象方法的类别),在 Object Pascal 里面我们也可以撰写纯粹的抽象类;也就是只包含虚拟抽象方法的类别。透过使用特别的关键词,interface 来定义一组作为接口(interfaces)的数据型别。
从技术面来看,接口不算是类,虽然接口可以重组类.因为类可以建立实体,但是接口不行.接口可以被一个或者多个类调用,所以这些实体就可以算是支持了或者调用了该接口.讲人话就是: 接口是用来给类调用的抽象类.
类与接口的对比:
1. 接口类型的变量会修改接口计数器,跟类类型的变量不同,接口提供了一系列的自动内存管理机制
2. 一个类可以继承多个接口(要列在基础类后面):
TMyClass = class(父类, 接口1, 接口2, ...) //Some Code end;
但是一个接口只能从另一个接口继承过来,而不能同时继承多个接口,如果继承的对象是根接口Iinterface,则可省略.
IMyInterface1 = interface(Iinterface) function Func1: Integer; function Func2: Integer; end;
3. 所有的类都是从TObject中衍生而来,所有的接口都是从IInteface衍生出来的,两都是各自独立正交的架构.
4. 不成文约定:所有类名,除异常类型外以E开头外,其他都以T开头,而所有接口都以字母I开头.
5. 接口只有方法与属性没有字段
6. 接口成员都是公开的,不需要private/protected/public/published等修饰语
7. 因为接口只声明、无实现, 所以也用不到继承与覆盖相关的修饰(virtual、dynamic、abstract、override).
8. 不管实现接口的类有多么丰富, 接口只拥有自己声明的成员.
9. 实现接口的类一般继承于 TInterfacedObject, 直接从 TObject 继承会增加一些麻烦而重复的工作.
10. 接口在用完后会自动释放, 并同时释放拥有它的类; 这很方便, 但同时带来很多问题.
11. 每个接口具有唯一的标识符,称为GUID,在接口内按下Ctrl+Shift+G,IDE会自动产生一组GUID
IFace = interface //GUID,平常是由系统分配的,所以基本不用理会.这里只是演示 ['{E5AC8E60-2DBE-43C5-B679-27731A56F3D4}'] procedure proc; end;
接口的声明与调用:
和类的声明一样,接口也是在interface区域下用关键字type进行声明:
unit Unit1; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; //接口只有属性和方法,没有字段!并且只能继承一个祖先,如果是根接口,可以省略不写 //接口1 IFace1 = interface function Fn1(Str: string): string; //抽象方法,只有声明没有实体,按Ctrl+Shift+c无响应 end; //接口2 IFace2 = interface procedure Proc2; //抽象方法,只有声明没有实体,按Ctrl+Shift+c无响应 end; //类调用上面的两个接口,注意先后顺序,先类,后接口,不然报错. //接口里有什么方法,调用的类里面就要定义什么方法,说白了就是抄一份下来 //否则会提示 接口方法 IFace1.test 没有实现部分 TAType = class(TInterfacedObject, IFace1, IFace2) //先类后接口 function Fn1(Str: string): string;//这里要按Ctrl+Shift+c来完成方法实体 procedure Proc2; end; var Form1: TForm1; implementation {$R *.dfm} { TAType } //方法实体 function TAType.Fn1(str: string): string; begin Result := ('Fn1:' + str); end; //方法实体 procedure TAType.Proc2; begin ShowMessage('Fn2'); end; procedure TForm1.Button1Click(Sender: TObject); var TA: TAType; begin Memo1.Clear; Memo1.Lines.Add(TA.Fn1('哈哈哈'));//接口调用 TA.Proc2;//接口调用 end; end.
接口属性:
{此接口声明了一个 Name 属性; 因为接口没有字段, read/write 都只能从方法} IMyInterface = interface function GetName : string; procedure SetName(val : string); property Name : string read GetName write SetName; end; {类实现的是接口的读写方法, 属性还是属于接口的; 类可以提供一个储存属性的字段} TMyClass = class(TInterfacedObject, IMyInterface) private FName: string; public function GetName: string; procedure SetName(val: string); end;
接口可以被多次调用.因为接口的方法是抽象的,实体部分要到调用接口的类里面才具现,所以类调用接口的方法时,方法能执行什么功能,还不是你说的算?
type //接口 IMyInterface1 = interface function Func(a,b: Integer): Integer; end; //调用接口的类1,这里的方法执行的是加法 TAdd = class(TInterfacedObject, IMyInterface1) public function Func(a: Integer; b: Integer): Integer; //Result:= a+b; destructor Destroy; override; end; //调用接口的类2,这里的方法执行的是乘法 TMul = class(TInterfacedObject, IMyInterface1) public function Func(a: Integer; b: Integer): Integer; //Result:= a*b; destructor Destroy; override; end;
弱化引用[weak]使变量的接口计数器不被修改.
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; IFace = interface procedure test; end; Ttype = class(TInterfacedObject, IFace) destructor Destroy; override; procedure test; end; var Form1: TForm1; implementation {$R *.dfm} { Ttype } destructor Ttype.Destroy; begin Form1.Memo1.Lines.Add('Destroy'); //在实体中调用控件的话,要加上完整的对象位置. inherited; end; procedure Ttype.test; begin Form1.Memo1.Lines.Add('test'); end;
调用:
procedure TForm1.Button1Click(Sender: TObject); var // [weak] a: IFace; begin a:= Ttype.Create ; a.test ; Form1.Memo1.Lines.Add('---') end;
可以看到,在不使用 weak 弱化引用时,程序正常执行.
如果把 [weak] 解除注释,则会出现内存错误.因为当a被创建时,因为weak的原因,a的接口计数器没有被加1,仍然等于0,所以系统就自动执行了destroy解析函数,把a给翻译掉了.当再执行a.test时,因为对象已经不存在,所以就会内存报错
[unSafe]和[weak]不同,它不会主动去清除接口计数器,它是憜性的,下面的例子还是延用上面的代码,可以看到Destroy程序并没有被执行
再看一个网上的例子:
type TA=class(TInterfacedObject) end; procedure TForm1.Button1Click(Sender: TObject); var a:IInterface; [weak]aweak:IInterface; begin a:=TA.Create; //创建对象,复制给a,执行完成后引用计数器=1 aweak:=a; //由于aweak定义有[weak]属性,所以赋值给aweak后,引用计数器依旧为1,但aweak变量的地址被保存到一个weak关联列表中 Memo1.Lines.Add(Format('Ptr:%d', [NativeInt(Pointer(aweak))])); a:=nil; //由于引用计数器=1,执行此句后,计数器清0,对象被释放,同时与此对weak关联列表中所有变量也被赋值为nil,包括aweak变量. Memo1.Lines.Add(Format('Ptr:%d', [NativeInt(Pointer(aweak))])); end;
运行结果:
Ptr:16360080
Ptr:0
weak引用非常适合用于两个对象需要互相引用的情况下,如果以往的引用,将无法让引用计数器清0.
方法别名:
IFace = interface ['{3BA60C4E-CA74-489C-B74B-3FA791BC2843}'] procedure Ptest; function Ftest: string; end; IFace2 = interface ['{324AB107-95F1-4145-B392-2E6F555822E3}'] function Ftest: string; end; Ttype = class(TInterfacedObject, IFace, IFace2) destructor Destroy; override; procedure Ptest; function F1: string; function F2: string; function IFace.Ftest = F1; function IFace2.Ftest = F2; end;
但是我在调用时,调不出F1和F2,不知道问题在哪里,在两个群问了半天,也没有等来答案,如果有大佬知道答案,麻烦指点一下,谢谢啦
P366-392 类操作
1.类方法与类函数.
在类里面声明的过程或者函数,只要在前面加上关键字class,就会变成类方法或者类函数.
类方法不能在 private 和 protected 区;
类方法不能是虚方法;
类方法只能使用类中的、在对象实例化以前的数据.
类方法中包含一个隐藏参数self,它是指向类本身的一个参数
字段不能定义在published区域.在没有任何修饰符的情况下,默认就是published.
Ttest = class private class var i: Integer; var n: Integer; public var j: Integer; class var k: Integer; class procedure Ptest; class function Ftest(o: integer): Integer; end; --------------------------------- { Ttest } class function Ttest.Ftest(o: integer): Integer; begin Result := o * 5; end; class procedure Ttest.Ptest; begin i := k * 2; ShowMessage(i.ToString); ShowMessage(Self.ClassName); end; //调用 procedure TForm1.Button1Click(Sender: TObject); begin Ttest.i := 100; //i=100 // Ttest.n := 100; //ERROR,需要create // Ttest.j := 100; // ERROR,需要create Ttest.k := 100; //k=100 Ttest.Ptest; //show 200(K*2) show Ttest ShowMessage(ttest.Ftest(10).ToString);//show 50(10*5) end;
上面的代码中,从头到尾都没有对TTest进行create过,但是对于在定义阶段有加class的方法和字段,跟全局变量一样都是可以直接拿来使用的.相反,没加class的字段,在直接使用时全部都出现了异常.因为他们都需要创建实例后才能正常使用.
警告:使用没有经过初始化(赋值)的字段会抛出异常错误!
类方法就是通过类名就可以访问的方法.或者你可以把它理解为命名空间也行.因为全局变量不被提倡,放到类里面就很好处理了,因为类里面有private, strict private ,public ,published ,protected等各种保护限制.当你要调用这些类方法或者类函数时,你得这么写: 类名.类方法,或者 类名 .类函数,这样就达到命名空间的效果.
调用时,这个类可以完全不用create而直接使用它内部的类方法与类函数:
procedure TForm1.Button1Click(Sender: TObject); var MyType: TMyType; str: string; begin MyType.Ptest; str := MyType.F1; MyType.Destroy; end;
2.类的静态方法
{现在的 Delphi 不仅仅有类方法, 同时有: 类变量: class var 类常量: class const 类类型: class type 类属性: class property 静态类方法就是给类属性来调用的, 它可以存在于私有区(private), 譬如下面的 SetName 就是一个静态类方法: } TMyClass = class(TObject) private class var FName: string; class procedure SetName(const Value: string); static; {静态类方法又多了一个 static 指示字} published class property Name: string read FName write SetName; end;
静态类方法相比类方法
1.静态方法多了一个关键字,就是在定义的末尾加上了Static.
2.静态方法没有隐藏的self参数,可以被当作callBack函数传给Windows API函数.
静态方法的调用约定:
只要记住Windows API参数调用是从右往左,D的调用方式跟他是想反的.
凡是在D里面调用Windows API函数的,肯定要加 static;stdcall ; 进行转化
下面这一段转自https://blog.csdn.net/aaa000830/article/details/79986275
-----------------------------------------------------------------------------------------------------------------------
注: 使用错误,或者在该加的地方没有加,可能会出现"privileged instruction"错误,或者地址访问错误。
常见的调用惯例有register, pascal, cdecl, stdcall, safecall。函数的调用管理决定了参数如何传递给子过程,并从堆栈中退出,以及寄存器在参数传递中的使用,错误和异常的处理。Delphi中默认的调用惯例是register。
1) register和pascal:参数从左向右传递,也就是说最左边的参数最先求值并传入,最右边的参数最后求值和传入。cdecl,stdcall和safecall则按从右向左方向。
2) 对于除cdecl之外的所有调用惯例,函数/过程在返回的时候要把堆栈中的参数退栈。对cdecl惯例,调用者在被调用的过程返回后执行参数退栈操作
3) register调用惯例最多能用3个CPU寄存器来传递参数,而其它调用惯例只能通过堆栈来传递参数
4) safecall调用惯例实现了异常的防火墙。在Windows上实现了跨进程的COM错误通知机制。
5) register调用效率最高,因为它避免了堆栈的创建。Delphi中published属性必须是register。
6) cdecl常用于调用C/C++编写的共享库中的函数;但是,如果要调用外部代码,那么一般要用stdcall和safecall
7) 在Windows上,系统的API都是stdcall和safecall;在其它操作系统上通常用cdecl(注意:stdcall比cdecl效率要高)
8) 在dual-interface(双接口)方法中必须用safecall惯例。
9) pascal惯例是为了向后兼容;near/far/export用于16位Window编程中的函数调用,在32位的应用程序中不发挥作用,仅仅是为了向后兼容。
10)即使静态方法只有一个参数,也要标明调用约定,因为不同的方式,汇编会最后给你弄不同的返回方式.
下表进行了总结:
Calling conventions Parameter order Clean-up Passes parameters in registers?
register Left-to-right Routine Yes
pascal Left-to-right Routine No
cdecl Right-to-left Caller No
stdcall Right-to-left Routine No
safecall Right-to-left Routine No
--------------------转载结束-----------------------------------------
静态类的属性(class property):
Ttest = class private class var FMyName: string; public class function GetMyName: string; static; class procedure SetMyName(Value: string); static; //下面这两种方法是等价的,二选一 class property MyName: string read GetMyName write SetMyName; class property DirectName: string read FMyName write FMyName; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); begin Ttest.SetMyName('张飞'); ShowMessage(Ttest.FMyName); //show 张飞 ShowMessage(Ttest.GetMyName); //show 张飞 end; { Ttest } class function Ttest.GetMyName: string; begin Result := fmyname; end; class procedure Ttest.SetMyName(Value: string); begin fmyname := Value; end;
这是说一下class var 与 var的区别.var是用来与class var做区分的,算了,我文笔不太好,还是用代码来说明吧
还有一种方法,就是把calss var变量写在限制区域的最后面,这样就不会影响到其他字段了,比如
类的构建函数与解析函数.
构建/解析函数优先级大于单元文件初始区(iniialization)与单元结束区(finalization).
我们可以通过constructor和destructor关键字创建多个构建/解析函数,却不能使用class创建第二个类构建/类解析函数
单例模式 (Singleton Pattern)
Singleton Pattern模式的设计意图是:保证一个类仅有一个实例,并提供一个访问他的全局访问点。单例模式在应用开发中比较常见,如 Application 或 Logger。在面试考试中出现率很高,别看书上一面带过,实际上还是有点复杂的,可以在B站上面搜索看看.
在 Delphi 的以前版本中,实现单例模式比较“另类”,自从 Delphi 后期加入一些新的语法元素后,单例模式的实现显得更为标准,和 C++、Java 中的实现方法几乎一致,最主要原因就是 Delphi 加入了类变量的支持,关键字为”class var”。
Delphi 同时支持类属性,可以让单例的访问更为友好;同时需要注意,在实现单例模式时,一定不要忘记把类本身的 Create 构建函数隐藏,否则的话,单例的实现将没有意义。
下面的代码实现了单例模式的 TLogger:
type TLogger = class ( TObject ) private class var FInstance: TLogger; class function GetInstance: TLogger; static; protected constructor Create; public procedure Login; procedure Logout; class procedure ReleaseInstance; class property Instance: TLogger read GetInstance; end;
Delphi Class of 类引用
以下内容转自Delphi Class of 类引用 - 走看看
Delphi Class of 类引用也就是类的类型,也可说是指向类的指针
Type TControlCls = Class of TControl; function CreateComponent(ControlCls: TControlCls): TControl; begin result:=ControlCls.Create(Form1); ... end;
前者要求传入一个类, 而后者要求传入一个对象(类的实例)
type MyClassRef=calss of CMyClass //表示MyClassRef为指向CMyClass或其父类的指针
类的引用就像指向类的指针一样
类引用就是类的类型,可以声明一个类引用变量赋给它一个类,可以通过这个变量创建对象的实例。
类的类,当你不确定调用的类模型时候用到类的类。也可以说是类指针~
System单元的TObject有如下方法:
function ClassType: TClass;
它就是获取对象的类类型,它的返回类型TClass就是class of TObject。
因为所有类都派生自TObject,所以所有对象都可以调用ClassType。比如:
procedure TForm1.Button1Click(Sender: TObject); var S: TStringList; C: TClass; begin S := TStringList.Create; C := S.ClassType; ShowMessage(C.ClassName);//对话框会显示出来TStringList,相当于TStringList.ClassName S.Free end;
而C.ClassName调用的是TObject的类方法ClassName,
原型:class function ClassName: ShortString;
就是说不需要用实例化的对象去调用,直接用类去调用就行了;不过用对象调用也是可以的,因为对象空间也保存了类的VMT地址。
用类调用形如:TStringList.ClassName
用对象调用形如:S.ClassName
一个有意思的语法 :
TTest = class //主类,正常定义 private Fnumber: Integer; FText: string; public FText2: string; procedure increase; constructor Create; end; TTestHelp = class helper for TTest //类助手,关键字为class Helper for后面跟主类名 private procedure Show; //可以扩充主类的属性和方法 public procedure increase; //将会覆盖掉原有的属性 function test(): string; //可以扩充主类的属性和方法 constructor Create(); //将会覆盖掉原有的方法 end; var Form1: TForm1; implementation {$R *.dfm} { ttest } constructor TTest.Create(); begin inherited; Fnumber := 100; FText := '123'; FText2 := 'TTest.Create'; Form1.Memo1.Lines.add(FText2); end; procedure TTest.increase; begin Inc(Fnumber); Form1.Memo1.Lines.add(Fnumber.ToString); end; { TTestHelp } constructor TTestHelp.Create(); begin inherited; Fnumber := 100; FText := 'abc'; FText2 := 'TTestHelp.Create'; Form1.Memo1.Lines.add(FText2); end; procedure TTestHelp.increase; begin Dec(Fnumber); Form1.Memo1.Lines.add(' TTestHelp.increase:' + Fnumber.ToString); end; procedure TTestHelp.Show; begin Form1.Memo1.Lines.add('TTestHelp.Show'); end; function TTestHelp.test(): string; begin Result := 'TTestHelp.test'; Form1.Memo1.Lines.add(Result); end; //调用 procedure TForm1.Button1Click(Sender: TObject); var test: TTest; begin test := TTest.Create; test.show; test.test(); end;
输出如下:
概括一下类助手的特点:
1.主类必须是已存在的类,定义时也要注意先后顺序,先定义主类,再定义类助手,否则报错,这一点应该不难理解吧.
2.类助手可以扩充主类的方法和属性.扩充的方法和属性可以是类方法,类变量与类属性,甚至还可以是虚拟方法!
3.字段只能定义在主类中,类助手中不允许定义字段,否则会提报错: E2599 字段定义不允许在帮助器类型中
4.主类和类助手是一体的,相当于把类助手的所有方法和属性都并到了主类里,同名即覆盖.
包括构建函数和解析函数.也包括方法和属性的访问权限.比如主类里面是private,类助手是可以把它设置成public覆盖过去的.
打个比方法,就像我们平时,把文件从A位置复制到B位置,然后中途弹出一个提示框,说存在同名文件,问你要不要覆盖,然后你选择了覆盖.
5.引用外部资源的类助手文件,必须在USES区域中加入.
6.编译器只承认最后一个类助手.注意!这里不是覆盖啦!是忽视最后一个类助手之前的所有类助手!!只取最后一个类助手进行覆盖!!然后你是不是想套娃?比如给类助手再配个助手?
TTestHelp2 = class helper for TTestHelp //ERROR : E2021 需要class类型
天真了吧!解决的方案之一是 真·套 "娃" ,这二是取别名(下面会讲)...重新定义一个类,让它继承原来的主类TTest,然后再给TTest2定义一个类助手,书中建议不要这么用,因为这会让类结构复杂化,源码难于解读.
TTest2=class (TTest ) //something end; TTestHelp2 = class helper for TTest2 public procedure aaa; end;
7.类助手的意义在于弥补主类功能上的不足,而不是覆盖.覆盖会使人解读混乱.希望读者能明白这一点.用下面这个例子帮助大家理解:
我们通常取ListBox当前选中的值,都是
ListBox1.Items [ListBox1.ItemIndex]
现在我觉得它使用起来太麻烦了,我需要按照我自己的方法来实现:
type TListboxHelper = class helper for TListBox //扩展 function ItemIndexValue: string; end; function TListboxHelper.ItemIndexValue: string; begin Result := ''; if ItemIndex >= 0 then Result := Items [ItemIndex]; end; //调用 str:= ListBox1.ItemIndexValue;
record helper for 与 class helper for相似,它是针对记录类型的.暂且叫它记录助手吧.
SysUils里的一些记录助手:
TGUIDHelper = record helper for TGUID TStringHelper = record helper for string TSingleHelper = record helper for Single TDoubleHelper = record helper for Double TExtendedHelper = record helper for Extended TByteHelper = record helper for Byte TShortIntHelper = record helper for ShortInt TWordHelper = record helper for Word TSmallIntHelper = record helper for SmallInt TCardinalHelper = record helper for Cardinal TIntegerHelper = record helper for Integer TUInt64Helper = record helper for UInt64 TInt64Helper = record helper for Int64 TNativeUIntHelper = record helper for NativeUInt TNativeIntHelper = record helper for NativeInt TBooleanHelper = record helper for Boolean TByteBoolHelper = record helper for ByteBool TWordBoolHelper = record helper for WordBool TLongBoolHelper = record helper for LongBool TCurrencyHelper = record helper for Currency // added in Delphi 11
类型助手:
我们上面说过,编译器对于类助手与记录助手,只会识别最后一个助手,除了真套娃,还有其他方法吗?书上说:定义一个类型别名.类型别名会被编译器当成一个全新的类型,所以它可以拥有它自己的类助手.打个比方:银行规定一个人只能开一个账户(我们抛开银行开户的逻辑不谈),张三开了一个账户之后 ,又取了个小名叫张飞又开了一个账户,这样张三就有了两个账户了(助手).当张三想用第一个账户时,他就是张三,当他想用第二的账户时,他只需要转换一下身份,变成张飞就可以了.
type MyInt = type Integer; //要取别名,因为系统中已存在 TIntegerHelper = record helper for Integer TMyIntHelper = record helper for MyInt function AsString: string; end; function MyIntHelper.AsString: string; begin Result := IntToStr (self); end; //调用 procedure TForm1.Button1Click(Sender: TObject); var MI: MyInt; begin MI := 10; Show (MI.AsString); // Show (MI.toString); // this doesn't work Show (Integer(MI).ToString) end;
delphi D11编程语言手册 学习笔记(P344-392) 接口/类操作 - 一曲轻扬 - 博客园 (cnblogs.com)