user3212191
user3212191

Reputation: 547

Delphi: How to reference an array from within a class

I work with arrays and have tested the functionality within the context of a class, like:

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

The methods, like doStuff all operate on the values array without the need to pass the array as a parameter. This suits me and it is fast. Now I want to use this class to work with an external array, like Ttest.create(myValues) In the constructor I could copy myValues to the internal values but that would be quite a waste and moreover, at the end the copy would have to be reversed to pass the updated values back. My question is how can I extend this class so that it can efficiently work with an external array. In pseudo code like this:

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

Upvotes: 2

Views: 1229

Answers (1)

Andreas Rejbrand
Andreas Rejbrand

Reputation: 109003

Lesson 1

In Delphi, dynamic arrays are reference types. A variable of dynamic array type contains only a pointer to the actual dynamic array heap object, and in an assignment,

A := B

where A and B are dynamic arrays of the same type, the dynamic array heap object isn't copied. The only thing that happens is that A and B will point to the same dynamic array heap object (that is, the 32- or 64-bit B pointer is copied to A) and that the reference count of the heap object is increased by 1.

Lesson 2

When you write

constructor Create(var AValues: array of Integer);

you need to realise that this, despite the appearance, isn't a dynamic array parameter, but an open array parameter.

If you explicitly want a dynamic array parameter, you need to use such a type explicitly:

constructor Create(AValues: TArray<Integer>);

By definition, TArray<Integer> = array of Integer is a dynamic array of Integers.

Please note that the language only has two types of arrays – static and dynamic; the open array concept is only about function parameters.

If you want to work with dynamic arrays, taking advantage of their nature as reference types, I would suggest you use dynamic array parameters. Then the only thing that is passed to the function (the constructor in this case) is the pointer to the heap object. And the heap object's reference count is increased, of course.

Lesson 3 – An example

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;

After SetLength, Arr points to a dynamic array heap object of reference count 1. You can see it in your RAM (press Ctrl+Alt+E, then Ctrl+G and goto Arr[0]). When you enter Test, the reference count is increased to 2 because both Arr and A refer to it. When you leave Test, the reference count is reduced back to 1 because A goes out of scope: now again only Arr refers to it.

Lesson 4

Now, a “gotcha”: if you change the number of elements of a dynamic array, it needs to be reallocated (*). Hence, a new dynamic array heap object is created with a reference count of 1, and the old object has its reference count decreased by 1 (and removed if it becomes zero).

Thus, while the previous example works as expected, the following will not:

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;

The SetLength will create a new dynamic array heap object with refcount 1 and copy half of the old array into this, put the new address in the local A parameter, and Test will transform this new array, not touching the old one which the global Arr variable points to. The reference count of the original heap object is decreased by one.

However, if you use a var parameter,

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;

it will work as before. This effectively works with Arr. If we had increased the number of elements, the array would probably have been reallocated and the global Arr variable would have been updated with the new address.

Conclusion

As long as you don't need to reallocate the memory (change the number of elements), Delphi already gives you what you want, because dynamic arrays are reference types. If you do need to reallocate, at least now you know enough of the technical details in order to reason about it.

Update: Hence, the suggestion is to do

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;

To test it:

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;

Footnotes

  • If you (1) decrease the number of elements and (2) the reference count is 1, typically the data isn't moved in memory. If the reference count is > 1, the data is always moved, because SetLength guarantees that the reference count of its argument is 1 when it returns.

Upvotes: 9

Related Questions