13.6.2 嵌套的Finally代码块
Finally代码块可能是确保程序安全最重要、最常用的技术。我不认为这是一个高级话题,但你是否在所有地方都使用了 finally?在边界情况下,例如嵌套操作中,你是否正确使用了finally,还是在一个finally块中合并了多个finalization语句?这是一个远非完美的代码示例:
procedure TForm1.BtnTryFClick(Sender: TObject);
var
A1, A2: TAClass;
begin
A1 := TAClass.Create;
A2 := TAClass.Create;
try
A1.Whatever := 'One';
A2.Whatever := 'Two';
finally
A2.Free;
A1.Free;
end;
end;
这是相同代码更安全,更正确的版本(再次从SafeCode
示例中提取):
procedure TForm1.BtnTryFClick(Sender: TObject);
var
A1, A2: TAClass;
begin
A1 := TAClass.Create;
try
A2 := TAClass.Create;
try
A1.Whatever := 'One';
A2.Whatever := 'Two';
finally
A2.Free;
end;
finally
A1.Free;
end;
end;
13.6.3 动态类型检查
一般来说,类型之间的动态转换操作,尤其是类类型之间的动态转换操作,是另一个陷阱可能的来源。特别是如果您不使用 is 和 as 操作符,而只是进行硬类型转换。事实上,每一次直接类型转换都是潜在的错误源(除非它遵循 is 检查)。
从对象到指针、从类引用到类引用、从对象到接口、从字符串到字符串的类型转换可能非常危险,但在某些特殊情况下很难避免。例如,你可能想在组件的 Tag 属性中保存对象引用,而 Tag 属性是一个整数,因此你不得不进行硬转换。另一种情况是使用老式的 TList(而不是类型安全的泛型列表,将在下一章介绍)将对象保存在指针列表中。
这里有一个相当愚蠢的例子:
procedure TForm1.BtnCastClick(Sender: TObject);
var
List: TList;
begin
List := TList.Create;
try
List.Add(Pointer(Sender));
List.Add(Pointer(23422));
// 直接类型转换
TButton(List[0]).Caption := 'Ouch';
TButton(List[1]).Caption := 'Ouch';
finally
List.Free;
end;
end;
运行这段代码通常会导致访问违规。
注解:这里我说 "一般 "是因为当你随机访问内存时,你永远不知道实际效果如何。有时,程序只是覆盖了内存,并不会立即导致错误,但事后你就很难弄清楚为什么其他数据会被破坏。
你应该尽可能避免类似情况的发生,但如果你碰巧别无选择,又该如何修复这段代码呢?最自然的方法是使用as安全类型转换(as safe cast)或is类型检查(is type check),就像下面的代码片段一样:
// "as"类型转换
(TObject(List[0]) as TButton).Caption := 'Ouch';
(TObject(List[1]) as TButton).Caption := 'Ouch';
// "is"类型转换
if TObject(List[0]) is TButton then
TButton(List[0]).Caption := 'Ouch';
if TObject(List[1]) is TButton then
TButton(List[1]).Caption := 'Ouch';
然而,这并不是解决问题的办法,你会继续遇到访问违规。问题在于,is 和 as 最终都会调用 TObject.InheritsFrom,而这是一种很难在数字上执行的操作!
解决办法是什么?真正的解决办法是首先避免出现类似情况(老实说,这类代码没有什么意义),例如使用 TObjectList 或其他安全技术(再次参阅下一章的通用容器类)。如果你真的喜欢低级黑客并喜欢玩指针,你可以试着找出给定的 "数字值 "是否真的是一个对象的引用。不过,这并不是一个简单的操作。这其中还有有趣的一面,我将以此为借口在下面的演示中解释对象和类引用的内部结构。
13.6.4 这个指针是对象引用吗
本节解释了对象和类引用的内部结构,远远超出了本书大部分内容的讨论范围。尽管如此,它仍能为更专业的读者提供一些有趣的见解,因此我决定保留这部分内容,这些内容来自我以前写过一篇关于内存管理的高级论文。还要注意的是,下面内存检查方面的具体实现只是适用于 Windows系统。
有时,你会有一些指针(指针只是一个数值,指的是某些数据的物理内存位置)。这些指针实际上可能是对对象的引用,你通常知道它们是什么时候的引用,并将它们作为引用使用。但是,每当你进行一次低层次的转换时,你就真的快要把整个程序搞砸了。有一些技术可以让这种指针管理更安全一些,即使不能保证百分之百安全。
在使用指针之前,你可能需要考虑的问题是,它是否是一个合法的指针。Assigned 函数只能检查指针是否为 nil,在这种情况下并没有帮助。不过,Object Pascal RTL(Windows 平台上的 System 单元)中鲜为人知的 FindHInstance 函数会返回堆块的基地址,其中包括作为参数传递的对象,如果指针指向的是无效页,则返回 0(防止出现频率很低但极难跟踪的内存页错误)。如果随便取一个数字,它很可能不是指向一个有效的内存页。
这是一个很好的起点,但我们可以做得更好,因为如果值是字符串引用或任何其他有效指针,而不是对象引用,这样做也无济于事。现在,如何知道指针是否真的是对象引用呢?我想出了以下经验测试方法。对象的前 4 个字节是指向其类的指针。如果考虑类引用的内部数据结构,那么它的 vmtSelfPtr 位置就是指向自身的指针。如图 13.7 所示。
图 13.7:对象和类引用内部结构的大致示意图
换句话说,从类引用指针(这是一个负偏移量,在内存中较低的位置)解引用内存位置 vmtSelfPtr 字节上的值,就可以再次获得相同的类引用指针。此外,在类引用的内部数据结构中,可以读取实例大小信息(位于 vmtInstanceSize 位置),看看其中是否有合理的数字。以下是实际代码:
function IsPointerToObject(Address: Pointer): Boolean;
var
ClassPointer, VmtPointer: PByte;
InstSize: Integer;
begin
Result := False;
if FindHInstance(Address) > 0 then
begin
VmtPointer := PByte(Address^);
ClassPointer := VmtPointer + vmtSelfPtr;
if Assigned(VmtPointer) and (FindHInstance(VmtPointer) > 0) then
begin
InstSize := (PInteger(VmtPointer + VmtInstanceSize))^;
// 检查Self指针和“合理”的实例大小
if Pointer(Pointer(ClassPointer)^ = Pointer(VmtPointer)) and
(InstSize > 0) and (InstSize < 10000) then
Result := True;
end;
end;
end;
注解 此函数返回正确值的概率非常高,但并非百分之百。不幸的是,内存中的随机数据可能会通过测试。
有了这个函数,在前面的 SafeCode 示例中,我们可以在进行安全转换之前添加一个指针到对象的检查:
if IsPointerToObject(List[0]) then
(TObject(List[0]) as TButton).Caption := 'Ouch';
if IsPointerToObject(List[1]) then
(TObject(List[1]) as TButton).Caption := 'Ouch';
同样的想法也可以直接应用于类引用,以实现它们之间的安全转换。同样,最好首先通过编写更安全、更简洁的代码来避免类似问题,但万一无法避免,IsPointerToObject 函数可能会派上用场。无论如何,本节应该已经解释了这些系统数据结构的一些内部构造。