Delphi:如何从 class 中引用数组

Delphi: How to reference an array from within a class

我使用数组并在 class 的上下文中测试了功能,例如:

Ttest   = class
 values : array of integer;
 procedure doStuff;
end;

doStuff这样的方法都对值数组进行操作,而无需将数组作为参数传递。这很适合我,而且速度很快。 现在我想使用这个 class 来处理外部数组,比如 Ttest.create(myValues) 在构造函数中我可以将 myValues 复制到内部 values 但这会很浪费而且,最后必须反转副本才能将更新后的值传回。 我的问题是如何扩展此 class 以便它可以有效地使用外部数组。在这样的伪代码中:

constructor create(var myValues : array of integer);
begin
  address of values := address of myValues;
  doSTuff;
end;

第 1 课

在Delphi中,dynamic arrays是引用类型。一个动态数组类型的变量只包含一个指向实际动态数组堆对象的指针,并且在一个赋值中,

A := B

其中AB是同一类型的动态数组,不复制动态数组堆对象。唯一发生的事情是 AB 将指向同一个动态数组堆对象(即,32 位或 64 位 B 指针被复制到 A) 并且堆对象的 reference count 增加 1.

第 2 课

写的时候

constructor Create(var AValues: array of Integer);

你需要意识到,尽管看起来如此,但这并不是一个动态数组参数,而是一个 open array parameter.

如果你明确想要一个动态数组参数,你需要明确地使用这样的类型:

constructor Create(AValues: TArray<Integer>);

根据定义,TArray<Integer> = array of IntegerInteger 的动态数组。

请注意,该语言只有两种类型数组——静态和动态;开放数组概念仅与函数参数有关。

如果您想使用动态数组,利用它们作为引用类型的特性,我建议您使用动态数组参数。然后唯一传递给函数(在本例中为构造函数)的是指向堆对象的指针。当然,堆对象的引用计数也增加了。

第 3 课 – 示例

var
  Arr: TArray<Integer>;

procedure Test(A: TArray<Integer>);
var
  i: Integer;
begin
  for i := Low(A) to High(A) do
    A[i] := 2*A[i];
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin

  SetLength(Arr, 10);
  for i := 0 to High(Arr) do
    Arr[i] := i;

  Test(Arr);

  for i := 0 to High(Arr) do
    ShowMessage(Arr[i].ToString);

end;

SetLength之后,Arr指向一个引用计数为1的动态数组堆对象,你可以在你的RAM中看到它(按Ctrl+ Alt+E,然后 Ctrl+G 然后转到Arr[0])。当您输入 Test 时,引用计数增加到 2,因为 ArrA 都引用了它。当您离开 Test 时,引用计数将减少回 1,因为 A 超出范围:现在再次只有 Arr 引用它。

第 4 课

现在,一个“陷阱”:如果您更改动态数组的元素数量,则需要重新分配它 (*)。因此,创建了一个引用计数为 1 的新动态数组堆对象,旧对象的引用计数减 1(如果变为零则删除)。

因此,虽然前面的示例按预期工作,但以下示例不会:

var
  Arr: TArray<Integer>;

procedure Test(A: TArray<Integer>);
var
  i: Integer;
begin
  SetLength(A, 5);
  for i := Low(A) to High(A) do
    A[i] := 2*A[i];
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin

  SetLength(Arr, 10);
  for i := 0 to High(Arr) do
    Arr[i] := i;

  Test(Arr);

  for i := 0 to High(Arr) do
    ShowMessage(Arr[i].ToString);

end;

SetLength会新建一个refcount为1的动态数组堆对象,将旧数组的一半复制进去,把新地址放在本地A参数中,Test 将转换这个新数组,而不触及全局 Arr 变量指向的旧数组。原堆对象的引用计数减一

但是,如果您使用 var 参数,

procedure Test(var A: TArray<Integer>);
var
  i: Integer;
begin
  SetLength(A, 5);
  for i := Low(A) to High(A) do
    A[i] := 2*A[i];
end;

它将像以前一样工作。这有效地适用于 Arr。如果我们增加了元素的数量,数组可能会被重新分配并且全局 Arr 变量会被更新为新地址。

结论

只要你不需要重新分配内存(改变元素的数量),Delphi已经给了你想要的,因为动态数组是引用类型。如果您确实需要重新分配,至少现在您了解足够的技术细节以便进行推理。

更新: 所以建议做

type
  TTest = class
    FData: TArray<Integer>;
    constructor Create(AData: TArray<Integer>);
    procedure Enlarge;
    procedure Shrink;
    procedure ShowSum;
  end;

{ TTest }

constructor TTest.Create(AData: TArray<Integer>);
begin
  FData := AData; // will NOT copy the array, since dynamic arrays are reference types
end;

procedure TTest.Enlarge;
var
  i: Integer;
begin
  for i := 0 to High(FData) do
    FData[i] := 2*FData[i];
end;

procedure TTest.ShowSum;
var
  s: Integer;
  i: Integer;
begin
  s := 0;
  for i := 0 to High(FData) do
    Inc(s, FData[i]);
  ShowMessage(s.ToString);
end;

procedure TTest.Shrink;
var
  i: Integer;
begin
  for i := 0 to High(FData) do
    FData[i] := FData[i] div 2;
end;

测试一下:

procedure TForm1.FormCreate(Sender: TObject);
var
  MyArray: TArray<Integer>;
  t: TTest;
begin

  MyArray := [1, 2, 3, 4, 5];

  t := TTest.Create(MyArray);
  try
    t.ShowSum;
    t.Enlarge;
    t.ShowSum;
    t.Shrink;
    t.ShowSum;
  finally
    t.Free;
  end;

end;

脚注

  • 如果您 (1) 减少元素的数量并且 (2) 引用计数为 1,通常数据不会在内存中移动。如果引用计数 > 1,数据总是被移动,因为 SetLength 保证它的参数的引用计数是 1 当它 returns.