delphi D11编程语言手册 学习笔记(P225-P343) OOP(面向对象)

这本书可以在 Delphi研习社②群 256456744 的群文件里找到. 书名: Delphi 11 Alexandria Edition.pdf
●P225-254
类是抽象的,变量是类的具现.
类在定义时,只是定义了它有这些属性和方法,不会把实际代码写在里面,但是必须要在同一个单元里完成(实际代码可写在dll里面然后链接进来,但不推荐),这使得类的源码更加精简与直观.
请注意类的定义顺序:
TBtype = class B: TAtype;//Error: [dcc32 错误] Unit1.pas(28): E2003 未声明的标识符:'TAtype' end; TAtype = class str: string; //Error: [dcc32 错误] Unit1.pas(31): E2004 重复声明标识符:'TAtype' end;
上面的代码,只要把TBtype与TAtype定义的顺序调整一下就没问题了
在写类的方法与过程代码的时候,需要声明这个方法或者过程是归属于哪个类的,就跟控件事件前面要加窗体名称一样,因为窗体也是一个类.
procedure TBtype.SetValue(M D, Y: Integer); Begin end; function TAtype.LeapYear: Boolean; Begin end;
1.重载构造函数overload关键字
eg:
constructor Create(Name:string;Sex:string;Year:integer;Tall:integer;Weight:integer);overload;
constructor Create(Name:string;Sex:string);overload;
ps: 重载构造函数,注意一定要使用关键字:overload
ps:重载就是对参数进行类型匹配,然后套用到合适的那一个函数.
2.自定义类中是否需要显式定义构造函数:
所有类默认继承自TObject,即使没有声明class(TObject),Create实际是通知编译器为其分配一块堆内存.在自定义类中,即使没有显式的定义构造函数,程序默认调用的是TObject的构造函数.
3.TObject.Free的真正作用:实际上只是为"堆内存块"解锁,使得其他程序可以使用该块堆内存,而引用并没有被 重置为nil,因此,经常Free之后,仍可以正确访问到对象的成员变量(解锁后,该块内存没有被其他程序使用)
4.自定义的类,什么时候需要专门定义析构函数?
自定义的类中,含有类成员.由于Free操作只是对当前对象的"堆内存块"进行了解锁,而类成员指向的另一块"堆内存块" 并没有被解锁,如果不在析构函数中对其解锁,则会造成内存泄漏.
5.不需要程序员显示调用Free的自定义类?
对于从Tcomponent继承下来的对象,在Create的时候可以指定一个所有者。如果一个对象在创建时指定了所有者,那么该对象的生存期将由所有者进行管理。所有者在析构时,会同时析构它所拥有的全部对象。
6.自定义类中,如何定义析构函数?
Delphi中所有类都继承自TObject ,Create默认不是虚方法 ,Destroy默认是虚方法.被覆盖的函数必须是虚(virtual)的,或者是动态(dynamic)的,因此自定义类的Destroy方法可以被覆盖. 而VCL中组件继承的大多Create都被声明成虚方法了。
还有一点要注意,通常我们不会直接调用 Destroy 来释放对象,而是调用 TObject.Free,它会在释放对象之前检查对象引用是否为 nil。
procedure DecodeTime(const DateTime: TDateTime; var Hour, Min, Sec, MSec: Word); //分解时间到这三个参数变量
procedure DecodeDate(const DateTime: TDateTime; var Year, Month, Day: Word); //分解日期到这三个参数变量
function EncodeTime(Hour, Min, Sec, MSec: Word): TDateTime; //返回一个合成时间
function EncodeDate(Year, Month, Day: Word): TDateTime; //返回一个合成日期
7.constructor Create构造函数的名称是可以不必是Create并且还可以有N多个版本.但是你在实例化类时,必须使用相同的名称.比如声明时是constructor LikeThis,具现类是就要这么写: TXXX.LikeThis而不是TXXX.Create
内部类,就是类里面套类,这种类一般都是要写在私有域里面的.
在 Object Pascal 当中唯一使用到内部类的用途是当我们在类别的私有区里面宣告一个类别数据字段时,不想把这个类别加到全局的命名空间,也不让它被全局命名空间看见,此时,内部类就派上用场了。
type TOne = class private FSomeData: Integer; public // Nested constant const Foo = 12; // Nested type type TInside = class public procedure InsideHello; private FMsg: string; end; public procedure Hello; end; procedure TOne.Hello; var Ins: TInside; begin Ins := TInside.Create; Ins.Msg := 'Hi'; Ins.InsideHello; Show('Constant is ' + IntToStr(Foo)); Ins.Free; end; procedure TOne.TInside.InsideHello; begin FMsg := 'New msg'; Show('Internal call'); if not Assigned(InsIns) then InsIns := TInsideInside.Create; InsIns.Two; end; procedure TOne.TInside.TInsideInside.Two; begin Show('This is a method of a nested/nested class'); end;
●P255- 281 继承
如果撰写类的主要原因是为了封装,那么使用继承的主要理由就是为了弹性 .在继续学习继承之前,各位一定要先学会如何追溯源码.在代码里面,按住ctrl再点击关键字,就会跳到这个类或者方法的源码位置,能不能看懂源码就是另外一门学问了.
为什么要继承?打个比方,你爹有个车子,你没有,但你是不是可以拿开啊?不然难道你还打算自己造一台? 只要是你爹允许的(public),你都可以拿来用! 同样的,如果你同时还有好几个干爹,哼哼,不得了不得了,直接起飞了有没有?
入侵类的保护区(Protected Hack)
因为TTest继承自TAtype,两个共享完全相同内存架构的类别,我们可以强迫编译器把一个类别的对象当做另一个类别的,通常可以透过不安全的型别转换来达成.
"狗一定是动物,但动物不一定是狗"
type TAnimal = class public constructor Create; function GetKind: string; private FKind: string; end; TDog = class (TAnimal) //继承,衍生子类 public constructor Create; end; //下面这种用法是合法的
var MyAnimal1, MyAnimal2: TAnimal; begin MyAnimal1 := TAnimal.Create; MyAnimal2 := TDog.Create; //但是反过来就不行啦,父级可以指派给子类,但子类的不能指派给父级 MyAnimal := MyDog; // This is OK MyDog := MyAnimal; // This is an error!!!
延迟绑定和多型.
我问群里什么是延迟绑定(动态绑定),然后无为大佬给我了一张图.
然后说到多态,大佬又给了我个链接: 剖析Delphi中的多态
最后平原君大佬说 :
inherited是子类中调用父级同名函数的关键字,当然也可以调用不同名的函数。这不是必须的,但是当组件的初始化在TComponent 类别层次已经完成 时,就必须要调用父级函数了.这是非常重要的,因为组件的 Create 是虚拟方法。跟所有的类别相似,解构函式 Destroy 也是个虚拟方法,我们必须记得透过 inherited 来调用低级类的同名方法。
constructor TMyComponent.Create (Owner: TComponent); begin inherited Create (Owner); // specific code... end;
建议一定要调用父级的建构函式,要养成这个好习惯。
如果在定义函数或者过程的结尾没有标明方法类型,则此函数或者过程为缺省的静态方法(static)。速度最快,但不支持重载和覆盖。
虚拟方法和动态方法类似,都是为了重载与覆盖做准备的,区别在于调试系统不一样罢了。虚拟快,动态省内存。
message 用来定义讯息处理的方法,这个方法必须是带有一个适当型别的var 参数的程序。Message 这个关键词之后必须跟着一个 Windows 讯息代号,也就是这个程序要处理的讯息代号。
type TForm1 = class(TForm) ... procedure WmUser (var Msg: TMessage); message wm_User; //程序的名称跟实际的参数类型我们可以自己决定,只要实际的数据结构跟Windows 讯息的结构相同即可。 end;
delphi中有abstract的就叫纯虚方法,就是抽象类。而abstract必须定义在 virtual、dynamic 之后。 没有abstract定义的virtual、dynamic的方法不一定是纯虚的,因为他们可以实现自己的东西。 接口的方法就是纯虚方法(默认的)。
TParent = class protected function MyFun(i: Integer): Integer; virtual; abstract; //抽象方法(纯虚方法),只有定义没有实现,一个类包含一个即成抽象类,抽象类不能直接创建对象。 end;
我们不能调用子类别当中才有提供的方法,父级类别必须至少也得宣告过要调用的方法才行-也就是抽象方法的一个种类。
type TAnimal = class //包含有abstract,是一个抽象方法,所以不可以创建对象 public constructor Create; function GetKind: string; function Voice: string; virtual; abstract; //这里你按ctrl+shift+c是不会自动生成代码框架的,因它不能在这写代码,要写到子类中去。写了也会报错。 private FKind: string; end; TDog = class(TAnimal) //子类,可以创建对象 public constructor Create; function Voice: string; override; //子类里面要写实际代码了 function Eat: string; virtual; end; TCat = class(TAnimal) //子类,可以创建对象 public constructor Create; function Voice: string; override; //子类里面要写实际代码了 function Eat: string; virtual; end;
封闭类(Sealed Classes)与最终方法(Final Methods),这两种方法都很少用。
封闭类的关键字是 Sealed ,意思就是绝育了,不允许再被衍生出其他子类了。
type TDeriv1 = class sealed (TBase) procedure A; override; end;
最终方法的关键字是 Final,意思是不允许任何子类再覆盖此方法了(但是可以重载哈)。
type TDeriv2 = class (TBase) procedure A; override; final; end;
安全的类型转换指令:
as 就是把某个类型对象转换为某个指定类型,这样方便使用指定类型中的一些成员.
(FMyAnimal as TDog).Eat;
is 就是判断某个对象是不是某个类型,可以筛选出我们需要的对象.
if FMyAnimal is TDog then begin MyDog := TDog (FMyAnimal); Text := MyDog.Eat; end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if Sender is TButton then
...
end
类型转换运算符号在效能上会有明显的负面影响,因为它得把整个类别的族谱跑一次,才能确实判别类型转换是否合法.
可视化窗体继承
任何可视化应用程序都是建立在继承这个技术.之上的 。当我们创建了一个窗口时,其实是创建了一个TForm类。
●P282- 304 异常处理
所有的异常类型都是由Excption类型衍生而来的。上面讲过,在D里面有个不成文的规定,所有的类都是T开头的。这里做一下补充: 除了异常类型都是由E开头的以外。
定义一个异常类:
type EArrayFull = class (Exception); //一般情况下定义一个异常类时,不需要往里再加其他方法了,这样就可以了。 //还有一种方法就是声明一个异常变量,可以直接使用,比如: var EMyerror: Extented; //用哪一种就看业务需求了。
以下是异常类型列表:
在整个Object Pascal的异常处理机制当中,是构建在五个关键字上的:
1.try 注册一个源码保护区的开始
2. except 注册这个源码保护区的结束,并宣告开始进入异常处理的源码
3.on 标示每个特定的异常处理叙述句,与特定的异常进行连结,每个 on的叙述句语法都是 on 异常类型 do 叙述句
4.finally 是用来标示不论如何都要被执行的源码,即使异常发生也一样。
5. Raise 是用来触发异常的叙述句,它的参数则是一个异常类型的对象(在其他语言中,使用的语法则是 throw)
白话版:
1.try 把你觉得可能会发生错误的代码丢到这里而来。这里要注意,在try的区域内,自出错行开始,到try区域的最后一行的代码都不会被运行!
2.except 出错后,你希望程序怎么做?
3.on 反馈错误信息,except下面可以有多个on(不提倡这种用法);如果你精准的知道它的异常类型,参照上表,可以这么写:
try Result := A div B; // Error if B equals 0 Result := Result + 1; except on EDivByZero do begin Result := 0; MessageDlg (‘Divide by zero error’, mtError, [mbOK], 0); end; on E: Exception do begin Result := 0; MessageDlg (E.Message, mtError, [mbOK], 0); end; end; // End of except block
4.finally 不管try区域里的代码错没错,始终执行此区域的代码!当然,如果try里面的异常没能处理好,导致程序终止的情况下,神仙也到不了finally区域。
5.Raise 主动抛出一个可控的异常对话框!
raise还有一种特殊情况.下图中,按钮的点击事件调用了abc过程,错在abc里,但是raise没带参数(这种情况下,raise会跳出abc转到点击事件了),让系统会自动调用点击事件里的Except代码块。但是!如果调用abc的模块里面,并没有Except语句,程序就会报错。以下代码来自群友@零度
这里有个技巧啊:一般情况下,你只能取try..finally 或者 try...except中的一个,如果你即希望处理异常,又想执行finally内容,你就可以像上面一样,在try区域里面套娃。
raise是可以单独使用的。我们在写构造函数时,往往一厢情愿的认为不会出错,但谁也说不清什么情况下会中奖,所以大家想想,如果拉S拉到一半出了异常,你会不会崩溃?
type TObjectWithList = class private FStringList: TStringList; public constructor Create (Value: Integer); destructor Destroy; override; end;
constructor TObjectWithList.Create(Value: Integer); begin if Value < 0 then raise Exception.Create('Negative value not allowed'); //主动抛出异常 FStringList := TStringList.Create; FStringList.Add('one'); end;
destructor TObjectWithList.Destroy;
begin
if Assigned (FStringList) then //这里判断对象是否存在,不然会触发异常
begin
FStringList.Clear;
FStringList.Free;
end;
inherited;
end;
特别说明: 只要是用F9编译执行程序的,都会弹错,不管你有没有处理好异常机制。只有单独执行EXE程序时,才能看出效果来
delphi 捕捉全局异常错误的方法
private { Private declarations } public procedure GlobalExceptionHandler(Sender: TObject; E: Exception); { Public declarations } end;
procedure TForm1.FormCreate(Sender: TObject); begin Application.OnException := GlobalExceptionHandler; //开局绑定异常处理方法。 end; procedure TForm1.GlobalExceptionHandler(Sender: TObject; E: Exception); begin mmLog.Lines.Add(Sender.ClassName + ': ' + E.Message); end;
书上提到了随机数,所以这里插播一下它的知道点:
函数原型:function Random ( Range: Integer) :integer;
参数:Range:整数,
返回值:整数,其范围为:
0 <= Random(Range) < Range (指定Range)
0 <= Random< 1 (不带参数Range)
begin Randomize; //初始化随机数发生器 Random(N); //生成一个随机整数,范围在0-N之间,包含0; end;//
光标的设置与恢复
procedure TForm1.Button1Click(Sender: TObject); begin var CurrCur := Screen.Cursor; //保存鼠标状态,内联变量 Screen.Cursor := crHourGlass; //修改鼠标状态为【等待状态】 try try // 某些执行会花比较久的源码 Sleep(5000); except on E: Exception do raise Exception.Create('Error Message'); end; finally Screen.Cursor := CurrCur; //恢复光标状态 end; end;
下面我们来看看异常的运行顺序问题:
解译一下上面的运行顺序:
1.点击事件里调用了过程abc,所以try区域里面直接跳到了abc
2.abc里面的try区域,因为 b[0] := 10这句出错了,直接跳到except,不会再执行'hellow',所以不弹出'hellow'
3.abc里面的except里面又抛出一个异常(raise),一定要记住,finally 是D里面规定的,不管如何都是要被执行,但是 raise 的特性又决定了不会再运行后面的代码,所以必须是先执行 finally 再执行 raise 。按照这个理论,就能理解为什么是先咕咕咕,才到‘abc'了。
5.'略略略'不弹出,是因为上一步的raise。
然后我们把abc里面的raise注释掉,再看看执行顺序:
因为abc里面的错误因为没有被抛出(raise),所以是先啦啦啦,点击事件里的except无效,不会执行raise.其他就就是按顺序执行了
我们最后再把例子改成正常人能写出来的例子:
当我们正在处理一个异常时,再触发另外一个异常怎么办?
procedure TFormExceptions.MethodWithTwoNestedExceptions; begin try raise Exception.Create ('Hello'); //第一个异常 except begin try Exception.RaiseOuterException ( Exception.Create ('Another')); //第二个异常 except Exception.RaiseOuterException ( Exception.Create ('A third')); //第三个异常 end; end; end; end;
拦截异常
procedure RaisingException(P: PExceptionRecord); virtual;
这个虚拟过程会在该异常将要被触发之前被调用。在外部异常的案例中,这个过程则会在异常对象建立完成,且触发的条件已经在处理时,尽快被调用。
以下是对其覆写的一个例子:
type ECustomException = class (Exception) protected procedure RaisingException( P: PExceptionRecord); override; end;
procedure ECustomException.RaisingException(P: PExceptionRecord); begin // Log exception information FormExceptions.Show('Exception Addr: ' + IntToHex (Integer(P.ExceptionAddress), 8)); FormExceptions.show('Exception Mess: ' + Message); inherited; end;
这个函数只对函数库和组件开发人员比较有用。不用太深入了解。
●P305- 343 属性与事件
函数指针
函数指针的声明只需要参数列表;如果是函数,再加个返回值。
例如声明一个过程类型,该类型带一个通过引用传递的整型参数:
type IntProc = procedure (var Num: Integer); //IntProc是指向过程的函数指针
这个过程类型与任何参数完全相同的例程兼容,即用它声明的变量,可以指向任何此类函数,并通过其进行函数的调用。
下面是一个兼容例程:
procedure DoubleTheValue (var Value: Integer); begin Value := Value * 2; end;
函数指针能用于两种不同的目的:声明函数指针类型的变量;或者把函数指针作为参数传递给另一例程。利用上面给定的类型和过程声明,你可以写出下面的代码:
var IP: IntProc; X: Integer; begin IP := DoubleTheValue; X := 5; IP (X); end;
虽然这种调用方法比直接调用麻烦了,那么我们为什么要用这种方式呢?
(1)因为在某些情况下,调用什么样的函数需要在实际中(运行时)决定,你可以根据条件来判断,实现用同一个表达,调用不同的函数,很是灵活.
(2)利用函数指针我们可以实现委托,委托在.NEt中被发挥的淋漓尽致,但Delphi同样能实现
(3)实现回调机制
delphi中可以通过函数指针把一个函数作为参数来传递,然后在另外一个函数中调用。
1) 首先,申明函数指针类型TFunctionParameter。
type TFunctionParameter = function(const value : integer) : string;
2) 定义准备被作为参数传递的函数
function One(const value : integer) : string; begin result := IntToStr(value) ; end; function Two(const value : integer) : string; begin result := IntToStr(2 * value) ;
end;
3) 定义将要使用动态函数指针参数的函数
function DynamicFunction(f : TFunctionParameter; const value : integer) : string; begin result := f(value) ; end
4) 上面这个动态函数的使用实例
var s : string; begin s := DynamicFunction(One,2011) ; ShowMessage(s) ; //will display "2006" s := DynamicFunction(Two,2011) ; ShowMessage(s) ; // will display "4022" end;
事件指针
Type TMouseProc = procedure (X,Y:integer); //这是上面刚讲过的函数指针 TMouseEvent = procedure (X,Y:integer) of Object; //这是现在要讲的对象的函数指针,这两个玩意很像,但是意义完全不同.这里自带了一个self隐藏参数
TMouseProc只是单一的函数指针类型;
TMouseEvent是对象的函数指针,也就是对象/类的函数/方法
区别在于类方法存在一个隐藏参数self,也就是说两者形参不一样,所以不能相互转换。
说白了它说就是一个事件类的声明:
type TNotifyEvent = procedure (Sender: TObject) of object; TMyButton = class OnClick: TNotifyEvent; //所有使用了onClick事件的控件,在点击时,都会指向TNotifyEvent,执行它的代码,这是就绑定的原理.而这些都是由系统内部来完成的. end;
TForm1 = class (TForm) //窗体也是一个类 procedure Button1Click (Sender: TObject); //定义一个过程 Button1: TMyButton; end;
这种方法不常见,书上也是一笔带过,所以我这里也就不展开了
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库