# 五、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) 书籍。**