分享

Open array parameters

 fssky 2011-03-01

Open array parameters and array of const

This article describes the syntax and use of open array parameters, and the use of the "array of const" parameter type. It also describes the internals of these two similar types of parameters, discusses lifetime issues, and gives a code solution for these issues. It has a short discusson on the confusion between open arrays and dynamic arrays, and between Variant arrays and variant open arrays.

to 
topOpen array parameters

Open array parameters are special parameters which allow you to write procedures or functions (I will use the word routines, if I mean both) that can act on any array of the same base type, regardless of its size. To declare an open array parameter, you use a syntax like this:

  procedure ListAllIntegers(const AnArray: array of Integer);
var
I: Integer;
begin
for I := Low(AnArray) to High(AnArray) do
WriteLn('Integer at ', I, ' is ', AnArray[I]);
end;

You can call this procedure with any one-dimensional array of Integers, so you can call it with an array[0..1] of Integer as well as with an array[42..937] of Integer, or even a dynamic type array of Integer.

The code also demonstrates how you can determine the size of the array in the routine. Delphi knows the pseudo-functions Low and High. They are not real functions, they are just syntactic items in Delphi that take the form of a function, but actually rely on the compiler to substitute them for code. Low gives the lower bound of an array, and High the upper bound. You can also use Length, which returns the number of elements of the array.

But if you call the code with an array that is not zero-based, like for instance in the following (nonsense) example,

  var
NonZero: array[7..9] of Integer;
begin
NonZero[7] := 17;
NonZero[8] := 325;
NonZero[9] := 11;
ListAllIntegers(NonZero);
end.

you will see that the output is like this:

  Integer at 0 is 17
Integer at 1 is 325
Integer at 2 is 11

That is because inside the procedure or function, the array is always seen as a zero based array. So for an open array parameter, Low is always 0, and High is adjusted accordingly (note that this is not necessarily true for other uses of High and Low, i.e. not on open array parameters). For open arrays, Length is always High + 1.

to topSlice

If you don't want to use an entire array, but only a part of it, you can do that using the Slice pseudo-function. It is only allowed where an open array parameter is declared. It is used in this fashion:

  const
Months: array[1..12] of Integer = (31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31);
begin
ListAllIntegers(Slice(Months, 6));
end;

That will only display the first 6 values of the array, not all 12.

to topInternals

But how does that work; how can the function know the size of the array? It is quite simple. An open array parameter is actually a combination of two parameters, a pointer, which contains the address of the start of the array, and an integer, which contains the High value, adjusted for a zero base. So in fact the real parameter list of the procedure is something like this:

  procedure ListAllIntegers(const AnArray: Pointer; High: Integer);

Each time you pass an array to an open array parameter, the compiler, which knows the size of the array, will pass its address and its adjusted High value to the procedure or function. For arrays of a static size, like array[7..9] of Integer, it uses the declared size to pass the High value; for dynamic arrays, it compiles code to get the High value of the array at runtime.

Usually, you can pass open arrays as const parameters. Open array parameters that are not passed as const will entirely be copied into local storage of the routine. The array is simply passed by reference, but if it is not declared const, the hidden start code of the routine will allocate room on the stack and copy the entire array to that local storage, using the reference as source address. For large arrays, this can be very inefficient. So if you don't need to modify items in the array locally, make the open array parameter const.

to 
topOpen array constructors

Sometimes you don't want to declare and fill an array just so you can use it with an open array parameter. Luckily, Delphi allows you to declare open arrays on the spot, using the so called open array constructor syntax, which uses [ and ] to define the array. The above example with the NonZero array could also have been written like this:

  ListAllIntegers([17, 325, 11]);

Here, the array is defined on the spot as [17, 325, 11]. The compiler ensures that the array exists during the call of the procedure, and it passes the correct High value. This is totally transparent to the code inside the procedure. After the call, the array is discarded.

to 
topConfusion

Although the syntax is unfortunately very similar, an open array parameter should not be confused with a Delphi dynamic array. A dynamic array is an array that is maintained by Delphi, and of which you can change the size using SetLength. It is declared like:

  type
TIntegerArray = array of Integer;

Unfortunately, this looks a lot like the syntax used for open array parameters. But they are not the same. An open array parameter will accept dynamic arrays like array of Month, but also static arrays like array[0..11] of Month. So in a function with an open array parameter, you can't call SetLength on the parameter. If you really only want to pass dynamic arrays, you'll have to declare them separately, and use the type name as parameter type.

  type
TMonthArray = array of Month;
procedure AllKinds(const Arr: array of Month);
procedure OnlyDyn(Arr: TMonthArray);

Procedure AllKinds will accept static arrays as well as dynamic arrays, so SetLength can't be used, since static arrays can't be reallocated. Procedure OnlyDyn will only accept dynamic arrays, so you can use SetLength here (this will however use a copy, and not change the original array; if you want to change the length of the original array, use var Arr: TMonthArray in the declaration).

Note: You should not forget that in Delphi, parameters can ony be declared with type specifications, and not with type declarations. So the following formal parameters, which would be type declarations, are not possible:

  procedure Sum(const Items: array[1..7] of Integer);
function MoveTo(Spot: record X, Y: Integer; end);

You'll have to declare a type first, and use the specifications as parameter type:

  type
TWeek = array[1..7] of Integer;
TSpot = record
X, Y: Integer;
end;
procedure Sum(const Items: TWeek);
function MoveTo(Spot: TSpot);

That is why array of Something in a parameter list can't be a type declaration for a dynamic array either. It is always an open array declaration.

to topArray of const

Array of const is a special case of open arrays. Instead of passing only one type, you can pass a variety of types. Have a look at the declaration of the Format function in Delphi:

  function Format(const Format: string; const Args: array of const): string;

(Actually, there is a second, overloaded function in some versions of Delphi, but I'll simply ignore that here.)

The first parameter is a string which indicates how you want your values formatted, and the second is an array of const, so you can pass a range of values using a similar syntax as the one for open array constructors. So you can call it like:

  var
Res: string;
Int: Integer;
Dub: Double;
Str: string;
begin
Int := Random(1000);
Dub := Random * 1000;
Str := 'Teletubbies';
Res := Format('%4d %8.3f %s', [Int, Dub, Str]);
end;

Note: The official name for array of const parameters is variant open array parameters. This should not be confused with the Variant type in Delphi, and the arrays it can contain. They are quite different, even though a TVarRec (see below) is a bit similar to how a Variant is internally stored. Even the name of the internal record of a Variant is confusingly similar: TVarData.

to 
topInternals

Internally, an array of const is an open array of TVarRec. The declaration of TVarRec is given in the online help for Delphi. It is a variant record (also not to be confused with the Variant type), that contains a field called VType, and an overlay of different types, some of which are only pointers. The compiler creates a TVarRec for each member in the open array constructor, fills the VType field with the type of the member, and places the value, or a pointer to it, in one of the other fields. Then it passes the array of TVarRec to the function.

Since each TVarRec contains type information, Format can use this to check if it matches with the type given in the format string. That is why you get a runtime error when passing a wrong type. You can tell the compiler that you want it to store a different type identifier, by casting to the desired type. If the type doesn't match one of the allowed types in a TVarRec exactly, the compiler will try to convert it to a matching type, so if you pass a Double, it will convert it to an Extended and pass that instead. Of course there are limitations on what the compiler can do, so for instance passing an object isn't going to work.

Inside the function or procedure, you can treat the members as TVarRec immediately. The help for Delphi gives an example how to do this.

to topLifetime issues

What you should notice is, that values in the TVarRec which are passed as pointers only exist during the course of the function or procedure. As soon as the routine ends, these values don't exist anymore. So you should not be tempted to return these pointers from the procedure or function, or to store the TVarRecs in an array outside the routine, unless you can make sure that you manage the values yourself.

If you must copy the TVarRecs to an array or variable outside the function (this can also be a var parameter), be sure to make a copy (i.e. on the heap) of the value, and replace the pointer in the TVarRec with one to the copy. You should also take care that the copy is disposed of when it is not needed anymore. An example follows:

Download
type
TConstArray = array of TVarRec;
// Copies a TVarRec and its contents. If the content is referenced
// the value will be copied to a new location and the reference
// updated.
function CopyVarRec(const Item: TVarRec): TVarRec;
var
W: WideString;
begin
// Copy entire TVarRec first.
Result := Item;
// Now handle special cases.
case Item.VType of
vtExtended:
begin
New(Result.VExtended);
Result.VExtended^ := Item.VExtended^;
end;
...
    vtPChar:
Result.VPChar := StrNew(Item.VPChar);
      ...
// A little trickier: casting to AnsiString will ensure
// reference counting is done properly.
vtAnsiString:
begin
// Nil out first, so no attempt to decrement
// reference count.
Result.VAnsiString := nil;
AnsiString(Result.VAnsiString) := AnsiString(Item.VAnsiString);
end; 
...
// VPointer and VObject don't have proper copy semantics so it
// is impossible to write generic code that copies the contents.
...
end;
end;
// Creates a TConstArray out of the values given. Uses CopyVarRec
// to make copies of the original elements.
function CreateConstArray(const Elements: array of const): TConstArray;
var
I: Integer;
begin
SetLength(Result, Length(Elements));
for I := Low(Elements) to High(Elements) do
Result[I] := CopyVarRec(Elements[I]);
end;
// TVarRecs created by CopyVarRec must be finalized with this function.
// You should not use it on other TVarRecs.
procedure FinalizeVarRec(var Item: TVarRec);
begin
case Item.VType of
vtExtended: Dispose(Item.VExtended);
vtString: Dispose(Item.VString);
      ...
end;
Item.VInteger := 0;
end;
// A TConstArray contains TVarRecs that must be finalized. This function
// does that for all items in the array.
procedure FinalizeVarRecArray(var Arr: TConstArray);
var
I: Integer;
begin
for I := Low(Arr) to High(Arr) do
FinalizeVarRec(Arr[I]);
Arr := nil;
end;

The functions above can help you manage TVarRecs outside the routine for which they were constructed. You can even use a TConstArray where an open array is declared. The following little program

  program VarRecTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
VarRecUtils in 'VarRecUtils.pas';
var
ConstArray: TConstArray;
begin
ConstArray := CreateConstArray([1, 'Hello', 7.9, IntToStr(1234)]);
try
WriteLn(Format('%d --- %s --- %0.2f --- %s', ConstArray));
Writeln(Format('%s --- %0.2f', Copy(ConstArray, 1, 2)));
finally
FinalizeConstArray(ConstArray);
end;
ReadLn;
end.

will produce the expected, but not very exciting, output

1 --- Hello --- 7.90 --- 1234
Hello --- 7.90

The little program above also demonstrates how you can use Copy to use only a part of the entire TConstArray. Copy will create a copy of the dynamic array, but not copy the contents, so you should not try to use Copy and then later on use FinalizeConstArray on that copy. In the program above, the copy will be removed automatically, since the lifetime of the copy is managed by the compiler.

to topFinally

Open arrays and arrays of const are powerful features of the language, but they come with a few caveats. I hope I succeeded in showing some of these, and how you can overcome them.

Questions regarding open arrays, array of const and many other language issues can be discussed in the Borland newsgroups (news://newsgroups.borland.com), especially the following ones:

These groups are valuable sources of information for all Delphi language questions for Win32. Note that I did not mention the newsgroup for .NET, since on .NET, things are a bit different.

Rudy Velthuis

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多