如何在Delphi中同时正确释放包含多种类型的记录?

24
type
  TSomeRecord = Record
    field1: integer;
    field2: string;
    field3: boolean;
  End;
var
  SomeRecord: TSomeRecord;
  SomeRecAr: array of TSomeRecord;
这是我现有内容的最基本示例。由于我想重用SomeRecord(某些字段保持为空,而不释放所有内容,则当我重新使用SomeRecord时,有些字段会被带入,这显然是不希望的),因此我正在寻找一种同时释放所有字段的方法。我从string[255]开始,使用了ZeroMemory(),直到它开始泄漏内存,那是因为我切换到了string。我仍然缺乏理解它为什么会出现问题的知识,但它似乎与它是动态的有关。我还在使用动态数组,所以我假设在任何动态数组上尝试使用ZeroMemory()都会导致内存泄漏。浪费了一天的时间来解决这个问题。我认为在ZeroMemory()之前对SomeRecordSomeRecAr使用Finalize()可以解决这个问题,但我不确定这是否是正确的方法,或者只是我自己有点傻。
所以问题是:如何一次性释放所有内容?是否存在某个单一的过程可以完成这个任务,而我不知道?
另外,另一种选择是,我愿意接受如何以不同的方式实现这些记录的建议,这样我就不需要尝试复杂的释放内容。我已经查看了使用New()创建记录,然后用Dispose()将其清除,但我不知道当变量在调用Dispose()后未定义(而不是nil)时,它意味着什么。另外,我不知道某种类型的变量(SomeRecord:TSomeRecord)与指向类型的变量(SomeRecord:^TSomeRecord)之间的区别。我正在研究上述问题,除非有人能够快速解释,否则可能需要一些时间。

相关:https://dev59.com/RWYq5IYBdhLWcg3wzDzk - Gabriel
相关:https://dev59.com/9KXja4cB1Zd3GeqPZfqc - Gabriel
相关:https://dev59.com/RG035IYBdhLWcg3wSOCr - Gabriel
4个回答

37

假设您有一个支持在记录上实现方法的Delphi版本,您可以这样清除记录:

type
  TSomeRecord = record
    field1: integer;
    field2: string;
    field3: boolean;
    procedure Clear;
  end;

procedure TSomeRecord.Clear;
begin
  Self := Default(TSomeRecord);
end;
如果你的编译器不支持Default,那么你可以像这样很简单地做同样的事情:
procedure TSomeRecord.Clear;
const
  Default: TSomeRecord=();
begin
  Self := Default;
end;

如果您想避免在方法中更改值类型,请创建一个返回空记录值的函数,并使用赋值运算符:

type
  TSomeRecord = record
    // fields go here
    class function Empty: TSomeRecord; static;
  end;

class function TSomeRecord.Empty: TSomeRecord;
begin
  Result := Default(TSomeRecord);
end;

....

Value := TSomeRecord.Empty;
作为旁注,我找不到任何有关 Default(TypeIdentifier) 的文档参考。有人知道在哪里可以找到吗?
至于你问题的第二部分,我认为没有理由不继续使用记录,并使用动态数组进行分配。尝试自己管理生命周期会更容易出错。

1
@NGLN,我意识到这个回答的实质就是你最初写的。我认为这是一个非常好的回答。在我看来,你把它删掉了真是太可惜了。如果你恢复了这样的回答,那么我会删除这个并点赞你的。但我认为这个问题应该有类似的回答。我宁愿是你的回答,因为你第一个回答了它。 - David Heffernan
2
同意。没有什么不满的,我会保持现状。 - NGLN
2
我正想问同样的问题,因为在帮助中快速查找没有关于“Default()”的任何内容。我有XE2,所以它绝对支持记录中的方法。然而,“Default()”我不知道,我需要尝试一下。我确实喜欢你可以这样做EmptySomeRecord: TSomeRecord=(),我将来肯定会在某个地方使用它。 - Raith
1
以前从未见过 Default() 的任何参考资料。它在 Delphi XE 和 Delphi 2009 中都有,但在 Delphi 2007 中没有。 - LU RD
七年过去了...仍然没有明显的文档记录。 - Darian Miller
显示剩余11条评论

9

不要把事情复杂化!

为"默认"记录分配空间只会浪费CPU功率和内存。

当在TClass中声明一个记录时,它将被填充为零,即初始化。当记录在堆栈上分配时,仅引用计数变量被初始化:其他类型的变量 (例如整数或双精度或布尔或枚举)处于随机状态(可能为非零)。当记录在堆上分配时,getmem不会初始化任何内容,allocmem将填充所有内容为零,new仅初始化引用计数成员(如在堆栈初始化):在所有情况下,应使用dispose或finalize+freemem来释放堆分配的记录。

因此,关于您的确切问题,您自己的假设是正确的:在先前的finalize之前不要使用“fillchar”(或“zeromemory”)重置记录内容。以下是正确且最快的方法:

Finalize(aRecord);
FillChar(aRecord,sizeof(aRecord),0);

再次强调,使用默认记录比分配默认值要快。而且,如果您使用Finalize,即使多次使用,也不会泄漏任何内存 - 百分之百的退款保证!
编辑:在查看aRecord := default(TRecordType)生成的代码后,发现代码已经进行了很好的优化:实际上是一个Finalize+一堆stosd来模拟FillChar。所以即使语法是复制/赋值(:=),它并没有实现为复制/赋值。这是我的错误。
但我仍然不喜欢必须使用:=的事实,Embarcadero应该更好地使用像aRecord.Clear这样的记录方法作为语法,就像DelphiWebScript的动态数组一样。事实上,这个:=语法与C#完全相同。似乎Embacardero只是在到处模仿C#的语法,而没有发现这很奇怪。如果Delphi只是一个追随者,而没有按照自己的方式实现想法,那还有什么意义呢?人们永远会更喜欢原始的C#而不是它的祖先(Delphi有同样的父亲)。

1
为什么一个那么自豪的“-1”没有任何评论?;) - Arnaud Bouchez
2
你的反对者显然改变了他们的想法,但在这里应该给出一个反对票。首先,性能可能不是 OP 最重要的因素。但更重要的是,你在这里的断言是不正确的。aRecord := Default(TSomeRecord) 是高效的。 - David Heffernan
2
@DavidHeffernan 我刚刚检查了Self := default(record)生成的代码。它使用stosd进行Finalize + inline fillchar。它是经过优化的(除非您不使用“rep stosd”)。所以你是对的,这是关于速度的好代码。如果只有记录复制(更常用)也被如此优化就好了。 - Arnaud Bouchez
1
代码清除记录的效率只有在该代码是应用程序中的热点时才会有影响。这似乎不太可能发生。 - David Heffernan
事实上,在我的简单基准测试中,Finalize/FillChar 比我的建议要慢。因此,即使性能问题可能无关紧要,你的事实似乎是不正确的。现在,你有机会通过编辑你的答案来纠正这个错误信息。 - David Heffernan
显示剩余3条评论

7
我认为最简单的解决方案是:

以下是需要翻译的内容:

const
  EmptySomeRecord: TSomeRecord = ();
begin
  SomeRecord := EmptySomeRecord;

但是为了回答您问题的其余部分,请参照以下定义:

type
  PSomeRecord = ^TSomeRecord;
  TSomeRecord = record
    Field1: Integer;
    Field2: String;
    Field3: Boolean;
  end;
  TSomeRecords = array of TSomeRecord;
  PSomeRecordList = ^TSomeRecordList;
  TSomeRecordList = array[0..MaxListSize] of TSomeRecord;    
const
  EmptySomeRecord: TSomeRecord = ();
  Count = 10;    
var
  SomeRecord: TSomeRecord;
  SomeRecords: TSomeRecords;
  I: Integer;
  P: PSomeRecord;
  List: PSomeRecordList;

procedure ClearSomeRecord(var ASomeRecord: TSomeRecord);
begin
  ASomeRecord.Field1 := 0;
  ASomeRecord.Field2 := '';
  ASomeRecord.Field3 := False;
end;

function NewSomeRecord: PSomeRecord;
begin
  New(Result);
  Result^.Field1 := 0;
  Result^.Field2 := '';
  Result^.Field3 := False;
end;

以下是一些关于如何操作它们的多个示例:

begin
  // Clearing a typed variable (1):
  SomeRecord := EmptySomeRecord;

  // Clearing a typed variable (2):
  ClearSomeRecord(SomeRecord);

  // Initializing and clearing a typed array variabele:
  SetLength(SomeRecords, Count);

  // Creating a pointer variable:
  New(P);

  // Clearing a pointer variable:
  P^.Field1 := 0;
  P^.Field2 := '';
  P^.Field3 := False;

  // Creating and clearing a pointer variable:
  P := NewSomeRecord;

  // Releasing a pointer variable:
  Dispose(P);

  // Creating a pointer array variable:
  ReallocMem(List, Count * SizeOf(TSomeRecord));

  // Clearing a pointer array variable:
  for I := 0 to Count - 1 do
  begin
    Pointer(List^[I].Field2) := nil;
    List^[I].Field1 := 0;
    List^[I].Field2 := '';
    List^[I].Field3 := False;
  end;

  // Releasing a pointer array variable:
  Finalize(List^[0], Count);

根据您的喜好进行选择和/或组合。


所以你的建议是每次想要清空SomeRecord时都将其设置为EmptyRecord吗?我猜那很简单,但这真的是正确的方法吗?涉及到的记录有很多其他字段,比如几百个具有自定义类型的字段。声明这样一个EmptyRecord并不是不可行的,但如果考虑常量声明的大小,则似乎不切实际。 - Raith
4
常量的更灵活的定义方式是 EmptySomeRecord: TSomeRecord = ();,让编译器自动填入零值。 - David Heffernan
我可能会实现一个名为Zeroise或Clear的记录方法,执行空记录的赋值。 - David Heffernan
@David 如果你有支持记录方法的 Delphi 版本,那么可以! - NGLN
-1 我真的一点都不喜欢你编辑后的版本,编辑之前的版本很棒。1. 你无休止地复制代码到三个字段的三个赋值中。使用你编写的帮助函数ClearSomeRecord,更好的方法是将其作为记录方法。2. 为什么要声明EmptySomeRecord而不在ClearSomeRecord中使用它?3. SetLength会将新元素初始化为零,请不要再次这样做。4. 不要使用GetMemRealloc等重新实现动态数组。使用动态数组即可。 - David Heffernan
@David 这些只是一些不同的解决方案供 OP 选择或组合。此外,OP 询问如何处理记录和记录数组的各种指针类型,因此使用了 ReallocMemFinalize。我会让原始答案更加突出。 - NGLN

1

使用 SomeRecord: TSomeRecordSomeRecord 将是类型为 TSomeRecord 的实例/变量。使用 SomeRecord: ^TSomeRecordSomeRecord 将是指向类型为 TSomeRecord 的实例或变量的指针。在最后一种情况下,SomeRecord 将是一个类型化的指针。如果您的应用程序在例程之间传输大量数据或与外部 API 交互,则建议使用类型化指针。

new()dispose() 仅与类型化指针一起使用。对于类型化指针,编译器无法控制/了解您的应用程序使用此类变量时使用的内存。释放类型化指针使用的内存由您自己负责。

另一方面,当您使用普通变量时,根据使用和声明的方式,编译器将在它们不再需要时释放所使用的内存。例如:

function SomeStaff();
var
    NativeVariable: TSomeRecord;
    TypedPointer: ^TSomeRecord;
begin
    NaviveVariable.Field1 := 'Hello World';

    // With typed pointers, we need to manually
    // create the variable before we can use it.
    new(TypedPointer);
    TypedPointer^.Field1 := 'Hello Word';

    // Do your stuff here ...

    // ... at end, we need to manually "free"
    // the typed pointer variable. Field1 within
    // TSomerecord is also released
    Dispose(TypedPointer);

    // You don't need to do the above for NativeVariable
    // as the compiler will free it after this function
    // ends. This apply also for native arrays of TSomeRecord.
end;

在上面的例子中,变量NativeVariable仅在SomeStaff函数内部使用,因此编译器在函数结束时自动释放它。这适用于几乎所有本地变量,包括数组和记录“字段”。对象处理方式不同,但这是另一篇文章要讨论的事情。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接