Records
To wrap up the analysis of built-in data types, we have to discuss structured types that can store elements of different types—namely records, classes, and interfaces.
The simplest of them are records. They are actually quite similar to simple types and static arrays. As simple types, records are, as we say, statically allocated. Local variables of record type are part of a thread's stack, global variables are part of the global process memory, and records that are parts of other structured types are stored as part of the owner's memory.
The important thing that you have to remember when using records is that the compiler manages the life cycle for their fields that are themselves managed. In other words, if you declare a local variable of a record type, all its managed fields will be automatically initialized, while unmanaged types will be left at random values (at whatever value the stack at that position contains at that moment).
The following code from the DataTypes demo demonstrates this behavior. When you run it, it will show some random values for fields a and c while the b field will always be initialized to an empty string.
The code also shows the simplest way to initialize a record to default values (zero for integer and real types, empty string for strings, nil for classes and so on). The built-in (but mostly undocumented) function Default creates a record in which all fields are set to default values and you can then assign it to a variable:
type
TRecord = record
a: integer;
b: string;
c: integer;
end;
procedure TfrmDataTypes.ShowRecord(const rec: TRecord);
begin
ListBox1.Items.Add(Format('a = %d, b = ''%s'', c = %d',
[rec.a, rec.b, rec.c]));
end;
procedure TfrmDataTypes.btnRecordInitClick(Sender: TObject);
var
rec: TRecord;
begin
ShowRecord(rec);
rec := Default(TRecord);
ShowRecord(rec);
end;
When you run this code, you'll get some random numbers for a and c in the first log, but they will always be zero in the second log:
The initialization of managed fields affects the execution speed. The compiler doesn't create an optimized initialization code for each record type but does the initialization by calling generic and relatively slow code.
This also happens when you assign one record to another. If all fields are unmanaged, the code can just copy data from one memory location to another. However, when at least one of the fields is of a managed type, the compiler will again call a generic copying method which is not optimized for the specific record.
The following example from the DataTypes demo shows the difference. It copies two different records a million times. Both records are of the same size, except that TUnmanaged contains only fields of unmanaged type NativeUInt and TManaged contains only fields of managed type IInterface:
type
TUnmanaged = record
a, b, c, d: NativeUInt;
end;
TManaged = record
a, b, c, d: IInterface;
end;
procedure TfrmDataTypes.btnCopyRecClick(Sender: TObject);
var
u1, u2: TUnmanaged;
m1, m2: TManaged;
i: Integer;
sw: TStopwatch;
begin
u1 := Default(TUnmanaged);
sw := TStopwatch.StartNew;
for i := 1 to 1000000 do
u2 := u1;
sw.Stop;
ListBox1.Items.Add(Format('TUnmanaged: %d ms', [sw.ElapsedMilliseconds]));
m1 := Default(TManaged);
sw := TStopwatch.StartNew;
for i := 1 to 1000000 do
m2 := m1;
sw.Stop;
ListBox1.Items.Add(Format('TManaged: %d ms', [sw.ElapsedMilliseconds]));
end;
The following image shows the measured difference in execution time. Granted, 31 ms to copy a million records is not a lot, but still the unmanaged version is five times faster. In some situations, that can mean a lot: