目录
===============================================================================
⊙ RTTI 简介
⊙ 类(
class
) 和 VMT 的关系
⊙ 类(
class
)、类的类(
class
of
class
)、类变量(
class
variable) 的关系
⊙ TObject
.
ClassType 和 TObject
.
ClassInfo
⊙
is
和
as
运算符的原理
⊙ TTypeInfo – RTTI 信息的结构
⊙ 获取类(
class
)的属性(
property
)信息
⊙ 获取方法(method)的类型信息
⊙ 获取有序类型(ordinal)、集合(
set
)类型的 RTTI 信息
⊙ 获取其它数据类型的 RTTI 信息
===============================================================================
正文
===============================================================================
⊙ RTTI 简介
===============================================================================
RTTI(Run-Time
Type
Information) 翻译过来的名称是“运行期类型信息”,也就是说可以在运行期获得数据类型或类(
class
)的信息。这个 RTTI 到底有什么用处,我现在也说不清楚。我是在阅读
Delphi 持续机制的代码中发现了很多 RTTI 的运用,只好先把 RTTI 学习一遍。下面是我的学习笔记。如果你发现了错误请告诉我。谢谢!
Delphi 的 RTTI 主要分为类(
class
)的 RTTI 和一般数据类型的 RTTI,下面从类(
class
)开始。
===============================================================================
⊙
类(
class
) 和 VMT 的关系
===============================================================================
一个类(
class
),从编译器的角度来看就是一个指向 VMT
的指针(在后文用 VMTptr 表示)。在类的 VMTptr 的负地址方向存储了一些类信息的指针,这些指针的值和指针所指的内容在编译后就确定了。比如 VMTptr -
44
的内容是指向类名称(ClassName)的指针。不过一般不使用数值来访问这些类信息,而是通过 System
.
pas 中定义的以 vmt 开头的常量,如 vtmClassName、vmtParent 等来访问。
类的方法有两种:对象级别的方法和类级别的方法。两者的 Self 指针意义是不同的。在对象级别的方法中 Self 指向对象地址空间,因此可以用它来访问对象的成员函数;在类级别的方法中 Self 指向类的 VMT,因此只能用它来访问 VMT 信息,而不能访问对象的成员字段。
===============================================================================
⊙ 类(
class
)、类的类(
class
of
class
)、类变量(
class
variable) 的关系
===============================================================================
上面说到类(
class
) 就是 VMTptr。在 Delphi 中还可以用
class
of
关键字定义类的类,并且可以使用类的类定义类变量。从语法上理解这三者的关键并不难,把类当成普通的数据类型来考虑就可以了。在编译器级别上表现如何呢?
为了简化讨论,我们使用 TObject、TClass 和 TMyClass 来代表上面说的三种类型:
type
TClass =
class
of
TObject;
var
TMyClass: TClass;
MyObject: TObject;
begin
TMyClass := TObject;
MyObject := TObject
.
Create;
MyObject := TClass
.
Create;
MyObject := TMyClass
.
Create;
end
;
在上面的例子中,三个 TObject 对象都被成功地创建了。编译器的实现是:TObject 是一个 VMTPtr 常量。TClass 也是一个 VMTptr 常量,它的值就是 TObject。
TMyClass 是一个 VMTptr 变量,它被赋值为 TObject。TObject
.
Create 与 TClass
.
Create
的汇编代码完全相同。但 TClass 不仅缺省代表一个类,而且还(主要)代表了类的类型,可以用它来定义类变量,实现一些类级别的操作。
===============================================================================
⊙ TObject
.
ClassType 和 TObject
.
ClassInfo
===============================================================================
function
TObject
.
ClassType: TClass;
begin
Pointer
(Result) := PPointer(Self)^;
end
;
TObject
.
ClassType 是对象级别的方法,Self 的值是指向对象内存空间的指针,对象内存空间的前
4
个字节是类的 VMTptr。因此这个函数的返回值就是类的 VMTptr。
class
function
TObject
.
ClassInfo:
Pointer
;
begin
Result := PPointer(
Integer
(Self) + vmtTypeInfo)^;
end
;
TObject
.
ClassInfo 使用
class
关键字定义,因此是一个类级别的方法。该方法中的 Self 指针就是 VMTptr。所以这个函数的返回值是 VMTptr 负方向的 vmtTypeInfo 的内容。
TObject
.
ClassInfo 返回的
Pointer
指针,实际上是指向类的 RTTI 结构的指针。但是不能访问 TObject
.
ClassInfo 指向的内容(TObject
.
ClassInfo
返回值是
0
),因为 Delphi 只在 TPersistent 类及 TPersistent 的后继类中产生 RTTI 信息。(从编译器的角度来看,这是在 TPersistent 类的声明之前使用 {
$M
+} 指示字的结果。)
TObject 还定义了一些获取类 RTTI 信息的函数,列举在下,就不一一分析了:
TObject
.
ClassName:
ShortString
; 类的名称
TObject
.
ClassParent: TClass;
对象的父类
TObject
.
InheritsFrom:
Boolean
; 是否继承自某类
TObject
.
InstanceSize:
Longint
; 对象实例的大小
===============================================================================
⊙
is
和
as
运算符的原理
===============================================================================
我们知道可以在运行期使用
is
关键字判断一个对象是否属于某个类,可以使用
as
关键字把某个对象安全地转换为某个类。在编译器的层次上,
is
和
as
的操作是由 System
.
pas 中两个函数完成的。
function
_IsClass(Child: TObject; Parent: TClass):
Boolean
;
begin
Result := (Child <>
nil
)
and
Child
.
InheritsFrom(Parent);
end
;
_IsClass 很简单,它使用 TObject 的 InheritsForm 函数判断该对象是否是从某个类或它的父类中继承下来的。每个类的 VMT 中都有一项 vmtParent 指针,指向该类的父类的 VMT。TObject
.
InheritsFrom
实际上是通过[递归]判断父类 VMT 指针是否等于自己的 VMT 指针来判断是否是从该类继承的。
class
function
TObject
.
InheritsFrom(AClass: TClass):
Boolean
;
var
ClassPtr: TClass;
begin
ClassPtr := Self;
while
(ClassPtr <>
nil
)
and
(ClassPtr <> AClass)
do
ClassPtr := PPointer(
Integer
(ClassPtr) + vmtParent)^;
Result := ClassPtr = AClass;
end
;
as
操作符实际上是由 System
.
pas 中的 _AsClass 函数完成的。它简单地调用
is
操作符判断对象是否属于某个类,如果不是就触发异常。虽然 _AsClass 返回值为 TObject 类型,但编译器会自动把返回的对象改变为 Parent 类,否则返回的对象没有办法使用 TObject 之外的方法和数据。
function
_AsClass(Child: TObject; Parent: TClass): TObject;
begin
Result := Child;
if
not
(Child
is
Parent)
then
Error(reInvalidCast);
end
;
===============================================================================
⊙ TTypeInfo – RTTI 信息的结构
===============================================================================
RTTI 信息的结构定义在 TypInfo
.
pas 中:
TTypeInfo =
record
Kind: TTypeKind;
Name:
ShortString
;
end
;
TTypeInfo 就是 RTTI 信息的结构。TObject
.
ClassInfo 返回指向存放
class
TTypeInfo 信息的指针。Kind 是枚举类型,它表示 RTTI 结构中所包含数据类型。Name 是数据类型的名称。注意,最后一个字段 TypeData 被注释掉了,这说明该处的结构内容根据不同的数据类型有所不同。
TTypeKind 枚举定义了可以使用 RTTI 信息的数据类型,它几乎包含了所有的 Delphi 数据类型,其中包括 tkClass。
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);
TTypeData 是个巨大的记录类型,在此不再列出,后文会根据需要列出该记录的内容。
===============================================================================
⊙ 获取类(
class
)的属性(
property
)信息
===============================================================================
这一段是 RTTI 中最复杂的部分,努力把本段吃透,后面的内容都是非常简单的。
下面是一个获取类的属性的例子:
procedure
GetClassProperties(AClass: TClass; AStrings: TStrings);
var
PropCount, I:
SmallInt
;
PropList: PPropList;
PropStr:
string
;
begin
PropCount := GetTypeData(AClass
.
ClassInfo).PropCount;
GetPropList(AClass
.
ClassInfo, PropList);
for
I :=
0
to
PropCount -
1
do
begin
case
PropList[I]^.PropType^.Kind
of
tkClass : PropStr :=
'[Class] '
;
tkMethod : PropStr :=
'[Method]'
;
tkSet : PropStr :=
'[Set] '
;
tkEnumeration: PropStr :=
'[Enum] '
;
else
PropStr :=
'[Field] '
;
end
;
PropStr := PropStr + PropList[I]^.Name;
PropStr := PropStr +
': '
+ PropList[I]^.PropType^.Name;
AStrings
.
Add(PropStr);
end
;
FreeMem(PropList);
end
;
你可以在表单上放置一个 TListBox ,然后执行以下语句观察执行结果:
GetClassProperties(TForm1, ListBox1
.
Items);
该函数先使用 GetTypeData 函数获得类的属性数量。GetTypeData 是 TypInfo
.
pas 中的一个函数,它的功能是返回
TTypeInfo 的 TypeData 数据的指针:
function
GetTypeData(TypeInfo: PTypeInfo): PTypeData; assembler;
class
的 TTypeData 结构如下:
TTypeData =
packed
record
case
TTypeKind
of
tkClass: (
ClassType: TClass;
ParentInfo: PPTypeInfo;
PropCount:
SmallInt
;
UnitName: ShortStringBase;
);
end
;
其中的 PropData 又是一个大小可变的字段。TPropData 的定义如下:
TPropData =
packed
record
PropCount:
Word
;
PropList:
record
end
;
end
;
每个属性信息在内存中的结构就是 TPropInfo,它的定义如下:
PPropInfo = ^TPropInfo;
TPropInfo =
packed
record
PropType: PPTypeInfo;
GetProc:
Pointer
;
SetProc:
Pointer
;
StoredProc:
Pointer
;
Index:
Integer
;
Default:
Longint
;
NameIndex:
SmallInt
;
Name:
ShortString
;
end
;
为了方便访问属性信息,TypInfo
.
pas 中还定义了指向 TPropInfo 数组的指针:
PPropList = ^TPropList;
TPropList =
array
[
0..16379
]
of
PPropInfo;
我们可以使用 GetPropList 获得所有属性信息的指针数组,数组用完以后要记得用 FreeMem 把数组的内存清除。
function
GetPropList(TypeInfo: PTypeInfo; out PropList: PPropList):
Integer
;
GetPropList 传入类的 TTypeInfo 指针和 TPropList 的指针,它为 PropList 分配一块内存后把该内存填充为指向 TPropInfo 的指针数组,最后返回属性的数量。
上面的例子演示了如何获得类的所有属性信息,也可以根据属性的名称单独获得属性信息:
function
GetPropInfo(TypeInfo: PTypeInfo;
const
PropName:
string
): PPropInfo;
GetPropInfo 根据类的 RTTI 指针和属性的名称字符串,返回属性的信息 TPropInfo 的指针。如果没有找到该属性,则返回
nil
。GetPropInfo 很容易使用,举个例子:
ShowMessage(GetPropInfo(TForm,
'Name'
)^.PropType^.Name);
这句调用显示了 TForm 类的 Name 属性的类型名称:TComponentName。