Monday, January 9, 2012

For The Record

Records are data structures that hold one or more pieces of information. While records have been around since the earliest days of Pascal, they have taken on a much larger role in the most recent versions of Delphi. Today's records possess many features previously only found in objects.

This post begins with a brief overview of records. It continues with a discussion of features that have been added to records since Delphi 2005. This post concludes with a look at several of the important new record types that have been introduced to Delphi in recent versions.

Record Basics

The traditional style of record definition is somewhat similar to an array, in that it can hold more than one data value. But there are a number of features that distinguish records from arrays.

In an array the various values are identified using an ordinal index. Records, by comparison, make use of named fields. Furthermore, arrays consist of a collection of elements that are all of the same data type (even if all elements of the array are variants). In records, the individual fields can be almost any valid Delphi type, including integers, Booleans, real numbers, object references, arrays, interfaces, and enumerations.

Declaring a Record

Consider the following type declarations. Together, these types define a record type, TDog, that includes four fields: A double, an enumeration, a TDateTime, and a TObject reference.

type
  TPerson = class(TObject)
private
  FName: String;
public
  property Name: String read FName write FName;
  constructor Create(Name: String);
end;

TBreed = (Akita, Beagle, Chihuahua, Dachshund);
TDog = record
  Weight: Double;
  Breed: TBreed;
  Born: TDatetime;
  Owner: TPerson;
end;

Using a Record

Once you have defined a record type, it is easy to create an instance of that record. Simply declare a variable of that record type and use it. It is not even necessary to allocate memory for the record — Delphi does that for you. For example, the following code demonstrates how to declare and use a record of type TDog.

procedure TForm1.Button2Click(Sender: TObject);
var
  Dog: TDog;
begin
  Dog.Name := 'Skippy';
  Dog.Breed := Beagle;
  Dog.Born := StrToDateTime('2010-10-15');
  Dog.Owner := TPerson.Create('Trevor');
...

Actually, it's not necessary to declare a record type in order to use a record. Instead, a variable can be defined as a record in its declaration. This might look something like the following:

procedure TForm1.Button3Click(Sender: TObject);
var
  Dog: record
    Name: String;
    Breed: TBreed;
    Born: TDatetime;
    Owner: TPerson;
   end;
begin
  Dog.Name := 'Skippy';
  // ...

More Variations

There are a couple of additional variations that are somewhat unique to records, and these are associated with enumerations and variant parts. Let's consider enumerations first.

In the TDog record type, Breed was declared as a field of type TBreed. An alternative would be to declare the Breed field to be an enumeration directly, without the TBreed declaration. Here is another record type declaration that uses this syntax:

type
  TAppointment = record
    Time: TDateTime;
    DayOfWeek: (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
    MeetingWith: TPerson;
    MeedtingBy: TPerson;
  end;

In the TAppointment record type, the DayOfWeek field is an enumeration, which is defined in the record type declaration directly.

Another interesting feature of traditional records is the optional variant part. A variant part is a conditional portion of the record structure that can hold variable pieces of information. The variant part, if present, always appear at the end of a record declaration.

For example, consider the following declaration of Delphi's TRect, which appears in the Types unit.

TRect = record
  case Integer of
    0: (Left, Top, Right, Bottom: Longint);
    1: (TopLeft, BottomRight: TPoint);
  end;

Using this syntax, you can use one or the other variant record structures (four Longints or two TPoints, though you cannot mix them in an individual instance of a record). For example, the following code declares and assigns data to two TRect instances (which are identical):

var
  r1, r2: TRect;
begin
  r1.TopLeft := Point(5,5);
  r1.BottomRight := Point(100, 200);
  r2.Top := 5;
  r2.Left := 5;
  r2.Bottom := 100;
  r2.Right := 200;

There is a second syntax, in which the value of one of the record's fields determines which of the alternative structures the record uses. For example, consider the following record type declaration:

type
  TAdult = record
    Name: String;
    DateOfBirth: TDate;
    case MilitaryService: Boolean of
      True: (
        WhichService: (Army, Navy, Marines, AirForce);
        StartDate: TDate;
        EndDate: TDate;
        );
      False: (
        Reason: (NoDraft, UnFit, Objection);
        );
    end;

In this record, when the Boolean field MilitaryService is assigned the value True the WhichService, StartDate, and EndDate fields are available. If MilitaryService is False, a single enumerated field, Reason, can be used.

Class-Like Records

Beginning with Delphi 2005, records began to receive a major overhaul with the introduction of member visibility. Until this time, all members of a record were public. Since Delphi 2005, the same visibility directives that you can use in class declarations can now also be used in record declarations.

But Delphi 2006 is the version in which the really big changes were introduced, giving records many features previously reserved for classes, and greatly improving their utility in your Delphi applications. These features include methods, properties, constructors, class constants, class variables, class static methods, class properties, nested types, overloaded operators, and record helpers. Each of these features are consider in the following sections.

Methods and Properties

A record's methods are functions and procedures associated with the record. Like their class cousins, a record's methods can access all of the other members of the record, including fields, properties, and other methods. Similar to the methods of a class, a record's methods have a reference to the record's instance through a variable named Self.

Record methods also permit overloading. When overloading a record's method, you follow the same rules that you follow when overloading the methods of a class. Specifically, the compiler must be able to distinguish between each overloaded version based on the signature of the method (including whether the method is a function or procedure).

Record properties are also similar to their class counterparts. A record's properties can be implemented with read and/or write parts, and can use either direct access (reading and writing from fields of the record) or accessor methods (implementing reading and writing through specified methods of the record. Accessor methods permit a property to perform side effects).

The following is a simple example of a record with two fields (variables), a method, and three properties.

type
  TRectangle = record
  strict private
    FWidth: Double;
    FDepth: Double;
    function GetArea: Double;
  public
    property Area: Double read GetArea;
    property Width: Double read FWidth write FWidth;
    property Depth: Double read FWidth write FDepth;
  end;

The methods declared in a record must be implemented (records do not permit abstract methods). Furthermore, after declaring a method in a record you can use class completion (Ctrl-Shift-C) to generate the necessary implementation stubs for your record's methods. The following is the implementation of the GetArea method of the TRectangle record.

function TRectangle.GetArea: Double;
begin
  Result := FWidth * FDepth;
end;

Record Constructors

All records have an implicit, zero parameter constructor that is called when you attempt to use a record for the first time. If you want, you can add additional constructors. These constructors, however, must have at least one parameter, and must be distinguishable from any other overloaded constructor based on their signatures.

The following shows the TRectangle record with an overloaded constructor (note that the overload directive was not required, but makes the code clearer). The implementation of the constructor follows in this code segment.

type
  TRectangle = record
  strict private
    FWidth: Double;
    FDepth: Double;
    function GetArea: Double;
  public
    property Area: Double read GetArea;
    property Width: Double read FWidth write FWidth;
    property Depth: Double read FWidth write FDepth;
    constructor Create(Width, Depth: Double); overload;
  end;
constructor TRectangle.Create(Width, Depth: Double);
begin
  FWidth := Width;
  FDepth := Depth;
end;

Class Constants and Class Variables

While each instance of a record has one memory location for each of its fields, class constants and class variables are shared between all instances of a given record type. A class constant is a constant value, and its declaration looks a lot like a regular declaration of a constant. The difference is that the class constant is a characteristic of the specific record type. The class constant can be read using a reference to an instance of the record type, or through the record type itself.

A class variable is a field that is shared by all instances of a particular record type. Changing the value of a class variable has the effect of changing the one memory location shared by the record type and all instances of it.

Class constants appear within a const clause of a record declaration, and class variables appear within a class var clause. These clauses conclude at the end of the record declaration, or when a method, type, variable, property, constructor, visibility directive, or variant part is encountered.

The following record type includes two class constants and two class variables:
TRectangle = record
  strict private
    FWidth: Double;
    FDepth: Double;
    function GetArea: Double;
    const
      MaxWidth = 100.0;
      MaxDepth = 200.0;
  public
    class var
      PreferredMinWidth: Double;
      PreferredMinDepth: Double;
    property Area: Double read GetArea;
    property Width: Double read FWidth write FWidth;
    property Depth: Double read FWidth write FDepth; 
    constructor Create(Width, Depth: Double); overload;
  end;

Class Static Methods and Class Properties

Unlike classes, which have both class methods and class static methods, records support only class static methods (as far as class methods go). A class static method is one that can be called on an instance of a record or through a reference to the record type itself. Like the class static methods in a class declaration, a record's class static methods have no reference to Self, and therefore cannot access any other members of the class, other than other class static methods or class variables.

Class properties are properties that can be referenced using an instance of a record type, or the record type itself. While class properties support both direct access and accessor methods (like normal properties), direct access can only employ class variables, and accessor methods must use only class static methods.

The following is a portion of a record type declaration that includes two class variables, a class static method, and two class properties. The implementation of the class static method follows.
type
  TRectangle = record
  strict private
    //...
    class var
      FPreferredMinWidth: Double;
      FPreferredMinDepth: Double;
    //...
  public
    class function PreferredMinArea: Double; static;
    class property PreferredMinWidth: Double
      read FPreferredMinWidth write FPreferredMinWidth;
    class property PreferredMinDepth: Double
      read FPreferredMinDepth write FPreferredMinDepth;
    //...
  end;
  class function TRectangle.PreferredMinArea: Double;  
  begin
     Result := FPreferredMinWidth * FPreferredMinDepth;
  end;

Nested Types

A nested type in a record is identical to a nested type in a class declaration. A nested type is a type declaration internal to a record. The nested type is often a class type that is designed to act as a helper class. Specifically, it is designed to perform tasks related to the record in which it is declared.

The following is another record class segment, which includes a simple nested type declaration.

type
  TRectangle = record
  strict private
    FWidth: Double;
    FDepth: Double;
  public
    //...
    function GetArea: Double;
    type
      WorkerClass = class(TObject)
      private
        FName: String;
      public
        function SayMyName: String;
        property Name: String read FName write FName;
    end;
    property Area: Double read GetArea;
    property Width: Double read FWidth write FWidth;
    property Depth: Double read FWidth write FDepth;
  end;

Operator Overloading

One additional feature added to records that is not shared by classes is operator overloading. In short, you can explicitly define what happens when an operator is applied to a particular record (unary operators) are to two records (binary operators). For example, you can define what happens when two of your records are added together using the addition (+) operator, or what happens if you implicitly cast your record to another type.

There are almost 30 operators that can be overloaded. These operators are shown in Listing 1.
Method NameOperatorUse ExampleReturns
Implicit return type
Explicit TRecord(r)return type
Negative-- rreturn type
Positive+ + rreturn type
Decdecdec(r)return type
LogicalNotnotnot rreturn type
Trunctrunctrunc(r)return type
Roundroundround(r)return type
Ininin [a, b, r]Boolean
Equal=a = rBoolean
NotEqual<>a <> rBoolean
GreaterThan>a > rBoolean
GreaterThanOrEqual>= a <= rBoolean
LessThan< a < rBoolean
LessThanOrEqual<= a <= rBoolean
Add+ a + rreturn type
Subtract- a - rreturn type
Multiply* a * rreturn type
Divide/ a / rreturn type
IntDividediv a div rreturn type
Modulusmod a mod rreturn type
LeftShiftshl a shl rreturn type
RightShiftshr a shr rreturn type
LogicalAndand a and rreturn type
LogicalOror a or rreturn type
LogicalXorxor a xor rreturn type
BitwiseAndand a and rreturn type
BitwiseOror a or rreturn type
BitwiseXorxor a xor rreturn type

Listing 1 Record operators that can be overloaded

To overload one of these operators, you implement the specified method name using the class operator syntax (instead of function) in your record type definition. You must also define the type of value that the operator will return. For those operators that are declared to return a Boolean value, the operator must return a Boolean value. All other operators can return whatever type is appropriate for that operator. In other words, a non-Boolean operator can return a value other than the record type.

The following code segment includes a portion of the TRectangle class with an overloaded addition operator, the implementation of this operator overload, and an event handle that uses the operator.
type
  TRectangle = record
  strict private
    FWidth: Double;
    //...
  public
    property Width: Double read FWidth write FWidth;
    //...
    class operator Add(a, b: TRectangle): TRectangle;
  end;
class operator TRectangle.Add(a, b: TRectangle): TRectangle;
begin
  Result.Width := a.Width + b.Width;
end;
procedure TForm1.Button7Click(Sender: TObject);
var
  a, b, c: TRectangle;
begin
  a.Width := 5;
  b.Width := 20;
  c := a + b;
end;

Record Helpers

Like classes in Delphi 2005, records can also have helpers. Record helpers serve essentially the same purpose as class helpers, though they only apply to records. Specifically, record helpers permit you to attach additional methods, properties, and class variables to an existing record. Record helpers can also replace existing methods of a class, if they use the same name and signature, since methods of a record helper supercede those of the record they are helping.

While the use of class helpers is largely discouraged, record helpers can play a role that cannot be provided through any other means. Specifically, record helpers provide the only mechanism for extending an existing record type. Classes, on the other hand, support inheritance. As a result, it is often better to descend a new class from an existing class, and introduce new methods and properties in the descendant than to use a class helper.

Record helpers have the same limitations as class helpers. A given record can be supported by only one record helper. If there is more than one record helper for a given record, the record will employ the record helper that is closer in scope.

The following code shows the declaration of a record helper for the TRectangle record, along with the implementation of the single method declared within the record helper. Notice in the GetMaxArea implementation that Self refers to the record instance.
type
  TRectHelper = record helper for TRectangle
function GetMaxArea: Double;
end;
function TRectHelper.GetMaxArea: Double;
begin
Result := Self.MaxWidth * Self.MaxDepth;
end;
procedure TForm1.Button8Click(Sender: TObject);
var
  a: TRectangle;
begin
  ShowMessage(FloatToStr(a.GetMaxArea));
end;

Records Versus Classes

At this point you might be inclined to think that the enhanced record syntax makes records nearly identical to classes, making it difficult to choose between them. The truth is that there are very significant differences between records and classes, and these differences can help you decide which to use in a particular situation.

First of all, records are allocated on the stack, as long as they are not declared globally or managed using New and Dispose. Class instances, by comparison, are allocated on the heap.

This has a significant impact on memory management. Specifically, while classes that do not have an owner must be explicitly freed, memory allocated for record instances is automatically freed when the record goes out of scope merely by its data being removed from the stack. Of course, if your record creates class instances during its lifecycle, that memory must be managed by you or your record, or a memory leak may exist. This is made a bit more difficult since records do not support destructors.
Another significant difference between classes and records is that you do not need to call a record's constructor, while a class instance must be explicitly created. You can declare a variable of a record type and then immediately use it. In this case, Delphi will call the zero parameter constructor for you.

You are still free to call a record's constructor if you want. In fact, if you've declared one or more additional constructors, it is probably because you want to create your record in a particular fashion, and you can do so by calling one of those overloaded constructors.
Another major difference between records and classes is that records do not support inheritance. A record never descends from an existing record. Similarly, records cannot implement interfaces (though they may have fields or properties of interface type, but that is different). The bottom line here is that records do not support polymorphism. A corollary of this is that record methods are necessarily static. Records do not support virtual, dynamic, or abstract methods.

As you learned earlier, records can also include a variant part. A variant part permits a record to hold data that is specific to its nature, and may vary from record instance to record instance. All instances of a class have the same set of members, regardless of their data.
Finally, records are value types, as opposed to reference types. Consider the following code:
var
  a, b: TRectangle;
begin
  a.Width := 5;
  a.Depth := 5;
  b := a;

The TRectangle named b in this code segment is a completely new record that contains a copy of the data held within record a. By comparison, if TRectangle was a class type, following the assignment of variable a to variable b, variable b would point to the same memory address as a.

Finally, records support operator overloading, while classes do not. Operator overloading provides you with a rich tool for managing exactly what happens when records are used in expressions, and the results can get very interesting.

Examples of Useful Records in Delphi

Record types are playing an increasingly important role in the latest versions of Delphi, and this is due in large part to the differences between record types and class types described in the preceding section. Let's consider a few of the more significant record types.

Delphi 2009 saw the introduction of the System.TMonitor type. TMonitor is a record that can be used to acquire and manage a lock on any TObject. It provides a convenient and powerful synchronization mechanism for multithreaded applications.

A major new layer of runtime type information (RTTI) was introduced in Delphi 2010. The principle type for working with the new RTTI, TRTTIContext, is a record type.

Additional records types introduced in Delphi 2010, and further enhanced in Delphi XE, include TFile, TPath, and TDirectory. These types, which are declared in the IOUtils unit, are record types.

Summary

It is no longer safe to assume that the entity you are calling a method on, or are reading a property from, is an object. It might be a type of record. Indeed, records types posses a unique blend of features that make them better suited for a variety of situations in which you would have previously used a class type.

Authors Note: I originally published this article in the SDN Magazine (issue 110), published by the Software Development Network on August 26, 2011. For information on the Software Development Network, visit http://www.sdn.nl/.

6 comments:

  1. Excellent work, should be included in the documentation!

    BTW, I think that the code sample in the "Nested Types" chapter is broken.

    ReplyDelete
  2. Actually, it isn't broken, it's just partial code. However, the original post had an indentation problem that I've now fixed. Thanks for pointing that out.

    ReplyDelete
  3. Hi Cary

    I was reading your article with interest but I think you got something wrong : About your TAdult record you write

    "In this record, when the Boolean field MilitaryService is assigned the value True the WhichService, StartDate, and EndDate fields are available. If MilitaryService is False, a single enumerated field, Reason, can be used."

    But if that was the case then the following would not be posible :

    procedure TForm146.FormCreate(Sender: TObject);
    var
    Person: TAdult;
    begin
    Person.Name := 'Test';
    Person.MilitaryService := False;
    Person.Reason := Objection;
    Person.StartDate := Now;
    Caption := DateToStr(Person.StartDate);
    end;


    Well it is. It compiles and it runs. Just testet in Delphi XE.

    ReplyDelete
  4. Thank you Cary. This is the best coverage on Delphi records anywhere. Easy to understand - clear and concise. As Primoz, stated this should be included in the documentation.

    ReplyDelete
  5. One additional point to make about variant records, as Jens' has pointed out, is that all fields from all variant sections are available during both runtime and compile-time.

    The compiler allows this and it also works in runtime but the reality is that each variant section actually occupies the same memory locations as the other variant sections, So in fact what occurs when you assign values to fields from different variant sections is that you end up just over-writing data that may have occupied that that same memory location from another field from another variant section. Of course, this will lead to data corruption which is not a good thing.

    You stated in your article that you cannot mix the variant parts in a variant record, but actually you can mix them. It's just that you shouldn't mix them.

    Ideally, the compiler and/or the IDE should pick this up but since it doesn't it's worthwhile letting developers know what is the consequence of using different variant sections in the same record instance.

    ReplyDelete
  6. You can do some degree of a polymorphism with records as well, using function reference variables, kind of simple vmt

    ReplyDelete