# 五、Delphi 面向对象编程 Delphi 完全支持面向对象编程范式(OOP)。 当您开始编写业务逻辑时,您可以选择遵循一种命令式的过程式编码方式,从而声明全局类型、变量和例程,或者利用对面向对象的支持,通过创建具有字段、方法和属性、接口和任何面向对象语言的其他典型元素的类来建模您的业务逻辑。你甚至可以混合搭配两种风格,利用每种风格的最佳部分。 本章展示了如何使用对象帕斯卡进行面向对象编程,但它假设您已经熟悉了它的概念。 面向对象程序设计的基本元素是类和对象。A `class`定义了一个包含数据和逻辑的结构:数据由字段表示,逻辑包含在方法内部。 每个类都可以看作是一个模板,它定义了属于它的任何`object`的状态和行为。 这里是一个单元的完整代码,包含一个样本类声明来表示一个抽象的二维形状。 代码清单 26:完整的样本类声明 ```delphi unit Shape2D; interface uses   System.Classes, Vcl.Graphics; type { TShape2D }   TShape2D = class abstract   private     FHeight: Integer;     FWidth: Integer;     procedure SetHeight(const Value: Integer);     procedure SetWidth(const Value: Integer);   protected     function GetArea: Integer; virtual; abstract;   public     constructor Create;     procedure Paint(ACanvas: TCanvas); virtual;     property Area: Integer read GetArea;     property Height: Integer read FHeight write SetHeight;     property Width: Integer read FWidth write SetWidth;   end; implementation { TShape2D } constructor TShape2D.Create; begin   inherited Create;   FHeight := 10;   FWidth := 10; end; procedure TShape2D.Paint(ACanvas: TCanvas); begin   ACanvas.Brush.Color := clWhite;   ACanvas.FillRect(ACanvas.ClipRect); end; procedure TShape2D.SetHeight(const Value: Integer); begin   if Value < 0 then     Exit;   FHeight := Value; end; procedure TShape2D.SetWidth(const Value: Integer); begin   if Value < 0 then     Exit;   FWidth := Value; end; end. ``` 我们将使用这个示例来探索一些对象帕斯卡面向对象的特性。 像任何其他帕斯卡类型一样,类可以在一个单元的接口部分声明,以使它们对其他单元可用。若要在声明类的同一单元中使用该类,请将其放在实现部分。 | ![](img/00010.jpeg) | 提示:您不必手动编写类实现的每一行。一旦在接口部分完成了类的声明,将编辑器光标放在里面,然后按 CTRL+SHIFT+C 自动创建它的实现。 | 如果您没有指定祖先类,Delphi 会假设您正在扩展到 Object,它是所有类的母类,并为内存分配和泛型对象管理提供基本支持。 这是我们`TShape2D`类的后代样本。 代码清单 28:示例子类声明 ```delphi unit ShapeRectangle; interface uses   Shape2D, Vcl.Graphics; type { TShapeRectangle }   TShapeRectangle = class (TShape2D)   protected     function GetArea: Integer; override;   public     procedure Paint(ACanvas: TCanvas); override;   end; implementation { TShapeRectangle } procedure TShapeRectangle.Paint(ACanvas: TCanvas); begin   inherited Paint(ACanvas);   ACanvas.Rectangle(ACanvas.ClipRect); end; function TShapeRectangle.GetArea: Integer; begin   Result := Height * Width; end; end. ``` TShapeRectangle 类是 TShape2D 的后代:这意味着该类继承了祖先类的所有内容,比如 Height 和 Width 属性,并且具有通过添加更多成员或更改逻辑来扩展它的能力。但是,这仅适用于基类允许的情况,并通过特定的扩展点,如虚拟(可重写)方法。 Delphi 不支持多重继承;不能从多个类继承。 类内部成员的可见性不依赖于声明或实现它们的单元部分(接口或实现),但是有特定的关键字来分配可见性。 表 9:可见性说明符 | 关键字 | 描述 | | 私人的 | 成员仅对包含它们的类和声明该类的单元可见。 | | 保护 | 成员对包含它们的类及其所有后代都是可见的,包括声明该类的单元。 | | 公众的 | 成员对包含它们的类、声明它的单元和其他单元都是可见的。 | | 出版者 | 这与公共说明符具有相同的效果,但是编译器会为成员生成额外的类型信息,如果应用于属性,它们将在对象检查器中可用。 | Delphi 还支持两个额外的说明符,严格私有和严格保护。如果应用于成员,这意味着成员仅对类本身可见,而不在声明该类的单元内。 TShape2D 类有一些字段: 代码清单 29:实例字段 ```delphi   TShape2D = class abstract   private     FHeight: Integer;     FWidth: Integer;     // ...   end; ``` 这些字段在私有部分下声明,因此类外的客户端代码无法访问它们。这意味着更改它们的值必须通过调用像 SetHeight()和 SetWidth()这样的方法来完成,或者通过更改链接到字段的属性值来完成。 | ![](img/00008.gif) | 注意:您在这里看到一些编码约定在起作用:所有类型通常以字母“T”开头,字段以字母“f”开头。如果您遵循这些广泛使用的约定,您将能够轻松阅读其他人编写的代码。 | 创建此类的实例时,字段会自动初始化为默认值,因此任何整数字段都会变成 0(零),字符串为空,布尔值为假,等等。 如果要为任何字段设置初始值,必须向类中添加一个构造函数。 这是从我们的示例代码中提取的 TShape2D 方法声明的摘录。 代码清单 30:方法声明 ```delphi   TShape2D = class abstract   private     procedure SetHeight(const Value: Integer);     procedure SetWidth(const Value: Integer);   protected     function GetArea: Integer; virtual; abstract;   public     procedure Paint(ACanvas: TCanvas); virtual;   end; ``` 方法是属于一个类的函数和过程,通常会改变对象的状态,或者让您以受控的方式访问对象字段。 这里是 SetWidth()方法的主体,可以在同一个单元的实现部分找到。 代码清单 31:方法实现 ```delphi procedure TShape2D.SetWidth(const Value: Integer); begin   if Value < 0 then     Exit;   FWidth := Value; end; ``` 非静态方法的每个实例都有一个名为 Self 的隐式参数,该参数是对调用该方法的当前实例的引用。 我们的示例包括一个用虚拟指令标记的虚拟方法。 代码清单 32:虚拟方法声明 ```delphi   TShape2D = class abstract   protected     procedure Paint(ACanvas: TCanvas); virtual;   end; ``` 从 TShape2D 继承的子类可以使用重写指令重写该方法。 代码清单 33:重写方法声明 ```delphi   TShapeRectangle = class (TShape2D)   protected     procedure Paint(ACanvas: TCanvas); override;   end; ``` 将调用子类中的实现来代替继承的方法。 代码清单 33:被覆盖的方法实现 ```delphi procedure TShapeRectangle.Paint(ACanvas: TCanvas); begin   inherited Paint(ACanvas);   ACanvas.Rectangle(ACanvas.ClipRect); end; ``` 如果需要,后代方法能够使用继承的关键字调用继承的实现。基类中的`Paint()`方法用颜色填充背景;子类继承并调用实现,添加指令在屏幕上绘制特定形状。 属性是创建类似字段的标识符的方法,用于从对象读取和写入值,同时保护存储实际值的字段,或者使用方法返回或接受值。 代码清单 34:属性 ```delphi   TShape2D = class abstract   private     property Area: Integer read GetArea;     property Height: Integer read FHeight write SetHeight;     property Width: Integer read FWidth write SetWidth;   end; ``` 在上面的示例中,当您读取 Height 属性时,Delphi 从私有的 FH h8 字段中获取值,该字段在 read 子句之后指定;设置 Height 的值时,Delphi 调用 write 子句后指定的 SetHeight 方法,并传递新值。 | ![](img/00008.gif) | 注意:读写访问器都是可选的:您可以创建只读和只写属性。 | 属性提供了字段的优点和易于访问的特性,但对客户端代码传入的值保留了一层保护。 构造函数是特殊的静态方法,负责初始化类的新实例。使用构造函数关键字声明它们。 这是从我们的示例类中提取的示例构造函数声明。 代码清单 35:构造函数声明 ```delphi   TShape2D = class abstract   public     constructor Create;   end; ``` 让我们检查构造函数方法的实现。 代码清单 36:构造函数实现 ```delphi constructor TShape2D.Create; begin   inherited Create;   FHeight := 10;   FWidth := 10; end; ``` 继承的关键字调用从基类继承的构造函数。您应该将此语句添加到几乎每个构造函数体中,以确保所有继承的字段都被正确初始化。 有些类还定义了析构函数方法。析构函数负责释放类分配的内存、拥有的对象,并释放对象创建的所有资源。它们总是用 override 指令标记,因此它们能够调用继承的析构函数实现并释放基类分配的资源。 当您考虑类层次结构时,有时您无法实现某些方法。 考虑我们的祖先`TShape2D`样本类:你将如何实现`GetArea()`方法?您可以添加一个返回 0(零)的虚拟方法,但是零值有特定的含义。明确的答案是在`TShape2D`类中实现`GetArea()`方法没有意义,但是它必须存在,因为逻辑上每个形状都有它。所以,你可以把它抽象化。 代码清单 40:抽象类 ```delphi TShape = class abstract protected   function GetArea: Integer; virtual; abstract; end; ``` | ![](img/00008.gif) | 注意:每个有抽象方法的类都应该标记为抽象本身。 | 抽象方法缺乏实现;它必须是一个虚拟方法,并且子类必须覆盖它。 代码清单 41:实现一个抽象类 ```delphi // Interface TShapeRectangle = class protected   function GetArea: Integer; override; end; // Implementation function TShapeRectangle.GetArea: Integer; begin   Result := Height * Width; end; ``` 显然你不能在基类中调用继承的方法,因为它是抽象的,会导致`AbstractError`。 | ![](img/00010.jpeg) | 提示:当您创建抽象类的实例时,编译器会发出警告。您不应该这样做来避免遇到对抽象方法的调用。 | 对象 Pascal 也支持静态成员。实例构造函数和析构函数本质上是静态的,但是您可以向类中添加静态字段、成员和属性。 看看系统中`TThread`类声明的摘录。班级单元。 代码清单 39:静态成员 ```delphi   TThread = class   private type     PSynchronizeRecord = ^TSynchronizeRecord;     TSynchronizeRecord = record       FThread: TObject;       FMethod: TThreadMethod;       FProcedure: TThreadProcedure;       FSynchronizeException: TObject;     end;   private class var     FProcessorCount: Integer;     class constructor Create;     class destructor Destroy;     class function GetCurrentThread: TThread; static;   end; ``` ProcessorCount 变量是一个私有的静态成员,由于类 var 关键字,它的值被`TThread`类的所有实例共享。 在这里,您还可以看到一个类构造函数和类析构函数的示例,这两个方法旨在初始化(和终结)静态字段值,其中类函数是一个静态方法。 我们在课堂上花了很多时间,但是我们如何创建具体的实例呢? 下面是一个创建和使用`TMemoryStream`实例的代码示例。 代码清单 42:创建实例 ```delphi var   Stream: TMemoryStream; begin   Stream := TMemoryStream.Create;   try     Stream.LoadFromFile('MyData.bin');   finally     Stream.Free;   end; end; ``` 使用 TClassName 形式调用的构造函数。Create 分配必要的内存来保存对象数据,并返回对对象结构的引用。 保存对所创建对象的引用的变量必须是同一类型或祖先类型。 | ![](img/00010.jpeg) | 提示:如果要使引用类型变量指向无对象,可以将其设置为 nil,这是一个代表空引用值的关键字,就像 C# 中的 null 一样。 | 调用方法`LoadFromFile`引入`TMemoryStream`。这将缓冲指定文件中包含的所有数据(路径被指定为方法的第一个参数)。 然后调用`Free`方法销毁该对象。 | ![](img/00008.gif) | 注意:为什么不调用 Destroy()方法而不是 Free()?毕竟,Destroy()是“官方”析构函数。您必须调用 Free(),因为它是非虚拟方法,并且具有静态地址,因此无论对象类型是什么,都可以安全地调用它。Free()方法还会在调用 Destroy()之前检查引用是否未赋值为零。 | 你可能会问自己“尝试……最终构造”代表什么。 Delphi 没有垃圾收集器;您负责释放您创建的每个对象。try…finally 块确保资源被释放,即使在其使用阶段出现错误。否则,会发生内存泄漏。 您可以使用 is 运算符检查对象是否属于某个类。如果得到肯定的结果,可以将对象强制转换为指定的类型,并安全地访问其成员。 代码清单 43:使用 is 关键字 ```delphi if SomeObject is TSomeClass then   TSomeClass(SomeObject).SomeMethod(); ``` 您可以使用 as 运算符同时进行类型检查和转换。 代码清单 44:使用 as 关键字 ```delphi (SomeObject as TSomeClass).SomeMethod(); ``` 如果`SomeObject`不是`TSomeClass`类型或者它的后代,你会得到一个例外。 | ![](img/00008.gif) | 注意:不要同时使用 is 和作为运算符,因为它们执行相同的类型检查操作,这在计算方面有一定的成本。如果运算符检查成功,请始终使用直接转换。但是,在没有检查之前,不要做直接转换。 | 接口是您可以放在实现之间的较薄的层,并且是实现代码高度解耦的基本工具。 接口是你可以在 Delphi 中创建的最抽象的类。它只有抽象方法,没有实现。 代码清单 45:接口 ```delphi ICanPaint = interface   ['{D3C86756-DEB7-4BF3-AA02-0A51DBC08904}']   procedure Paint; end; ``` 任何类只能有一个祖先,但是它可以实现任意数量的接口。 | ![](img/00008.gif) | 注意:每个接口声明必须有一个关联的 GUID。这个需求是在 COM 支持下引入的,但是也有很多 RTL 的部分涉及到使用 GUIDs 的接口。这可能看起来很烦人,但是将 GUID 添加到界面上确实又快又简单:只需在代码编辑器中按 CTRL+SHIFT+G 组合键。 | 而类有`TObject`作为共同的祖先,接口都有`IInterface`。 代码清单 46:基本接口声明 ```delphi IInterface = interface   ['{00000000-0000-0000-C000-000000000046}']   function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;   function _AddRef: Integer; stdcall;   function _Release: Integer; stdcall; end; ``` `IInterface`方法增加了对参考计数的支持。实现接口的类应该继承自`TInterfacedObject`,因为这个类提供了负责引用计数的`IInterface`方法的实现。 代码清单 47:接口实现 ```delphi TShape = class (TInterfacedObject, ICanPaint)   procedure Paint; end; ``` Delphi 包含一个独特而强大的特性,称为类引用类型(也称为“元类”)。 这里有一个示例声明来阐明这个概念。 代码清单 48:类引用类型声明 ```delphi TShapeClass = class of TShape; ``` 简短而简单,但这意味着什么?您可以将此类型用于变量、字段、属性和参数,并且可以将`TShape`类或其后代之一作为值传递。 假设您想要声明一个方法,该方法能够通过调用其构造函数来创建任何类型的`TShape`对象。您可以使用类引用类型将 shape 类作为参数传递,其实现类似于代码清单 49 所示。 代码清单 49:类引用类型用法 ```delphi function TShapeFactory.CreateShape(AShapeClass: TShapeClass): TShape; begin   Result := AShapeClass.Create; end; ``` 元类允许您将类作为参数传递。 代码清单 50:类引用类型值 ```delphi MyRectangle := ShapeFactory.CreateShape(TRectangleShape); ``` 在这一章中,我们已经看到了面向对象编程的基础知识。由于这是一本简洁的*系列书籍,我们没有足够的空间来探索 Delphi 中类实现的每一个小细节。* *如果你真的想深入研究 Object Pascal 语言的所有可能性,并应用最先进的编程技术,比如 GoF 设计模式、控制反转、依赖注入等等,我推荐尼克·霍奇斯的《Delphi 中的[](http://codingindelphi.com)*[*【Delphi 中的更多编码】*](http://morecodingindelphi.com) 书籍。**