Skip to content

nostdahl.com

A technology journal
Article archive
← All articles
Programming

Object Pascal Refresher: Units, Records, Classes, and Generics

Bjørn T. Nøstdahl

~5 min read

Most people who tell me they know Pascal learned it in school, wrote a program that printed a triangle of stars, and never looked at it again. The language has changed a lot since then. This article is the refresher I wish I had when I came back: the core building blocks of Object Pascal, with working code, and notes on where Delphi and Free Pascal differ.

Units: the original module system

Every Pascal program is built from units. A unit has an interface section that declares what other units can see, and an implementation section that holds the code. It can also have initialization and finalization blocks that run when the program starts and stops.

pascal
unit Temperature;

interface

type
  TCelsius = type Double;

function ToFahrenheit(const Value: TCelsius): Double;

implementation

function ToFahrenheit(const Value: TCelsius): Double;
begin
  Result := Value * 9 / 5 + 32;
end;

end.

The compiler checks that nothing in the implementation leaks out, and the IDE can tell you exactly which units depend on which. When I came back from C#, the explicit interface section felt old-fashioned for about a day. Then I noticed how much easier it made reading other people's code: the public surface of a unit is at the top, in one place, with no scrolling.

Records: value types that grew methods

A record is a value type, allocated on the stack or inline inside other structures. Modern Object Pascal lets records have methods, properties, constructors and operator overloads, which makes them a good fit for small immutable values like coordinates, money or units of measure.

pascal
type
  TReading = record
    SensorId: string;
    Value: Double;
    Timestamp: TDateTime;
    class function Create(const AId: string; AValue: Double): TReading; static;
    function IsStale(MaxAgeMinutes: Integer): Boolean;
  end;

class function TReading.Create(const AId: string; AValue: Double): TReading;
begin
  Result.SensorId := AId;
  Result.Value := AValue;
  Result.Timestamp := Now;
end;

function TReading.IsStale(MaxAgeMinutes: Integer): Boolean;
begin
  Result := (Now - Timestamp) * 24 * 60 > MaxAgeMinutes;
end;

Because records are copied on assignment, they are safe to pass around between threads and they need no memory management. Ask yourself: how many of the classes in your current project could have been records?

Classes, properties and ownership

Classes are reference types, created with a constructor and freed with Free. The most important habit in Object Pascal is pairing every Create with a Free in a try..finally block. I will dedicate a whole article to memory management later, so for now here is the shape of a typical class.

pascal
type
  TSensor = class
  private
    FName: string;
    FReadings: TList<TReading>;
    function GetCount: Integer;
  public
    constructor Create(const AName: string);
    destructor Destroy; override;
    procedure Add(const AReading: TReading);
    property Name: string read FName;
    property Count: Integer read GetCount;
  end;

constructor TSensor.Create(const AName: string);
begin
  inherited Create;
  FName := AName;
  FReadings := TList<TReading>.Create;
end;

destructor TSensor.Destroy;
begin
  FReadings.Free;
  inherited;
end;

procedure TSensor.Add(const AReading: TReading);
begin
  FReadings.Add(AReading);
end;

function TSensor.GetCount: Integer;
begin
  Result := FReadings.Count;
end;

Properties are the part of the language that C# borrowed most directly. A property can read and write a field, or call a getter and setter, and callers never know the difference. virtual, override and abstract work the way you expect.

Interfaces: reference counting on demand

An interface declares a contract without implementation, just like in C# or Java. The twist is that interface references in Delphi and Free Pascal are reference counted. When the last reference goes out of scope, the object is freed automatically.

pascal
type
  ILogger = interface
    ['{7D3E0C62-4C58-4A2B-9E0A-3F1A2B4C5D6E}']
    procedure Log(const Msg: string);
  end;

  TConsoleLogger = class(TInterfacedObject, ILogger)
  public
    procedure Log(const Msg: string);
  end;

procedure TConsoleLogger.Log(const Msg: string);
begin
  WriteLn(FormatDateTime('hh:nn:ss', Now), ' ', Msg);
end;

var
  Logger: ILogger;
begin
  Logger := TConsoleLogger.Create;
  Logger.Log('Started');
end; // Logger is released here, no Free needed

This is the closest Pascal gets to garbage collection, and it is deliberate: you choose which objects are reference counted by giving them an interface. The GUID string is what lets you query one interface for another at runtime.

Generics: typed collections at last

If your memory of Pascal involves TList holding untyped pointers and casting everything, generics will be the biggest relief. Delphi ships TList<T>, TObjectList<T>, TDictionary<TKey, TValue> and friends in System.Generics.Collections. Free Pascal has a compatible Generics.Collections unit as well as its older fgl unit.

pascal
var
  Totals: TDictionary<string, Double>;
  Reading: TReading;
begin
  Totals := TDictionary<string, Double>.Create;
  try
    for Reading in Sensor.Readings do
      Totals.AddOrSetValue(Reading.SensorId,
        Totals.GetValueOrDefault(Reading.SensorId) + Reading.Value);
  finally
    Totals.Free;
  end;
end;

You can write your own generic types and methods with constraints, and for..in works over anything that exposes an enumerator.

Anonymous methods and inline variables

Delphi has anonymous methods (closures) and, since 10.3, inline variable declarations with type inference:

pascal
procedure Demo;
begin
  var Names := TList<string>.Create;
  try
    Names.Add('boiler');
    Names.Add('attic');
    Names.Sort(TComparer<string>.Construct(
      function(const Left, Right: string): Integer
      begin
        Result := CompareText(Left, Right);
      end));
  finally
    Names.Free;
  end;
end;

This is where Free Pascal lags. The stable 3.2 release has neither anonymous methods nor inline variables; both are in development builds. If you write code for both compilers, stick to declared procedure types, nested procedures and var blocks at the top of the routine. It is less convenient, but it is also easier to read six months later, which I count as a draw.

Small things that trip up returning programmers

  • Strings are Unicode. Delphi uses UTF-16, Lazarus defaults to UTF-8, and Length counts code units rather than characters, so be careful with string indexing when non-ASCII text is involved.
  • = compares, := assigns. You will get this wrong once.
  • Identifiers are case insensitive. MyVar, myvar and MYVAR are the same thing. Pick a convention and let the IDE enforce it.
  • Free Pascal has two main dialect modes: {$mode objfpc} with a few stricter rules, and {$mode delphi} for compatibility. Lazarus projects default to objfpc; I usually leave it.
  • The with statement exists. Do not use it.

Questions for you

  • Which modern feature of Object Pascal surprised you most when you came back, or which one do you still refuse to use?
  • Records with methods versus small classes: where do you draw the line?
  • Do you write code that compiles on both Delphi and Free Pascal? What does your lowest common denominator look like?

Email me at bjorn.nostdahl@nostdahl.com. Next time we put the language to work and build a cross-platform desktop application in Lazarus.

Continue exploring
← All articlesMore in Programming →