为什么这个字符串的引用计数是4?(Delphi 2007)

16

这是一个非常特定于Delphi(甚至可能是特定于Delphi 2007)的问题。我目前正在编写一个简单的字符串池类来池化字符串。作为一名优秀的程序员,我还添加了单元测试,并发现了一些困惑我的事情。

下面是实现字符串池化的代码:

function TStringPool.Intern(const _s: string): string;
var
  Idx: Integer;
begin
  if FList.Find(_s, Idx) then
    Result := FList[Idx]
  else begin
    Result := _s;
    if FMakeStringsUnique then
      UniqueString(Result);
    FList.Add(Result);
  end;
end;

代码很简单:FList是一个已排序的TStringList,所以它只是在列表中查找字符串,并且如果该字符串已经存在,则返回现有字符串。如果尚未在列表中,则首先调用UniqueString以确保引用计数为1,然后将其添加到列表中。(我检查了Result的引用计数,当'hallo'被添加两次时,它的引用计数为3,符合预期。)

现在是测试代码:

procedure TestStringPool.TestUnique;
var
  s1: string;
  s2: string;
begin
  s1 := FPool.Intern('hallo');
  CheckEquals(2, GetStringReferenceCount(s1));
  s2 := s1;
  CheckEquals(3, GetStringReferenceCount(s1));
  CheckEquals(3, GetStringReferenceCount(s2));
  UniqueString(s2);
  CheckEquals(1, GetStringReferenceCount(s2));
  s2 := FPool.Intern(s2);
  CheckEquals(Integer(Pointer(s1)), Integer(Pointer(s2)));
  CheckEquals(3, GetStringReferenceCount(s2));
end;

这将字符串“hallo”添加到字符串池中两次,并检查字符串的引用计数,以及s1和s2确实指向同一个字符串描述符。

每个CheckEquals按预期工作,除了最后一个。它失败并显示错误“expected: <3> but was: <4>”。

那么为什么这里的引用计数是4?我本来希望是3:

  • s1
  • s2
  • 以及在StringList中的另一个

这是Delphi 2007,因此字符串是AnsiStrings。

噢,是的,函数StringReferenceCount的实现如下:

function GetStringReferenceCount(const _s: AnsiString): integer;
var
  ptr: PLongWord;
begin
  ptr := Pointer(_s);
  if ptr = nil then begin
    // special case: Empty strings are represented by NIL pointers
    Result := MaxInt;
  end else begin
    // The string descriptor contains the following two longwords:
    // Offset -1: Length
    // Offset -2: Reference count
    Dec(Ptr, 2);
    Result := ptr^;
  end;
end;

在调试器中,可以将其表示为:

plongword(integer(pointer(s2))-8)^

只是为了补充一下来自Serg的回答(看起来是完全正确的):

如果我替换

s2 := FPool.Intern(s2);

使用

s3 := FPool.Intern(s2);
s2 := '';

然后检查s3(和s1)的引用计数,预期为3。这仅是因为将FPool.Intern(s2)的结果再次赋值给s2(s2既是参数又是函数结果的目标),导致了这种现象。Delphi引入了一个隐藏的字符串变量来分配结果。

此外,如果将函数更改为过程:

procedure TStringPool.Intern(var _s: string);
参考计数是3,因为不需要隐藏变量,如预期所示。对于那些对TStringPool实现感兴趣的人:它是MPL下的开源项目,并作为dzlib的一部分可用,而该库又是dzchart的一部分。您可以在以下链接中找到TStringPool.pas的代码:https://sourceforge.net/p/dzlib/code/HEAD/tree/dzlib/trunk/src/u_dzStringPool.pas。但正如上面说的那样:这并不是特别高深的技术。;-)

你能在TestUnique结束时检查一下S1的引用计数吗?我很好奇此时它的引用计数是多少。 - Marjan Venema
当然可以使用调试 DCU。 - David Heffernan
@david:我已经尝试过调试dcus,但那并没有给我任何见解。 - dummzeuch
@Marjan:S1的引用计数也是4(这是应该的,因为两个变量都指向同一个字符串描述符:CheckEquals(Integer(Pointer(s1)), Integer(Pointer(s2)));)。 - dummzeuch
2
你为什么对使字符串唯一感兴趣? - David Heffernan
显示剩余8条评论
1个回答

14

测试一下:

function RefCount(const _s: AnsiString): integer;
var
  ptr: PLongWord;
begin
  ptr := Pointer(_s);
  Dec(Ptr, 2);
  Result := ptr^;
end;

function Add(const S: string): string;
begin
  Result:= S;
end;

procedure TForm9.Button1Click(Sender: TObject);
var
  s1: string;
  s2: string;

begin
  s1:= 'Hello';
  UniqueString(s1);
  s2:= s1;
  ShowMessage(Format('%d', [RefCount(s1)]));   // 2
  s2:= Add(s1);
  ShowMessage(Format('%d', [RefCount(s1)]));   // 2
  s1:= Add(s1);
  ShowMessage(Format('%d', [RefCount(s1)]));   // 3
end;

如果你写下 s1:= Add(s1),编译器会创建一个隐藏的本地字符串变量,而这个变量负责增加引用计数。你不需要关心它。


1
这基本上是 Delphi 函数结果作为 VAR 参数传递的产物吗?我知道接口有类似的效果(有时用于节省 try..finally 结构),但不知道它也适用于字符串。 - dummzeuch
1
@dummzeuch - 我认为这是正确的。编译器无法将s1作为var传递给s1:= Add(s1),因此它会创建一个隐藏变量,将其作为var传递并将其分配给s1(增加引用计数)。 - kludg

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