TObject vs String 来回转换

TObject vs String casting there and back

请有人解释一下它是如何工作的? TObject 和 String 不兼容,应该不行,我想。

var
  xObj: TObject;
begin
  // xObj := TObject('yyy'); // invalid typecast
  xObj := TObject('yyy ' + ClassName); // no error
  ShowMessage(String(xObj)); // no error too
end;

另外我很好奇 xObj 的内存何时会被释放?

这是一个很难回答的问题,因为

  1. 如果您知道 Delphi 内部数据格式和内存管理是如何工作的,那么这个问题就很简单了。

  2. 如果您不知道 Delphi 内部数据格式和内存管理是如何工作的,那么单个 SO 答案无法告诉您。

但是,由于非常天真,我会给你一个非常简短的介绍。此介绍包含一些指向文档的链接。我希望你会阅读那些文档页面。我还使用了很多技术词汇。我希望您努力真正理解这些概念中的每一个。


在Delphi中,object variable只不过是指向对象的指针。因此,它是一个 native-sized 整数。例如,在 Win64 上,它是一个 64 位整数——计算机内存中对象的地址。

如果A, B: TBitmap而你A := B,那么只会复制这样一个整数,这样AB之后就指向同一个位图对象堆(或者都是 nil 指针)。换句话说,NativeInt(B) = NativeInt(A).

对象(如果我们暂时忘记接口对象)不是由编译器管理的。您必须手动创建它们,并且必须手动释放它们:

var Frog := TFrog.Create;
try
  // Use Frog
finally
  Frog.Free;
end;

显然,您必须非常小心,不要造成内存泄漏或使用悬挂指针。始终使用 try..finally 成语(及其朋友),就像你的生活取决于它一样。

类似地,string 类型的变量是指向 string heap object 的指针。因此,字符串变量只不过是一个 native-sized 整数。例如,在 Win64 上,它是一个 64 位整数——计算机内存中字符串堆对象的地址。

与对象一样,字符串也是引用类型。但是,与对象不同的是,它们由编译器进行引用计数和管理,因此您永远不需要自己创建或释放它们。

尽管字符串是引用类型,但它们“看起来”像值类型,因为它们使用 copy-on-write (COW) 语义。

因此,在 string-variable 作业中,如

b := a;

赋值将 (1) 使 b 指向与 a 相同的字符串堆对象(以便之后 NativeInt(b) = NativeInt(a)),以及 (2) 更改 a 字符串堆对象的引用计数增加 1,因为现在 b 也指向它。此外,b 曾经指向的堆对象(如果有的话)的引用计数将减一,因为 b 不再指向它。如果它达到零,则释放堆对象。


现在,如果 A 是一个对象而 s 是一个字符串,那么 As 都是 native-sized 整数,所以当然你可以告诉编译器执行赋值。

然而,这(几乎总是)是一个非常糟糕的主意。如果使用 string(X),其中 X 是一个非 nil 指针,不指向字符串堆对象,编译器仍会将这部分内存视为字符串。然后会发生可怕的事情,比如内存损坏(如果你不走运)和 AV(如果你很幸运)。

类似地,如果 s 是一个字符串并且您开始使用 TBitmap(s),编译器会将 s 指向的内存(字符串堆对象)视为TBitmap 实例。再一次,可怕的事情会发生。

事实上,如果您混合使用托管类型和 non-managed 类型,即使您只是(看似)从错误类型的指针“读取”,也可能会发生可怕的事情。例如,在 end,编译器可能会添加代码以减少不指向字符串堆对象的“字符串”变量的引用计数。


或者,与OP的实际情况相关:可能会在你背后释放一个字符串。

考虑这个程序:

var
  p: Pointer;

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
begin
  s := Random(1024).ToString;
  p := Pointer(s);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ShowMessage(string(p));
end;

当点击Button1时,会设置一个局部字符串变量s指向一个引用计数为1的随机数字符串堆对象,然后我们将地址保存到这个字符串堆对象中在 non-managed 指针变量中。

当我们命中Button1Clickend时,这个堆对象的引用计数降为0,所以它被释放了。因此,p 现在是一个悬挂指针。

因此,如果您按 Button2,您将访问一个悬空指针:p 不指向字符串堆对象,而是指向您无法控制的某些内存。

您可能会得到字符串的旧内容。或者完全不同的东西。或者AV。

提示:在 p := Pointer(s); 上设置断点并转到“内存”面板 (Ctrl+Alt+1)。在此处按 Ctrl+G 并键入 s[1] 以转到字符串。在负偏移处,您将找到堆对象的引用计数。当您跨过 end 时,您会看到它下降到零。


奖励 material: Dynamic arrays 是引用类型,由编译器进行引用计数和管理,但 使用 COW 语义。 [实际上,在我的 YouTube 频道上,您可以找到一段关于他们内部机制的非常糟糕的视频。]