23 June 2010

Initialising dynamic arrays

When writing some unit tests a while ago I found myself needing to initialise some dynamic arrays with test data. It would be nice if we could do something like this:

var
  A: array of Integer;
begin
  A := (1,2,3,4);  // !! WRONG
end;

but we can't. So I decided to write some functions to initialise dynamic arrays to the contents of another array, be it a constant, a literal or another dynamic array.

The result was a set of overloaded routines, one for each data type I needed to handle, for example for Integer and string arrays I had:

function CloneArray(const A: array of Integer): TIntegerDynArray; overload;
var
  Idx: Integer;
begin
  SetLength(Result, Length(A));
  for Idx := Low(A) to High(A) do
    Result[Idx - Low(A)] := A[Idx];
end;

function CloneArray(const A: array of string): TStringDynArray; overload;
var
  Idx: Integer;
begin
  SetLength(Result, Length(A));
  for Idx := Low(A) to High(A) do
    Result[Idx - Low(A)] := A[Idx];
end;

Noticing how similar these functions were - only the first line differs - I decided to try a generic approach. Since I didn't want to parameterise both the type of the array (e.g. the TIntegerDynArray return value) and the array element (e.g. Integer) I first decided to change the return values of the functions to use the TArray<T> type defined in the System unit. So the function prototypes got changed to

function CloneArray(const A: array of Integer): TArray<Integer>; overload;

and

function CloneArray(const A: array of string): TArray<string>; overload;

The implementation of the routines did not need changing. It seemed clear how to parameterise the routines: just replace the Integer or string types with T, like this:

function CloneArray<T>(const A: array of T): TArray<T>;

Wrong! At this point I learned that you can't parameterise global routines, as my blog entry "Being dim #2: No generic global procedures" explains. I needed to make the CloneArray routine a class method of some suitable class. Rather than create a new class I cast around and decided to extend the TArray class defined in the Generics.Collections unit.

type
  TArrayEx = class(TArray)
  public
    class function CloneArray<T>(const A: array of T): TArray<T>;
  end;

...

class function TArrayEx.CloneArray<T>(const A: array of T): TArray<T>;
var
  Idx: Integer;
begin
  SetLength(Result, Length(A));
  for Idx := Low(A) to High(A) do
    Result[Idx - Low(A)] := A[Idx];
end;

Don't confuse Generics.Collections.TArray with System.TArray<T> - the first is a class, the second a generic dynamic array type.

CloneArray assumes that straighforward assignment of array elements is what we want. For example if the array elements are objects, just the object pointer is copied. If you need the object's fields to be physically copied this simplistic approach won't work.

I'll close this post with an example of use. Here's a code fragment that initialises string and integer arrays:

var
  IA: TArray<Integer>;
  SA: TArray<string>;
begin
  IA := TArrayEx.CloneArray<Integer>([1,2,3,5,7,11]);
  SA := TArrayEx.CloneArray<string>(['bonnie', 'clyde']);
end;

5 comments:

Chris said...

WRT your opening question, you can in fact do this:

uses Types;

var
A: TIntegerDynArray;
begin
A := TIntegerDynArray.Create(1,2,3,4);
end;

There's nothing special to TIntegerDynArray - it's just to use a predefined typedef. So, this works as well:

type
TMyIntArray = array of Integer;
var
A: TMyIntArray;
begin
A := TMyIntArray.Create(1,2,3,4);
end;

I think this syntax was added in something like D2005 or D2006.

DelphiDabbler said...

Thanks Chris.

I'd not come across this before. Very handy.

This makes some of this post redundant with one exception. The TArrayEx.CloneArray method works for cloning another dynamic array stored in a variable whereas the Create construct doesn't work in this case.

DelphiDabbler said...

I've also just noticed that this also works:

var
  K: TArray<Integer>;
begin
  K := TArray<Integer>.Create(1, 2, 3);
end;

Chris said...

Actually, now I come to think of it, for cloning, there's the single parameter version of the Copy standard function -

A2 := Copy(A1);

DelphiDabbler said...

One more new one for me! Thanks.