8.6 安全类型转换运算符
正如我们之前所见,子类的语言类型兼容性规则允许你在期望祖先类的地方使用子类。如我所提到的,反向则不可能。
现在假设TDog
类有一个Eat
方法,而TAnimal
类中不存在此方法。如果变量FMyAnimal
引用了一个狗,你可能希望能够调用这个函数。但如果你尝试调用,而变量指向的是另一个类,结果就会出错。通过显式类型转换,我们可能会导致令人讨厌的运行时错误(或者更糟糕的是,出现微妙的内存覆盖问题),因为编译器无法确定对象的类型是否正确,以及我们调用的方法是否确实存在。
为了解决这个问题,我们可以使用基于运行时类型信息的技术。从本质上讲,因为每个对象在运行时“知道”自己的类型和父类。我们可以使用is
运算符或使用TObject
类的一些方法询问此信息。is
运算符的参数是一个对象和一个类类型,返回值是一个布尔值:
if FMyAnimal is TDog then
...
只有当 FMyAnimal 对象当前引用的是 TDog 类对象或 TDog 的子类且与 TDog 兼容时,is 表达式才会返回 True。这意味着,如果测试存储在 TAnimal 变量中的 TDog 对象是否真的是 TDog 对象,测试将会成功。换句话说,如果可以安全地将对象 (FMyAnimal) 赋值给数据类型 (TDog) 的变量,则该表达式的值为 True。
注解: 实际上,
is
运算符的实现是由TObject
类的InheritsFrom
方法提供的。因此,您可以将相同的表达式写为FMyAnimal.InheritsFrom(TDog)
。直接使用这个方法的原因是它还可以应用于类引用和不支持is
运算符的其他特殊类型。
现在您确信动物是一只狗后,可以通过以下代码使用直接类型转换(通常是不安全的):
if FMyAnimal is TDog then
begin
MyDog := TDog(FMyAnimal);
Text := MyDog.Eat;
end;
同样的操作也可以直接通过另一个相关的类型转换运算符 as
来完成。as
仅在请求的类与实际类兼容时才转换对象,否则将导致运行时异常。as
运算符的参数是一个对象和一个类类型,结果是一个“转换”为新的类类型的对象。我们可以编写以下片段:
MyDog := FMyAnimal as TDog;
Text := MyDog.Eat;
如果只想调用 Eat
函数,还可以使用更简洁的表示法:
(FMyAnimal as TDog).Eat;
这个表达式的结果是一个 TDog
类数据类型的对象,因此您可以对其应用该类的任何方法。传统转换和使用 as
转换的区别在于,后者会检查对象的实际类型,并在类型与试图转换的类型不兼容时引发异常。引发的异常是 EInvalidCast
(下一章将介绍异常)
警告:相比之下,在 C# 语言中,如果对象与类型不兼容,则
as
表达式将返回nil
,而直接类型转换将引发异常。因此,基本上这两个操作与 Object Pascal 相比是相反的。
要避免这种异常,请使用 is
运算符,如果成功,则进行普通类型转换(实际上,没有理由连续使用 is
和 as
,进行两次类型检查,尽管通常会看到它们的组合使用):
if FMyAnimal is TDog then
TDog(MyAnimal).Eat;
在 Object Pascal
中,这两个类型转换运算符非常有用,因为您通常希望编写可用于相同类型或不同类型的多个组件的通用代码。例如,当将组件作为参数传递给事件响应方法时,使用通用数据类型(TObject
)时,通常需要将其转换回原始组件类型:
procedure TForm1.Button1Click(Sender: TObject);
begin
if Sender is TButton then
...
end;
这是一种常用技术,我将在后面的示例中使用它(事件将在第 10 章中介绍)。
这两个类型转换运算符 is
和 as
非常强大,您可能会被诱使将其视为标准的编程构造。尽管它们确实很强大,但您可能应该将它们的使用限制在特殊情况下。当您需要解决涉及多个类的复杂问题时,请首先尝试使用多态。只有在特殊情况下,多态无法应用时,才应尝试使用类型转换运算符来作为补充。
注意:使用类型转换操作符会对性能产生轻微的负面影响,因为它必须遍历类的层次结构,以查看类型转换是否正确。正如我们所看到的,虚拟方法调用只需要查找内存,速度要快得多。