为什么两个TBytes不能使用重叠数据?

17

考虑以下 XE6 代码。意图是将 ThingData 写入控制台,分别针对 Thing1Thing2,但实际上并没有输出。为什么会这样呢?

program BytesFiddle;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TThing = class
  private
    FBuf : TBytes;
    FData : TBytes;
    function GetThingData: TBytes;
    function GetThingType: Byte;
  public
    property ThingType : Byte read GetThingType;
    property ThingData : TBytes read GetThingData;

    constructor CreateThing(const AThingType : Byte; const AThingData: TBytes);
  end;

{ TThing1 }

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + 1);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));

  FData := @FBuf[1];
  SetLength(FData, Length(FBuf) - 1);
end;

function TThing.GetThingData: TBytes;
begin
  Result := FData;
end;

function TThing.GetThingType: Byte;
begin
  Result := FBuf[0];
end;

var
  Thing1, Thing2 : TThing;

begin
  try
    Thing1 := TThing.CreateThing(0, TEncoding.UTF8.GetBytes('Sneetch'));
    Thing2 := TThing.CreateThing(1, TEncoding.UTF8.GetBytes('Star Belly Sneetch'));

    Writeln(TEncoding.UTF8.GetString(Thing2.ThingData));
    Writeln(Format('Type %d', [Thing2.ThingType]));

    Writeln(TEncoding.UTF8.GetString(Thing1.ThingData));
    Writeln(Format('Type %d', [Thing1.ThingType]));

    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

提供了一个标题建议,随意编辑。 - Johan
这么短的时间内有这么多的浏览量:)。这里是否有来自知名论坛的链接? - LU RD
@LURD - 可能是因为Dr. Seuss的参考 :) - Hugh Jones
4个回答

33

让我带你了解一下这段代码失败的方式以及编译器如何允许你给自己放枪。

如果你使用调试器逐步执行代码,你就可以看到发生了什么。

enter image description here

在初始化Thing1之后,你可以看到FData被填充了所有的零。
奇怪的是,Thing2没问题。
因此错误出现在CreateThing中。 让我们进一步调查...

在名为CreateThing的构造函数中,你会看到以下代码:

FData := @FBuf[1];

看起来像是一个简单的赋值语句,但实际上是对DynArrayAssign函数的调用。

Project97.dpr.32: FData := @FBuf[1];
0042373A 8B45FC           mov eax,[ebp-$04]
0042373D 83C008           add eax,$08
00423743 8B5204           mov edx,[edx+$04]
00423746 42               inc edx
00423747 8B0DE03C4000     mov ecx,[$00403ce0]
0042374D E8E66DFEFF       call @DynArrayAsg      <<-- lots of stuff happening here.  

DynArrayAsg的其中一个检查是检查源动态数组是否为空。
你还需要知道DynArrayAsg会做一些其他的事情。

首先让我们看一下动态数组的结构;它不仅仅是指向数组的简单指针!

Offset 32/64  |   Contents     
--------------+--------------------------------------------------------------
-8/-12        | 32 bit reference count
-4/-8         | 32 or 64 bit length indicator 
 0/ 0         | data of the array.

执行 FData = @FBuf [1] 时,您正在操纵动态数组的前缀字段。
@Fbuf [1] 前面的4个字节被解释为长度。
对于Thing1,这些是:

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 00 00 00  08 00 00 00  00  'S' 'n' .....
FData:    00 00 00 08  00 00 00 00  .............. //Hey that's a zero length.

哎呀,当DynArrayAsg开始检查时,它发现它认为是赋值源的长度为零,即它认为源为空并且没有进行任何分配。它使FData保持不变!

Thing2是否按预期工作?
看起来是这样,但实际上会以相当糟糕的方式失败,让我向您展示。

enter image description here

您已成功欺骗运行时,使其相信@FBuf[1]是对动态数组的有效引用。
由于此原因,FData指针已更新为指向FBuf [1] (到目前为止还好),并且增加了FData的引用计数1次(不好),还将保存动态数组的内存块增长到运行时认为的与FData 正确大小的块中(糟糕)。

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 01 00 00  13 00 00 00  01  'S' 'n' .....
FData:    01 00 00 13  00 00 00 01  'S' .............. 

哎呀,FData现在的引用计数为318,767,105,长度为16,777,216字节。
FBuf的长度也增加了,但其引用计数现在为257。

这就是为什么你需要调用SetLength来撤消过度分配的内存。但这仍无法修复引用计数。
过度分配可能会导致内存错误(尤其是在64位上),而奇怪的引用计数会导致内存泄漏,因为你的数组永远不会被释放。

解决方案
根据David的答案:启用类型检查指针:{$TYPEDADDRESS ON}

你可以通过将FData定义为普通的PAnsiCharPByte来修复代码。 如果确保始终用双零终止对FBuf的赋值,则FData将按预期工作。

像这样将FData变成一个TBuffer

TBuffer = record
private
  FData : PByte;
  function GetLength: cardinal;
  function GetType: byte;
public
  class operator implicit(const A: TBytes): TBuffer;
  class operator implicit(const A: TBuffer): PByte;
  property Length: cardinal read GetLength;
  property DataType: byte read GetType;
end;

CreateThing重写为:

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + Sizeof(AThingType) + 2);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));
  FBuf[Lengh(FBuf)-1]:= 0;
  FBuf[Lengh(FBuf)-2]:= 0;  //trailing zeros for compatibility with pansichar

  FData := FBuf;  //will call the implicit class operator.
end;

class operator TBuffer.implicit(const A: TBytes): TBuffer;
begin
  Result.FData:= PByte(@A[1]);
end;

我不明白为什么要费这么大劲去试图比编译器更聪明。
为什么不直接这样声明FData:

type
  TMyData = record
    DataType: byte;
    Buffer: Ansistring;  
    ....

跟那个一起工作。


它永远不会起作用,他试图将FData作为FBuf中第二个元素的引用。 - whosrdaddy
2
@HughJones,不会的,也不会,稍等我还在处理中。 - Johan
1
@Hugh,对于每个TThing.CreateThing,只要AThingType参数不为0,它似乎都能正常工作,就像Johan在他的回答中所解释的那样。 - fantaghirocco
@fantaghirocco - 明白了。 - Hugh Jones
@david 我不确定该接受哪个答案 - 一个回答信息丰富,另一个则简洁明了;我可以从两个方面进行辩论。 - Hugh Jones
显示剩余7条评论

18
问题可以通过启用类型检查指针轻松地看到。将以下内容添加到您的代码顶部:
{$TYPEDADDRESS ON}

文档中提到:

$T指令控制由@运算符生成的指针值的类型和指针类型的兼容性。

在{$T-}状态下,@运算符的结果始终是一个无类型指针(Pointer),它与所有其他指针类型兼容。当对{$T+}状态下的变量引用应用@时,结果是一个类型化指针,它只与Pointer和指向变量类型的其他指针兼容。

在{$T-}状态下,除了Pointer之外的不同指针类型是不兼容的(即使它们是指向相同类型的指针)。在{$T+}状态下,指向相同类型的指针是兼容的。

如果进行此更改,则您的程序将无法编译。这一行会失败:

FData := @FBuf[1];

错误信息为:
E2010 不兼容的类型:'System.TArray<System.Byte>''Pointer' 现在,FData 的类型是TArray<Byte>,但@FBuf[1]不是动态数组,而是指向动态数组中间一个字节的指针。两者不兼容。通过在默认模式下操作,即指针未经过类型检查,编译器让您犯下了这个可怕的错误。为什么这是默认模式完全超出了我的理解。
动态数组不仅仅是指向第一个元素的指针-还有长度和引用计数等元数据。该元数据存储在距第一个元素的偏移量处。因此,您的整个设计是有缺陷的。将类型代码存储在单独的变量中,而不是作为动态数组的一部分。

正如你所猜测的那样,我的问题源于一个更大的项目,其中我必须“切割”一个更大的TArray<System.Byte>。我必须重新思考我的设计,就像你说的那样。 - Hugh Jones
2
很容易创建一个类,它看起来像是一个数组,但实际上映射到了实际数组的一部分。这个类(或记录)呈现了一个视图。总之,在未来让类型检查指针成为你的朋友! - David Heffernan

6
动态数组在内部是指针,并且与指针兼容; 但是,在赋值的右侧,唯一正确的指针是 nil 或另一个动态数组。显然,FData := @FBuf[1]; 是错误的,但有趣的是,即使启用了 $TYPEDADDRESSFData := @FBuf[0]; 也可能是正确的。
以下代码在 Delphi XE 中编译并按预期工作:
program Project19;

{$APPTYPE CONSOLE}
{$TYPEDADDRESS ON}

uses
  SysUtils;

procedure Test;
var
  A, B: TBytes;

begin
  A:= TBytes.Create(11,22,33);
  B:= @A[0];
  Writeln(B[1]);
end;

begin
  try
    Test;
    readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

看起来编译器“知道”@A[0]是一个动态数组,而不仅仅是一个指针。


在第一个对 B 的赋值语句下面添加 B:= A;,并检查生成的代码。它是相同的。因此编译器确实知道。 - David Heffernan

-1
constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
var
  Buffer : array of Byte;
begin
  SetLength(Buffer, Length(AThingData) + Sizeof(AThingType));
  Buffer[0] := AThingType;
  Move(AThingData[0], Buffer[1], Length(AThingData));

  SetLength(FBuf, Length(Buffer));
  Move(Buffer[0], FBuf[0], Length(Buffer));
  SetLength(FData, Length(AThingData));
  Move(Buffer[1], FData[0], Length(AThingData));
end;

那个方案可行,但我的主要设计考虑是最小化内存复制——这也就是我试图重新引用TBytes部分的原因;在我的实际例子中,ThingData可能会非常巨大。 - Hugh Jones
这个问题是一个“为什么”的问题。提问者想要了解代码为什么会表现出某种行为。这样的问题需要用言语和解释来回答。简短的代码回答很少能够满足要求。这个回答完全没有回答到问题。 - David Heffernan

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