7.2 对象引用模型
在某些 OOP 语言(如 C++)中,声明一个类类型的变量就会创建该类的一个实例(与 Object Pascal 中的记录差不多)。本地对象的内存来自堆栈,并在函数结束时释放。不过,在大多数情况下,你必须明确地使用指针和引用,才能更灵活地管理对象的生命周期,这就增加了很多额外的复杂性。
Object Pascal 语言则基于对象引用模型,与 Java 或 C# 完全相同。其原理是,类类型的每个变量并不保存对象的实际值及其数据(例如,存储日、月和年)。相反,它只包含一个引用或指针,用来指示存储实际对象数据的内存位置。
注解: 在我看来,采用对象引用模型是编译器团队在语言早期做出的最佳设计决策之一,当时这种模型在编程语言中并不常见(事实上,那时还没有Java,也没有C#)。
因此,在这些语言中,您需要显式创建一个对象并将其赋值给变量,因为对象不会自动初始化。换句话说,当你声明一个变量时,你并没有在内存中创建一个对象,你只是为对象的引用保留了内存位置。对象实例必须手动和显式地创建,至少对于你定义的类的对象是这样。(不过,在 Object Pascal 中,运行库会自动创建放置在窗体上的组件实例)。
在 Object Pascal 中,要创建对象的实例,我们可以调用其特殊的 Create 方法,即类本身定义的构造函数或其他自定义构造函数。以下是代码:
ADay := TDate.Create;
正如你所看到的,构造函数应用于类(类型),而不是对象(变量)。这是因为你要求类创建一个其类型的新实例,而结果是一个你通常会赋值给变量的新对象。
Create
方法从何而来呢?它是 TObject 类的构造函数,所有其他类都继承自该构造函数(下一章将讨论该主题)。不过,在类中添加自定义构造函数是很常见的,我们将在本章后面的章节中看到。
7.2.1 释放对象
在使用对象引用模型的语言中,您需要一种方法在使用对象之前创建该对象,同时还需要一种方法在不再需要该对象时释放其占用的内存。如果不对其进行清理,就会导致不再需要的对象占满内存,造成内存泄漏问题。为了解决这个问题,C# 和 Java 等基于虚拟执行环境(或虚拟机)的语言都采用了垃圾回收机制。虽然这让开发人员变得更轻松,但这种方法却受到一些复杂的性能问题的制约,而这些问题在解释 Object Pascal 时其实并不重要。因此,尽管这些问题很有趣,但我不想在此深入探讨。
在 Object Pascal 中,一般通过调用对象特殊的 Free 方法(同样是 TObject 的方法,在每个类中都可用)来释放对象的内存。在调用对象的析构函数(可能有特殊的清理代码)后,Free 会将对象从内存中删除。因此,您可以将上面的代码片段补充为:
var
ADay: TDate;
begin
// 创建
ADay := TDate.Create;
// 使用(略)
// 释放内存
ADay.Free;
end;
虽然这是标准的做法,但组件库还增加了像对象所有权这样的概念,大大减少了手动内存管理的影响,使这一问题的处理相对简单。使这个问题的处理相对简单。
注解: 稍后我们将看到,当使用接口引用对象时,编译器采用了一种自动引用计数(ARC)内存管理形式。几年来,Delphi 移动编译器中的常规类类型变量也采用了这种方式。从 10.4 版(也称为 Sydney 版)开始,内存管理模型得到了统一,所有目标平台都采用了经典的桌面 Delphi 内存管理。
需要了解的内存管理知识还有很多;但鉴于这是一个相当重要的主题,而且并不简单,我决定在这里只提供简短的介绍,并用一整章(即第 13 章)专门讨论这一主题。在这一章中,我将向你详细介绍你可以使用的不同技巧。
7.2.2 什么是"nil"?
如前所述,变量可以引用某个类的对象。但变量可能还没有初始化,或者曾经引用的对象可能已经不可用了。这时可以使用 nil。这是一个常量值,表示变量没有引用任何对象。其他编程语言使用符号 null 来表达相同的概念。
当类类型的变量没有值时,您可以通过以下方式初始化它:
ADay := nil;
要检查对象是否已赋值给变量,可以编写以下任一表达式:
if ADay <> nil then ...
if Assigned(ADay) then ...
切勿错误地将 nil 赋值给对象,从而将其从内存中删除。将对象引用设置为 nil 和释放它是两种不同的操作。因此,通常需要同时释放一个对象并将其引用设置为 nil,或者调用一个同时执行这两种操作的特殊过程,称为 FreeAndNil。同样,更多信息和一些实际演示将在以内存管理为重点的第 13 章中介绍。
7.2.3 记录与的内存模型对比
正如我前面提到的,记录与对象的主要区别之一在于它们的内存模型。记录类型的变量使用本地内存,默认情况下是以传值形式传递参数给调用函数,并且在赋值时是进行 “值复制”。而类类型变量是在动态的堆内存上分配的,按引用传递,并在赋值时进行 "引用复制 "(因此复制的是内存中同一对象的引用,而不是实际数据)。
注解: 这种不同的内存管理模型的结果是,记录缺乏继承和多态性,这是我们将在下一章中重点关注的两个特性。
private访问说明符表示类的字段和方法在声明该类的单元(源代码文件)之外不可访问。
例如,当您在堆栈上声明记录变量时,可以立即开始使用,而无需调用其构造函数(除非它们是自定义托管记录)。这意味着记录变量比常规对象在内存管理上更精简、更高效,因为它们不参与动态内存管理。这是使用记录而不是对象来处理小型和简单数据结构的关键原因。
关于记录和对象在作为参数时传递方式不同,默认情况下是复制代表记录的内存块(包括其所有数据)或对象的引用(数据不会被复制)。当然,您可以使用var或const修饰的记录类型参数来改变传递记录类型参数的默认行为,从而避免任何复制。