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.
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.
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.
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.
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.
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:
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
Lengthcounts 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,myvarandMYVARare 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
withstatement 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.