5.3.5 运算符和自定义托管记录
在 Delphi 语言中,有一组特殊的运算符可用于记录,以定义自定义托管记录。在此之前,请允许我回顾一下记录内存初始化的规则,以及普通记录和托管记录之间的区别。
Delphi 中的记录可以包含任何数据类型的字段。当记录具有普通(非托管)字段(如数值或其他枚举值)时,编译器无需做太多工作。创建和处置记录只需分配或释放内存区域即可。(请注意,默认情况下,Delphi 不会对记录进行零初始化,但会对数组进行零初始化,正如我们稍后将学习的,也会对新对象实例进行零初始化)。
如果记录的字段属于编译器管理的类型(如字符串或接口),编译器需要注入额外的代码来管理初始化或终止。例如,字符串是有引用计数的,因此当记录超出作用域时,记录中的字符串需要减少其引用计数,这可能会导致为字符串去释放内存。因此,当你在某部分代码使用托管记录时,编译器会自动在代码周围添加一个 try-finally 块,以确保即使出现异常也能清除数据。长期以来,Delphi 语言中的托管记录一直是这种情况。
从 10.4 开始,除了编译器为托管记录执行的默认操作外,Delphi 记录类型还支持自定义初始化(initialization)和终止化(finalization)。无论记录字段的数据类型如何,您都可以声明带有自定义初始化和最终化代码的记录,也可以编写此类自定义初始化和最终化代码。这些记录被称为 “自定义托管记录”。
开发人员可以通过在记录类型中添加一个或多个特定的新操作符,将记录转化为自定义托管记录:
Initialize
运算符在为记录分配内存后调用,允许您编写代码来设置字段的初始值Finalize
运算符在为记录释放内存之前调用,允许您执行任何必要的清理Assign
运算符在将记录数据复制到相同类型的另一条记录时调用,因此您可以以自定义方式从一条记录复制信息到另一条记录
注解:由于托管记录的清理即使在发生异常时也会执行(编译器会自动生成try-finally块),它们通常被用作保护资源分配或执行清理操作的替代方式。我们将在第9章的“使用托管记录还原光标”部分中看到此用法的示例。
记录的 Initialize
和 Finalize
运算符
我们用以下简单的代码片段介绍初始化和终止化:
typeTMyRecord = recordValue: Integer;class operator Initialize(out Dest: TMyRecord);class operator Finalize(var Dest: TMyRecord);end;
当然,您需要为这两个类方法编写代码,例如,可以记录其执行情况或初始化记录的 Value 字段。在本例(ManagedRecords_101
示例项目的一部分)中,我对 Value 字段进行了初始化,并记下了对内存位置的引用,以便查看执行每个操作的记录:
class operator TMyRecord.Initialize(out Dest: TMyRecord);
beginDest.Value := 10;Log('Created' + IntToHex(Integer(Pointer(@Dest)))));
end;class operator TMyRecord.Finalize(var Dest: TMyRecord);
beginLog('Destroyed' + IntToHex(Integer(Pointer(@Dest)))));
end;
这种构造机制与以前的记录机制的区别在于自动调用。如果你编写了类似下面的代码,你就可以同时调用初始化和终止化代码,最后由编译器为你的托管记录实例生成一个 try-finally
块:
procedure LocalVarTest;
varMy1: TMyRecord;
beginLog(My1.Value.ToString);
end;
使用上述代码,您将获得类似于以下内容的日志(地址将有所不同):
Created 0019F2A8
10
Destroyed 0019F2A8
另一个场景是使用内联变量,例如:
beginvar T: TMyRecord;Log(T.Value.ToString);
这将在日志中产生相同的序列。
赋值运算符
一般来说,赋值操作符(:=)会直接复制记录字段的所有数据。编译器也会正确处理具有托管类型(如字符串)的记录。
如果有自定义数据字段和自定义初始化,则您可能需要更改默认行为。因此,您也可以为自定义托管记录定义赋值操作符。新操作符使用 :=
语法调用,但定义为 Assign
:
class operator Assign(var Dest: TMyRecord; const [ref] Src: TMyRecord);
运算符定义必须遵循非常精确的规则,包括第一个参数必须是引用传递(var)的参数,将第二个参数是引用传递的const参数。如果未这样做,编译器将报出以下错误消息:
[dcc32 Error] E2617 First parameter of Assign operator must be a var
parameter of the container type
[dcc32 Hint] H2618 Second parameter of Assign operator must be a
const[Ref] or var parameter of the container type
这是调用 Assign
运算符的一个示例:
varMy1, My2: TMyRecord;
beginMy1.Value := 22;My2 := My1;
这将产生以下日志(我还为记录添加了一个序列号):
Created 5 0019F2A0
Created 6 0019F298
5 copied to 6
Destroyed 6 0019F298
Destroyed 5 0019F2A0
请注意,销毁的顺序与构建的顺序相反,最后创建的记录是第一个销毁的。
将托管记录作为参数传递
托管记录在作为参数传递或由函数返回时,其工作方式也与普通记录不同。下面的几个例程展示了各种情况:
procedure ParByValue(Rec: TMyRecord);
procedure ParByConstValue(const Rec: TMyRecord);
procedure ParByRef(var Rec: TMyRecord);
procedure ParByConstRef(const [ref] Rec: TMyRecord);
function ParReturned: TMyRecord;
现在,无需逐一检查每个日志(您可以运行ManagedRecords_101
演示来查看它们),这是信息摘要:
ParByValue
创建一个新记录并调用赋值运算符(如果可用)来复制数据,在超出范围时销毁临时副本ParByConstValue
不进行复制,也不调用任何内容ParByRef
不进行复制,也不调用任何内容ParByConstRef
不进行复制,也不调用任何内容ParReturned
创建一个新记录(通过Initialize
)并在返回时调用Assign
运算符(如果调用类似于my1 := ParReturned
),然后在赋值后删除临时记录
异常和托管记录
与对象不同,当异常发生时,即使没有显式的 try-finally 块,记录一般也会被清除。这是一个根本区别,也是托管记录真正有用的关键所在。
procedure ExceptionTest;
beginvar A: TMRE;var B: TMRE;raise Exception.Create('Error Message');
end;
在这个过程里,有两次构造函数调用和两次析构函数调用。同样,这也是托管记录的根本区别和关键特征。
托管记录的数组
如果定义了托管记录的静态数组,则会在声明时调用 Initialize 操作符对其进行初始化:
varA1: array[1..5] of TMyRecord; // 在这里调用初始化
beginLog('ArrOfRec');
当超出作用域时,它们就会被全部销毁。如果定义了托管记录的动态数组,则在调用初始化代码时,要确定数组的大小(使用 SetLength):
varA2: array of TMyRecord;
beginLog('ArrOfDyn');SetLength(A2, 5); // 在这里调用初始化