Structured Types (Delphi)

From RAD Studio
Jump to: navigation, search

Go Up to Data Types, Variables, and Constants Index

Instances of a structured type hold more than one value. Structured types include sets, arrays, records, and files as well as class, class-reference, and interface types. Except for sets, which hold ordinal values only, structured types can contain other structured types; a type can have unlimited levels of structuring.

This topic covers the following structured types:

  • Sets
  • Arrays, including static and dynamic arrays
  • Records
  • File types

Alignment of Structured Types

By default, the values in a structured type are aligned on word- or double-word boundaries for faster access.

You can, however, specify byte alignment by including the reserved word packed when you declare a structured type. The packed word specifies compressed data storage. Here is an example declaration:

 type TNumbers = packed array [1..100] of Real;

Using packed is not a recommended practice, because it can prevent compatibility with other languages or platforms, it slows data access, and, in the case of a character array, it affects type compatibility. For more information, see Memory management and Implicit Packing of Fields with a Common Type Specification.

Sets

A set is a collection of values of the same ordinal type. The values have no inherent order, nor is it meaningful for a value to be included twice in a set.

The range of a set type is the power set of a specific ordinal type, called the base type; that is, the possible values of the set type are all the subsets of the base type, including the empty set. The base type can have no more than 256 possible values, and their ordinalities must fall between 0 and 255. Any construction of the form:

set of baseType

where baseType is an appropriate ordinal type, identifies a set type.

Because of the size limitations for base types, set types are usually defined with subranges. For example, the declarations:

 type
   TSomeInts = 1..250;
   TIntSet = set of TSomeInts;

create a set type called TIntSet whose values are collections of integers in the range from 1 to 250. You could accomplish the same thing with:

 type TIntSet = set of 1..250;

Given this declaration, you can create a set like this:

 var Set1, Set2: TIntSet;
     ...
     Set1 := [1, 3, 5, 7, 9];
     Set2 := [2, 4, 6, 8, 10]

You can also use the set of ... construction directly in variable declarations:

 var MySet: set of 'a'..'z';
     ...
     MySet := ['a','b','c'];
Note: For more information, see the following warning message: W1050 WideChar reduced to byte char in set expressions (Delphi).

Other examples of set types include:

set of Byte
set of (Club, Diamond, Heart, Spade)
set of Char;

The in operator tests set membership:

 if 'a' in MySet then ... { do something } ;

Every set type can hold the empty set, denoted by []. For more information about sets, see "Set Constructors" and "Set Operators" in Expressions (Delphi).

Arrays

An array represents an indexed collection of elements of the same type (called the base type). Because each element has a unique index, arrays, unlike sets, can meaningfully contain the same value more than once. Arrays can be allocated statically or dynamically.

Static Arrays

Static array types are denoted by constructions of the form:

array[indexType1, ..., indexTypen] of baseType;

where each indexType is an ordinal type whose range does not exceed 2GB. Since the indexTypes index the array, the number of elements an array can hold is limited by the product of the sizes of the indexTypes. In practice, indexTypes are usually integer subranges.

In the simplest case of a one-dimensional array, there is only a single indexType. For example:

 var MyArray: array [1..100] of Char;

declares a variable called MyArray that holds an array of 100 character values. Given this declaration, MyArray[3] denotes the third character in MyArray. If you create a static array but don't assign values to all its elements, the unused elements are still allocated and contain random data; they are like uninitialized variables.

A multidimensional array is an array of arrays. For example:

 type TMatrix = array[1..10] of array[1..50] of Real;

is equivalent to:

 type TMatrix = array[1..10, 1..50] of Real;

Whichever way TMatrix is declared, it represents an array of 500 real values. A variable MyMatrix of type TMatrix can be indexed like this: MyMatrix[2,45]; or like this: MyMatrix[2][45]. Similarly:

 packed array[Boolean, 1..10, TShoeSize] of Integer;

is equivalent to:

 packed array[Boolean] of packed array[1..10] of packed array[TShoeSize] of Integer;

The standard functions Low and High operate on array type identifiers and variables. They return the low and high bounds of the array's first index type. The standard function Length returns the number of elements in the array's first dimension.

A one-dimensional, packed, static array of Char values is called a packed string. Packed-string types are compatible with string types and with other packed-string types that have the same number of elements. See Type Compatibility and Identity (Delphi).

An array type of the form array[0..x] of Char is called a zero-based character array. Zero-based character arrays are used to store null-terminated strings and are compatible with PChar values. See "Working with null-terminated strings" in String Types (Delphi).

Dynamic Arrays

Dynamic arrays do not have a fixed size or length. Instead, memory for a dynamic array is reallocated when you assign a value to the array or pass it to the SetLength procedure. Dynamic-array types are denoted by constructions of the form:

array of baseType

For example:

 var MyFlexibleArray: array of Real;

declares a one-dimensional dynamic array of reals. The declaration does not allocate memory for MyFlexibleArray. To create the array in memory, call SetLength. For example, given the previous declaration:

 SetLength(MyFlexibleArray, 20);

allocates an array of 20 reals, indexed 0 to 19. An alternative method of allocating memory for dynamic arrays is to invoke the array constructor:

 type
   TMyFlexibleArray = array of Integer;
 
 begin
   MyFlexibleArray := TMyFlexibleArray.Create(1, 2, 3 {...});
 end;

which allocates memory for three elements and assigns each element the given value.

Similar to the array constructor, a dynamic array may also be initialized from an array constant expression as below.

  procedure MyProc;
  var
    A: array of Integer;
  begin
    A := [1, 2, 3];
  end;

Notice that unlike with an array constructor, an array constant can be applied to unnamed dynamic array type directly. This syntax is specific to dynamic arrays; applying this technique to other array types is likely to result in the constant being interpreted as a set, leading to an incompatible types error at compile-time.

Dynamic arrays are always integer-indexed, always starting from 0.

Dynamic-array variables are implicitly pointers and are managed by the same reference-counting technique used for long strings. To deallocate a dynamic array, assign nil to a variable that references the array or pass the variable to Finalize; either of these methods disposes of the array, provided there are no other references to it. Dynamic arrays are automatically released when their reference-count drops to zero. Dynamic arrays of length 0 have the value nil. Do not apply the dereference operator (^) to a dynamic-array variable or pass it to the New or Dispose procedure.

If X and Y are variables of the same dynamic-array type, X := Y points X to the same array as Y. (There is no need to allocate memory for X before performing this operation.) Unlike strings and static arrays, copy-on-write is not employed for dynamic arrays, so they are not automatically copied before they are written to. For example, after this code executes:

 var
   A, B: array of Integer;
   begin
     SetLength(A, 1);
     A[0] := 1;
     B := A;
     B[0] := 2;
   end;

the value of A[0] is 2. (If A and B were static arrays, A[0] would still be 1.)

Assigning to a dynamic-array index (for example, MyFlexibleArray[2] := 7) does not reallocate the array. Out-of-range indexes are not reported at compile time.

In contrast, to make an independent copy of a dynamic array, you must use the global Copy function:

 var
   A, B: array of Integer;
 begin
   SetLength(A, 1);
   A[0] := 1;
   B := Copy(A);
   B[0] := 2; { B[0] <> A[0] }
 end;

When dynamic-array variables are compared, their references are compared, not their array values. Thus, after execution of the code:

 var
   A, B: array of Integer;
 begin
    SetLength(A, 1);
    SetLength(B, 1);
    A[0] := 2;
    B[0] := 2;
 end;

A = B returns False but A[0] = B[0] returns True.

To truncate a dynamic array, pass it to SetLength, or pass it to Copy and assign the result back to the array variable. (The SetLength procedure is usually faster.) For example, if A is a dynamic array, either of the following truncates all but the first 20 elements of A:

 SetLength(A, 20)
 A := Copy(A, 0, 20)

Once a dynamic array has been allocated, you can pass it to the standard functions Length, High, and Low. Length returns the number of elements in the array, High returns the array's highest index (that is, Length - 1), and Low returns 0. In the case of a zero-length array, High returns -1 (with the anomalous consequence that High < Low).

Note: In some function and procedure declarations, array parameters are represented as array of baseType, without any index types specified. For example,function CheckStrings(A: array of string): Boolean;

This indicates that the function operates on all arrays of the specified base type, regardless of their size, how they are indexed, or whether they are allocated statically or dynamically.

Multidimensional Dynamic Arrays

To declare multidimensional dynamic arrays, use iterated array of ... constructions. For example:

 type TMessageGrid = array of array of string;
 var Msgs: TMessageGrid;

declares a two-dimensional array of strings. To instantiate this array, call SetLength with two integer arguments. For example, if I and J are integer-valued variables:

 SetLength(Msgs,I,J);

allocates an I-by-J array, and Msgs[0,0] denotes an element of that array.

You can create multidimensional dynamic arrays that are not rectangular. The first step is to call SetLength, passing it parameters for the first n dimensions of the array. For example:

 var Ints: array of array of Integer;
 SetLength(Ints,10);

allocates ten rows for Ints but no columns. Later, you can allocate the columns one at a time (giving them different lengths); for example:

 SetLength(Ints[2], 5);

makes the third column of Ints five integers long. At this point (even if the other columns haven't been allocated) you can assign values to the third column - for example, Ints[2,4] := 6.

The following example uses dynamic arrays (and the IntToStr function declared in the SysUtils unit) to create a triangular matrix of strings.

 var
   A : array of array of string;
   I, J : Integer;
 begin
   SetLength(A, 10);
   for I := Low(A) to High(A) do
   begin
     SetLength(A[I], I);
     for J := Low(A[I]) to High(A[I]) do
       A[I,J] := IntToStr(I) + ',' + IntToStr(J) + ' ';
     end;
   end;

Array Types and Assignments

Arrays are assignment-compatible only if they are of the same type. Because the Delphi language uses name-equivalence for types, the following code will not compile.

 var
   Int1: array[1..10] of Integer;
   Int2: array[1..10] of Integer;
       ...
   Int1 := Int2;

To make the assignment work, declare the variables as:

 var Int1, Int2: array[1..10] of Integer;

or:

 type IntArray = array[1..10] of Integer;
 var
    Int1: IntArray;
    Int2: IntArray;

String-Like Operations Supported on Dynamic Arrays

Dynamic arrays can be manipulated similarly to strings. For example:

var
  A: array of integer;
  B: TBytes = [1,2,3,4]; //Initialization can be done from declaration
begin
  ...
  A:=[1,2,3]; // assignation using constant array
  A:=A+[4,5]; // addition - A will become [1,2,3,4,5]
  ...
end;

String-like Support Routines

Some of the Delphi Intrinsic Routines support operations on dynamic arrays in addition to operations on strings.

System.Insert The Insert function inserts a dynamic array at the beginning at the position index. It returns the modified array:

  
var
  A: array of integer;
begin
  ...
  A:=[1,2,3,4];
  Insert(5,A,2); // A will become [1,2,5,3,4]
  ...
end;

System.Delete

The Delete function eliminates elements from a dynamic array and returns the modified array:

  
var
  A: array of integer;
begin
  ...
  A:=[1,2,3,4];
  Delete(A,1,2); //A will become [1,4]
  ...
end;

System.Concat The Concat function can be used to put together two different dynamic arrays:

  
  A := Concat([1,2,3],[4,5,6]); //A will become [1,2,3,4,5,6]

Records (traditional)

A record (analogous to a structure in some languages) represents a heterogeneous set of elements. Each element is called a field; the declaration of a record type specifies a name and type for each field. The syntax of a record type declaration is:

 type recordTypeName = record
   fieldList1: type1;
    ...
   fieldListn: typen;
 end

where recordTypeName is a valid identifier, each type denotes a type, and each fieldList is a valid identifier or a comma-delimited list of identifiers. The final semicolon is optional.

For example, the following declaration creates a record type called TDateRec.

 type
   TDateRec = record
     Year: Integer;
     Month: (Jan, Feb, Mar, Apr, May, Jun,
             Jul, Aug, Sep, Oct, Nov, Dec);
     Day: 1..31;
   end;

Each TDateRec contains three fields: an integer value called Year, a value of an enumerated type called Month, and another integer between 1 and 31 called Day. The identifiers Year, Month, and Day are the field designators for TDateRec, and they behave like variables. The TDateRec type declaration, however, does not allocate any memory for the Year, Month, and Day fields; memory is allocated when you instantiate the record, like this:

 var Record1, Record2: TDateRec;

This variable declaration creates two instances of TDateRec, called Record1 and Record2.

You can access the fields of a record by qualifying the field designators with the record's name:

 Record1.Year := 1904;
 Record1.Month := Jun;
 Record1.Day := 16;

Or use a with statement:

 with Record1 do
 begin
   Year := 1904;
   Month := Jun;
   Day := 16;
 end;

You can now copy the values of Record1's fields to Record2:

 Record2 := Record1;

Because the scope of a field designator is limited to the record in which it occurs, you don't have to worry about naming conflicts between field designators and other variables.

Instead of defining record types, you can use the record ... construction directly in variable declarations:

 var S: record
   Name: string;
   Age: Integer;
 end;

However, a declaration like this largely defeats the purpose of records, which is to avoid repetitive coding of similar groups of variables. Moreover, separately declared records of this kind will not be assignment-compatible, even if their structures are identical.

Variant Parts in Records

A record type can have a variant part, which looks like a case statement. The variant part must follow the other fields in the record declaration.

To declare a record type with a variant part, use the following syntax:

 type recordTypeName = record
   fieldList1: type1;
    ...
   fieldListn: typen;
 case tag: ordinalType of
   constantList1: (variant1);
    ...
   constantListn: (variantn);
 end;

The first part of the declaration - up to the reserved word case - is the same as that of a standard record type. The remainder of the declaration - from case to the optional final semicolon - is called the variant part. In the variant part,

  • tag is optional and can be any valid identifier. If you omit tag, omit the colon (:) after it as well.
  • ordinalType denotes an ordinal type.
  • Each constantList is a constant denoting a value of type ordinalType, or a comma-delimited list of such constants. No value can be represented more than once in the combined constantLists.
  • Each variant is a semicolon-delimited list of declarations resembling the fieldList: type constructions in the main part of the record type. That is, a variant has the form:
 fieldList1: type1;
   ...
 fieldListn: typen;

where each fieldList is a valid identifier or comma-delimited list of identifiers, each type denotes a type, and the final semicolon is optional. The types must not be long strings, dynamic arrays, variants (that is, Variant types), or interfaces, nor can they be structured types that contain long strings, dynamic arrays, variants, or interfaces; but they can be pointers to these types.

Records with variant parts are complicated syntactically but deceptively simple semantically. The variant part of a record contains several variants which share the same space in memory. You can read or write to any field of any variant at any time; but if you write to a field in one variant and then to a field in another variant, you may be overwriting your own data. The tag, if there is one, functions as an extra field (of type ordinalType) in the non-variant part of the record.

Variant parts have two purposes. First, suppose you want to create a record type that has fields for different kinds of data, but you know that you will never need to use all of the fields in a single record instance. For example:

 type
   TEmployee = record
   FirstName, LastName: string[40];
   BirthDate: TDate;
   case Salaried: Boolean of
     True: (AnnualSalary: Currency);
     False: (HourlyWage: Currency);
 end;

The idea here is that every employee has either a salary or an hourly wage, but not both. So when you create an instance of TEmployee, there is no reason to allocate enough memory for both fields. In this case, the only difference between the variants is in the field names, but the fields could just as easily have been of different types. Consider some more complicated examples:

 type
   TPerson = record
   FirstName, LastName: string[40];
   BirthDate: TDate;
   case Citizen: Boolean of
     True: (Birthplace: string[40]);
     False: (Country: string[20];
             EntryPort: string[20];
             EntryDate, ExitDate: TDate);
   end;
 
 type
   TShapeList = (Rectangle, Triangle, Circle, Ellipse, Other);
   TFigure = record
     case TShapeList of
       Rectangle: (Height, Width: Real);
       Triangle: (Side1, Side2, Angle: Real);
       Circle: (Radius: Real);
       Ellipse, Other: ();
   end;

For each record instance, the compiler allocates enough memory to hold all the fields in the largest variant. The optional tag and the constantLists (like Rectangle, Triangle, and so forth in the last example) play no role in the way the compiler manages the fields; they are there only for the convenience of the programmer.

The second reason for variant parts is that they let you treat the same data as belonging to different types, even in cases where the compiler would not allow a typecast. For example, if you have a 64-bit Real as the first field in one variant and a 32-bit Integer as the first field in another, you can assign a value to the Real field and then read back the first 32 bits of it as the value of the Integer field (passing it, say, to a function that requires integer parameters).

Records (advanced)

In addition to the traditional record types, the Delphi language allows more complex and "class-like" record types. In addition to fields, records may have properties and methods (including constructors), class properties, class methods, class fields, and nested types. For more information on these subjects, see the documentation on Classes and Objects (Delphi). Here is a sample record type definition with some "class-like" functionality.

 type
   TMyRecord = record
     type
       TInnerColorType = Integer;
     var
       Red: Integer;
     class var
       Blue: Integer;
     procedure printRed();
     constructor Create(val: Integer);
     property RedProperty: TInnerColorType read Red write Red;
     class property BlueProp: TInnerColorType read Blue write Blue;
 end;
 
 constructor TMyRecord.Create(val: Integer);
 begin
   Red := val;
 end;
 
 procedure TMyRecord.printRed;
 begin
   Writeln('Red: ', Red);
 end;

Though records can now share much of the functionality of classes, there are some important differences between classes and records.

  • Records do not support inheritance.
  • Records can contain variant parts; classes cannot.
  • Records are value types, so they are copied on assignment, passed by value, and allocated on the stack unless they are declared globally or explicitly allocated using the New and Dispose function. Classes are reference types, so they are not copied on assignment, they are passed by reference, and they are allocated on the heap.
  • Records allow operator overloading on the Win32 platform; classes, however, do not allow operator overloading.
  • Records are constructed automatically, using a default no-argument constructor, but classes must be explicitly constructed. Because records have a default no-argument constructor, any user-defined record constructor must have one or more parameters.
  • Record types cannot have destructors.
  • Virtual methods (those specified with the virtual, dynamic, and message keywords) cannot be used in record types.
  • Unlike classes, record types on the Win32 platform cannot implement interfaces.

File Types (Win32)

File types, as available on the Win32 platform, are sequences of elements of the same type. Standard I/O routines use the predefined TextFile or Text type, which represents a file containing characters organized into lines. For more information about file input and output, see Standard Routines and Input-Output under the "File Input and Output" section.

To declare a file type, use the syntax:

type fileTypeName = file of type

where fileTypeName is any valid identifier and type is a fixed-size type. Pointer types - whether implicit or explicit - are not allowed, so a file cannot contain dynamic arrays, long strings, classes, objects, pointers, variants, other files, or structured types that contain any of these.

For example:

 type
    PhoneEntry = record
      FirstName, LastName: string[20];
      PhoneNumber: string[15];
      Listed: Boolean;
    end;
    PhoneList = file of PhoneEntry;

declares a file type for recording names and telephone numbers.

You can also use the file of ... construction directly in a variable declaration. For example,

 var List1: file of PhoneEntry;

The word file by itself indicates an untyped file:

 var DataFile: file;

For more information, see "Untyped Files" in Standard Routines and Input-Output.

Files are not allowed in arrays or records.

See Also

Code Samples