10.2 published访问说明符
除了 public、protected 和 private 访问指令(以及不太常用的strict private 和strict protected)之外,Object Pascal 语言还有一个非常特别的访问指令,叫做 published。published属性(或字段或方法)不仅可以像public属性一样在运行时可用,而且还能生成可查询的扩展运行时信息(extended RTTI)。
事实上,在编译型语言中,编译后的符号由编译器处理并且在测试应用程序时调试器可以使用,但在运行时一般不会留下这些符号。换句话说(至少在早期的 Object Pascal 语言中是这样),如果一个类有一个名为 Name 的属性,你可以在代码中使用它来与类交互,但这种交互会被转换成数字地址,而源代码级别的标识符概念在运行时就不存在了,这意味着没有办法找出一个类是否有与给定字符串(如 “Name”)相匹配的属性标识符。
**注意:**Java 和 C# 语言都是采用了复杂虚拟执行环境(虚拟机)的编译型语言,因此,它们通过一种称为反射的概念拥有大量的运行时信息。Object Pascal 语言早在其他编译语言之前就有了基本的反射(通过published关键字),而之后随着语言的演变又引入了更多的反射或者说扩展的RTTI。本章将探讨与published关键字相关的基本 RTTI,第 16 章将探讨扩展反射。
为什么需要这些关于类的额外信息呢?这些信息是Object Pascal库所依靠的组件模型和可视化编程模型的基础之一。其中一些信息在开发环境中进行设计时使用,以填充“对象检视器”(Object Inspector)中组件提供的属性列表。这不是一个硬编码的列表,而是通过运行时检查编译后的代码中包含的额外运行时类型信息(RTTI)数据生成的。
另一个例子,可能有点复杂,现在不太适合深入讨论的是创建FMX和DFM文件以及任何附带的可视化窗体背后的流式传输机制。流式传输将仅在第18章中介绍,因为它更多地属于运行时库而不是核心语言。
总结这一概念:当您编写供自己或其他人在开发环境中使用的组件时,使用published
关键字是重要的。通常,组件的published
部分仅包含属性,但是窗体类还会使用published
字段和方法,这些将在稍后介绍。
10.2.1 设计时属性
在本章的前面部分,我们已经了解到属性在类的数据封装中扮演着重要角色。它们还在启用可视化开发模型方面起着基础性作用。事实上,您可以编写一个组件类,将其提供给开发环境,通过将其添加到窗体或类似的设计界面来创建对象,并使用“对象检视器”与其属性进行交互——这是可视化编程环境提供的工具,让您能够访问属性。并非所有属性都可以在这种情况下使用,只有那些在组件类中标记为"published"的属性才能够。这就是为什么Object Pascal程序员区分设计时属性和运行时属性。
设计时属性是在类声明的"published"部分中声明的,可以在IDE中以及在代码中使用。在类的"public"部分中声明的任何其他属性都不可在设计时使用,只能在代码中使用,通常被称为仅在运行时可用的属性。
换句话说,在设计时,您可以使用“对象检视器”查看并更改published属性的值。在运行时,您可以通过完全一样的方式在代码中读取值或写入值的方式访问另一个类的任何public或published属性。
并非所有类都有属性。属性存在于组件和TPersistent类的其他子类中,因为属性通常可以被流式传输并保存到文件中。事实上,窗体文件只不过是窗体上组件的published属性的集合。
更准确地说,要在一个类中支持published节(section),你不需要继TPersistent 类,但你需要使用$M
编译器指令编译这个类。每一个使用该指令编译的类,或者从使用该指令编译的类派生出来的类,都支持published节。由于 TPersistent 是使用此设置编译的,那么其任何派生类都支持此功能。
注意 下面关于默认可见性和自动 RTTI 的两节将进一步介绍$M 指令和published关键字的更多信息。
10.2.2 published属性与窗体
当IDE(集成开发环境)生成一个表单时,它会把表单上组件和方法的定义放在类的初始部分,也就是public和private关键词之前。这些初始部分的字段和方法就是已发布的属性。
尽管published属性是全局可见的,但这并不意味着进行全局访问是一个好主意。这意味着您必须特别注意您正在访问的全局属性(正如在前面关于向表单添加属性的部分所讨论的)。此外,当在组件类的元素之前指定显式访问指令时,默认访问级别是published。
注意 更准确地说,只有当类是在使用
$M+
编译器指令的情况下编译的,或者是从使用$M+
编译器指令编译的类派生的,published才是默认关键字。由于TPersistent类中使用了该指令,因此库的大多数类和所有组件类默认是published。然而,非组件类(如TStream和TList)是在使用$Mand
指令的情况下编译的,它们默认是public可见性。
这里是一个例子:
type
TForm1 = class(TForm)
Memo1: TMemo;
BtnTest: TButton;
分配给任何事件的方法都应该是published方法,窗体中与您的组件相对应的字段应该也是published,以便能够自动与窗体文件中描述的对象连接并随着窗体一起创建。
只有窗体声明初始published部分中的组件和方法才会出现在对象检查器中(在窗体组件列表中或当您选择事件下拉列表时显示的可用方法列表)。
为什么类的组件应该使用published段进行声明,而它们可以是私有的并更好地遵循OOP封装规则?原因在于这些组件是通过读取其流式表示而创建的,但一旦创建,就需要分配给相应的窗体字段。
这是通过RTTI完成的,RTTI由published字段生成。
注意,从技术上讲,并不是真的强制要求为组件使用published字段。通过将组件设为私有,可以让代码更加符合面向对象设计原则。然而,这样做需要额外的运行时代码。我将在本章的最后一节“RAD和OOP”中对此进行进一步解释。
10.2.3 RTTI自动生成
Object Pascal编译器另一个特殊的行为是,如果你在一个不是从TPersistent类继承的类中添加published关键字,编译器将自动启用RTTI生成,自动添加{$M+}
编译指令。假设你有这样的一个类:
type
TMyTestClass = class
private
FValue: Integer;
procedure SetValue(const Value: Integer);
published
property Value: Integer read FValue write SetValue;
end;
编译器将发出如下警告:
[dcc32 Warning] AutoRTTIForm.pas(27): W1055 PUBLISHED caused RTTI ($M+)
to be added to type 'TMyTestClass'
幕后所发生的是,编译器会自动在代码中注入{$M+}
指令,正如你在AutoRTTI示例中所看到的那样,该示例包含了上面的代码。在这个程序中,你可以编写以下代码,该代码可以动态地访问属性(使用老式的System.TypInfo单元):
uses
TypInfo;
procedure TFormAutoRtti.BtnTestClick(Sender: TObject);
var
Test1: TMyTestClass;
begin
Test1 := TMyTestClass.Create;
try
Test1.Value := 22;
Memo1.Lines.Add(GetPropValue(Test1, 'Value'));
finally
Test1.Free;
end;
end;
注意 尽管我偶尔会使用TypInfo单元及其定义的GetPropValue等函数,但RTTI访问的真正威力是由更现代的RTTI单元及其对反射的大量支持提供的。考虑到这是一个相当复杂的话题,我觉得应该单独为它写一章,并且还要区分现代Object Pascal支持的两种RTTI风格。