Output & Running
Hello, World
Of every target on this anchor, this is the one that will look most like home.
Option Strict On
Imports System
Module HelloWorld
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End Moduleprogram HelloWorld;
begin
WriteLn('Hello, World!');
end.Module becomes program, the body sits between begin and end, and the program ends with end. — a full stop, not a semicolon. Console.WriteLine becomes WriteLn, built in with nothing to import. Strings use single quotes. Both languages descend from the same family of readable, keyword-delimited syntax, and it shows immediately.Formatted output
No interpolated string — but two other ways to do the job, one of which is unique to Pascal.
Option Strict On
Imports System
Module FormatDemo
Sub Main()
Dim name As String = "Ada"
Dim score As Integer = 42
Dim ratio As Double = 0.8756
Console.WriteLine($"Hello, {name}! Score: {score}")
Console.WriteLine($"Rounded: {ratio:F2}")
End Sub
End Moduleprogram FormatDemo;
uses SysUtils;
var
Name: string;
Score: Integer;
Ratio: Double;
begin
Name := 'Ada';
Score := 42;
Ratio := 0.8756;
WriteLn('Hello, ', Name, '! Score: ', Score);
WriteLn('Rounded: ', Ratio:0:2);
WriteLn(Format('%s scored %d', [Name, Score]));
end.There is no
$"...". WriteLn takes any number of arguments of any type and prints them in order, which covers most cases. The unusual one is the write specifier: Ratio:0:2 means "minimum width 0, two decimal places", replacing {ratio:F2}. Format from SysUtils is String.Format, using C-style %s and %d with the arguments in a bracketed array.Case does not matter here either
The adjustment every other target on this anchor demands is the one adjustment you do not have to make here.
Option Strict On
Imports System
Module CaseDemo
Sub Main()
Dim customerName As String = "Grace"
' All one variable, whatever case you type
Console.WriteLine(customerName)
Console.WriteLine(CustomerName)
Console.WriteLine(CUSTOMERNAME)
End Sub
End Moduleprogram CaseDemo;
var
CustomerName: string;
begin
CustomerName := 'Grace';
{ All one variable — Pascal is case-insensitive too }
WriteLn(customername);
WriteLn(CustomerName);
WriteLn(CUSTOMERNAME);
end.Pascal identifiers are case-insensitive, exactly as Visual Basic's are, so
customerName and CustomerName are one name and the compiler does not care which you type. That removes the single most dangerous silent difference on every other page under this anchor. Convention is PascalCase for almost everything, with a T prefix on type names (TPerson) and an F prefix on private class fields (FName).Program Structure
Declarations come first, in their own blocks
Every declaration is hoisted to the top of its scope, in labelled sections — this is the biggest structural change on the page.
Option Strict On
Imports System
Module StructureDemo
Const Greeting As String = "Hello"
Sub Main()
Dim count As Integer = 3
Console.WriteLine(Greeting)
' A variable may be declared anywhere
Dim label As String = "widget"
Console.WriteLine($"{count} {label}")
End Sub
End Moduleprogram StructureDemo;
const
Greeting = 'Hello';
type
TCount = Integer;
var
Count: TCount;
Label_: string;
begin
Count := 3;
WriteLn(Greeting);
Label_ := 'widget';
WriteLn(Count, ' ', Label_);
end.A Pascal routine declares its constants, types and variables in
const, type and var blocks before the begin, never interleaved with statements. So a Dim written halfway down a Sub has to move up. It is more rigid than Dim-anywhere and has one real benefit: every name a routine uses is visible in one place at the top. The type block is where you name your own types, which the collections section uses heavily.begin and end replace every End keyword
The keyword is different and the semicolon rule is genuinely fiddly — read where they are and are not.
Option Strict On
Imports System
Module BlockDemo
Sub Main()
Dim temperature As Integer = 30
If temperature > 25 Then
Console.WriteLine("Warm")
Console.WriteLine("Very warm")
ElseIf temperature > 10 Then
Console.WriteLine("Mild")
Else
Console.WriteLine("Cold")
End If
End Sub
End Moduleprogram BlockDemo;
var
Temperature: Integer;
begin
Temperature := 30;
if Temperature > 25 then
begin
WriteLn('Warm');
WriteLn('Very warm');
end
else if Temperature > 10 then
WriteLn('Mild') { no semicolon before else }
else
WriteLn('Cold');
end.End If, End Sub and End Module all become end, and a block of more than one statement must be wrapped in begin ... end — a single statement needs neither. The rule that catches everyone: a semicolon is a separator, not a terminator, so there is no semicolon before else. ElseIf becomes two words. Then survives in lower case.Comments
Three comment forms, and Pascal is the rare language with two block forms.
Option Strict On
Imports System
Module CommentDemo
''' <summary>Doubles a number.</summary>
Function Twice(value As Integer) As Integer
' A line comment
Return value * 2
End Function
Sub Main()
Console.WriteLine(Twice(21))
End Sub
End Moduleprogram CommentDemo;
{ A brace comment, which may span lines }
(* An older form, equally valid *)
// A line comment
function Twice(Value: Integer): Integer;
begin
Result := Value * 2;
end;
begin
WriteLn(Twice(21));
end.' becomes //. Both { ... } and (* ... *) are block comments — the second exists for keyboards that lacked braces, and both are still accepted. Because {$...} is a compiler directive rather than a comment, a brace comment starting with $ means something: {$mode objfpc} is how a file selects its dialect. There is no XML documentation convention; Lazarus reads the comment immediately above a declaration.Variables & Types
:= assigns, = compares
Comparison keeps the symbol you already use — it is assignment that changes.
Option Strict On
Imports System
Module AssignDemo
Sub Main()
Dim left As Integer = 5
Dim right As Integer = 5
' One symbol, two jobs
If left = right Then Console.WriteLine("equal")
If left <> 6 Then Console.WriteLine("not six")
left = left + 1
Console.WriteLine(left)
End Sub
End Moduleprogram AssignDemo;
var
Left, Right: Integer;
begin
Left := 5;
Right := 5;
if Left = Right then WriteLn('equal');
if Left <> 6 then WriteLn('not six');
Left := Left + 1;
WriteLn(Left);
end.= means equality and nothing else, and <> is inequality — both exactly as you write them today, and a relief after C#'s ==/!=. Assignment is :=, which is where Pascal took the ambiguity out. Writing Left = 6 as a statement is a compile error rather than a silent assignment, which is the same protection C# gets from requiring a bool condition.The built-in types
Nearly a rename exercise, and the money type is one of the few outside .NET that exists at all.
Option Strict On
Imports System
Module TypeDemo
Sub Main()
Dim small As Short = 1S
Dim whole As Integer = 2
Dim big As Long = 3L
Dim precise As Double = 6.7
Dim exact As Decimal = 8.9D
Dim letter As Char = "A"c
Dim flag As Boolean = True
Console.WriteLine($"{small} {whole} {big} {precise} {exact} {letter} {flag}")
End Sub
End Moduleprogram TypeDemo;
var
Small: SmallInt;
Whole: Integer;
Big: Int64;
Precise: Double;
Exact: Currency;
Letter: Char;
Flag: Boolean;
begin
Small := 1;
Whole := 2;
Big := 3;
Precise := 6.7;
Exact := 8.9;
Letter := 'A';
Flag := True;
WriteLn(Small, ' ', Whole, ' ', Big, ' ', Precise:0:1, ' ',
Exact:0:2, ' ', Letter, ' ', Flag);
end.Short→SmallInt, Integer→Integer, Long→Int64, Double→Double, Single→Single, Boolean→Boolean, Char→Char with single quotes. The notable one is Currency: a fixed-point type with four decimal places, stored as a scaled 64-bit integer — not identical to Decimal (fewer digits, fixed rather than floating scale) but a real money type, which Go, Rust, JavaScript and Java all lack.Strings, and their 1-based index
If you still write
Mid and InStr, this column will feel like coming home.Option Strict On
Imports System
Module StringDemo
Sub Main()
Dim text As String = "Visual Basic"
Console.WriteLine(text.Length)
Console.WriteLine(text.ToUpper())
Console.WriteLine(text.Substring(0, 6))
Console.WriteLine(Mid(text, 1, 6))
Console.WriteLine(text.IndexOf("Basic"))
Console.WriteLine(InStr(text, "Basic"))
End Sub
End Moduleprogram StringDemo;
uses SysUtils;
var
Text: string;
begin
Text := 'Visual Basic';
WriteLn(Length(Text));
WriteLn(UpperCase(Text));
WriteLn(Copy(Text, 1, 6));
WriteLn(Pos('Basic', Text));
WriteLn(Text[1]);
WriteLn(Text + ' — concatenated');
end.Pascal strings are 1-based, like the
Microsoft.VisualBasic string functions and unlike Substring. Mid(text, 1, 6) becomes Copy(Text, 1, 6) with the same numbers; InStr becomes Pos, also 1-based and also returning 0 when not found; Len becomes Length, UCase becomes UpperCase. Text[1] indexes a character directly. Concatenation is +, and strings are managed and reference-counted, so no manual freeing is needed for them.Converting between types
The conversion family maps almost one to one — including the
TryParse pattern, which most targets on this anchor lack.Option Strict On
Imports System
Module ConversionDemo
Sub Main()
Dim text As String = "123"
Dim parsed As Integer = CInt(text)
Dim asText As String = CStr(parsed * 2)
Dim value As Integer
If Integer.TryParse("12x", value) Then
Console.WriteLine(value)
Else
Console.WriteLine("not a number")
End If
Console.WriteLine($"{parsed} {asText}")
End Sub
End Moduleprogram ConversionDemo;
uses SysUtils;
var
Text, AsText: string;
Parsed, Value: Integer;
begin
Text := '123';
Parsed := StrToInt(Text);
AsText := IntToStr(Parsed * 2);
if TryStrToInt('12x', Value) then
WriteLn(Value)
else
WriteLn('not a number');
WriteLn(Parsed, ' ', AsText);
WriteLn(StrToIntDef('12x', -1));
end.CInt→StrToInt, CStr→IntToStr, CDbl→StrToFloat, CBool→StrToBool, all from SysUtils. Integer.TryParse becomes TryStrToInt, with the same shape: a Boolean result and an output parameter. There is also StrToIntDef, which takes a fallback and needs no output parameter at all. Of every target on this anchor this is the closest correspondence, because both languages evolved the same idea independently.Arrays & Records
Arrays choose their own bounds
The array declares its own first and last index — which is what
Option Base 1 was reaching for.Option Strict On
Imports System
Module ArrayDemo
Sub Main()
' The number is the UPPER BOUND — six slots, 0 to 5
Dim scores(5) As Integer
scores(1) = 10
scores(5) = 50
Console.WriteLine(scores.Length)
Console.WriteLine(LBound(scores))
Console.WriteLine(UBound(scores))
Console.WriteLine(scores(1))
End Sub
End Moduleprogram ArrayDemo;
type
TScores = array[1..5] of Integer; { 1-based, by choice }
var
Scores: TScores;
Index: Integer;
begin
for Index := Low(Scores) to High(Scores) do
Scores[Index] := Index * 10;
WriteLn(Length(Scores));
WriteLn(Low(Scores));
WriteLn(High(Scores));
WriteLn(Scores[1]);
end.array[1..5] means indices 1 through 5, and you may pick any bounds at all: array[1900..2100] indexed by year is idiomatic Pascal. LBound and UBound become Low and High, and using them rather than literals is the convention, so a bound change never breaks a loop. Anyone still missing Option Base 1 from VB6 has it back — properly, per array, rather than per file.Dynamic arrays replace List(Of T)
A dynamic array grows — and note that it is 0-based where a fixed array was whatever you declared.
Option Strict On
Imports System
Imports System.Collections.Generic
Module DynamicDemo
Sub Main()
Dim fruits As New List(Of String) From {"apple", "banana"}
fruits.Add("cherry")
Console.WriteLine(fruits.Count)
Console.WriteLine(fruits(0))
Console.WriteLine(String.Join(", ", fruits))
End Sub
End Moduleprogram DynamicDemo;
var
Fruits: array of string;
Index: Integer;
begin
Fruits := ['apple', 'banana']; { dynamic array literal }
SetLength(Fruits, Length(Fruits) + 1);
Fruits[High(Fruits)] := 'cherry';
WriteLn(Length(Fruits));
WriteLn(Fruits[0]); { dynamic arrays ARE 0-based }
for Index := Low(Fruits) to High(Fruits) do
Write(Fruits[Index], ' ');
WriteLn;
end.array of T with no bounds is a dynamic array, sized by SetLength and always starting at 0. That inconsistency with fixed arrays is a genuine wart, and Low/High is why it rarely bites. There is no Add: growing means SetLength then assigning, or Insert from SysUtils. For a real list with methods, Free Pascal ships TList, TStringList and the generic TFPGList / TList<T> — and those are objects, so the memory section applies to them.Structure becomes record
A value type that copies on assignment, with methods — exactly what
Structure is.Option Strict On
Imports System
Public Structure Point
Public X As Integer
Public Y As Integer
Public Sub New(x As Integer, y As Integer)
Me.X = x
Me.Y = y
End Sub
Public Function Describe() As String
Return $"({X}, {Y})"
End Function
End Structure
Module RecordDemo
Sub Main()
Dim origin As New Point(1, 2)
Dim copy As Point = origin
copy.X = 99
Console.WriteLine(origin.Describe())
Console.WriteLine(copy.Describe())
End Sub
End Moduleprogram RecordDemo;
uses SysUtils;
type
TPoint = record
X, Y: Integer;
end;
function Describe(const APoint: TPoint): string;
begin
Result := Format('(%d, %d)', [APoint.X, APoint.Y]);
end;
var
Origin, Copy_: TPoint;
begin
Origin.X := 1;
Origin.Y := 2;
Copy_ := Origin; { copies, like a Structure }
Copy_.X := 99;
WriteLn(Describe(Origin));
WriteLn(Describe(Copy_));
end.Structure ... End Structure becomes record ... end, and assignment copies the whole value in both. A plain record is data only, so the behaviour becomes a standalone routine — which is why the example passes the point in rather than calling a method on it. Records can carry methods, but only with {$modeswitch advancedrecords} turned on, and even then declaration and implementation stay separate. A record needs no constructor and no New: declaring the variable is enough, and it needs no freeing.Control Flow
Select Case becomes case
A near-literal translation, including the range form.
Option Strict On
Imports System
Module CaseDemo
Function Describe(code As Integer) As String
Select Case code
Case 1
Return "one"
Case 2, 3
Return "two or three"
Case 4 To 6
Return "four to six"
Case Else
Return "something else"
End Select
End Function
Sub Main()
Console.WriteLine(Describe(1))
Console.WriteLine(Describe(3))
Console.WriteLine(Describe(5))
Console.WriteLine(Describe(9))
End Sub
End Moduleprogram CaseDemo;
function Describe(Code: Integer): string;
begin
case Code of
1: Result := 'one';
2, 3: Result := 'two or three';
4..6: Result := 'four to six';
else
Result := 'something else';
end;
end;
begin
WriteLn(Describe(1));
WriteLn(Describe(3));
WriteLn(Describe(5));
WriteLn(Describe(9));
end.Select Case x becomes case x of, Case labels become value:, Case 2, 3 keeps its comma, and Case 4 To 6 becomes 4..6. Case Else becomes a bare else. No branch falls through, as in Visual Basic. The one limitation: a Pascal case works only on ordinal types — integers, characters, enumerations, booleans — so Select Case on a String has to become an if chain.Loops, including repeat ... until
Every Visual Basic loop has a direct counterpart — including
Loop Until, which almost nothing else on this anchor does.Option Strict On
Imports System
Imports System.Collections.Generic
Module LoopDemo
Sub Main()
For index As Integer = 1 To 5
Console.Write(index & " ")
Next
Console.WriteLine()
For countdown As Integer = 5 To 1 Step -1
Console.Write(countdown & " ")
Next
Console.WriteLine()
Dim words As New List(Of String) From {"alpha", "beta"}
For Each word As String In words
Console.WriteLine(word.ToUpper())
Next
Dim attempt As Integer = 0
Do
attempt += 1
Loop Until attempt >= 2
Console.WriteLine(attempt)
End Sub
End Moduleprogram LoopDemo;
uses SysUtils;
var
Index, Attempt: Integer;
Word_: string;
Words: array of string;
begin
for Index := 1 to 5 do
Write(Index, ' ');
WriteLn;
for Index := 5 downto 1 do
Write(Index, ' ');
WriteLn;
Words := ['alpha', 'beta'];
for Word_ in Words do
WriteLn(UpperCase(Word_));
Attempt := 0;
repeat
Attempt := Attempt + 1;
until Attempt >= 2;
WriteLn(Attempt);
end.For i = 1 To 5 becomes for Index := 1 to 5 do, inclusive at both ends, so no off-by-one to convert. Step -1 becomes downto; other step sizes have no form and need a while. For Each becomes for x in collection do. And Do ... Loop Until becomes repeat ... until, which needs no inversion and no begin/end — the repeat and until bracket the block themselves.The With block is still here
Of every target on this anchor, Pascal is the only one that kept this construct.
Option Strict On
Imports System
Public Class Report
Public Property Title As String = ""
Public Property Pages As Integer
Public Property Author As String = ""
End Class
Module WithDemo
Sub Main()
Dim report As New Report()
With report
.Title = "Quarterly"
.Pages = 12
.Author = "Ada"
End With
Console.WriteLine($"{report.Title} / {report.Pages} / {report.Author}")
End Sub
End Moduleprogram WithDemo;
type
TReport = record
Title: string;
Pages: Integer;
Author: string;
end;
var
Report: TReport;
begin
with Report do
begin
Title := 'Quarterly';
Pages := 12;
Author := 'Ada';
end;
WriteLn(Report.Title, ' / ', Report.Pages, ' / ', Report.Author);
end.With obj ... End With becomes with Obj do begin ... end, and it does the same job — though note the members are written without a leading dot. C#, Python, Ruby, Go, Rust and JavaScript all lack it entirely; only Pascal has it, which is unsurprising given that Visual Basic borrowed the idea from the same tradition. Modern Pascal style is wary of it for the same reason modern Visual Basic style is: a nested with makes it genuinely unclear which object a bare name belongs to.Procedures & Functions
Sub and Function become procedure and function
The
Sub/Function distinction survives intact — this is the language Visual Basic got it from.Option Strict On
Imports System
Module RoutineDemo
Sub Announce(message As String)
Console.WriteLine($"** {message} **")
End Sub
Function Add(left As Integer, right As Integer) As Integer
Return left + right
End Function
Sub Main()
Announce("starting")
Console.WriteLine(Add(2, 3))
End Sub
End Moduleprogram RoutineDemo;
procedure Announce(const Message: string);
begin
WriteLn('** ', Message, ' **');
end;
function Add(Left, Right: Integer): Integer;
begin
Result := Left + Right;
end;
begin
Announce('starting');
WriteLn(Add(2, 3));
end.Sub becomes procedure and Function ... As Integer becomes function ... : Integer, with the return type after a colon exactly where As Integer sits. Return value becomes Result := value, which will feel familiar to anyone who remembers VB6's Add = 5; there is also an Exit statement for an early return. Parameters sharing a type share the annotation, and const on a parameter promises not to modify it and lets the compiler avoid a copy.ByRef becomes var
The feature survives with a shorter keyword, and Pascal splits it the way C# does.
Option Strict On
Imports System
Module ByRefDemo
Sub Twice(ByRef value As Integer)
value *= 2
End Sub
Function TryHalve(input As Integer, ByRef result As Integer) As Boolean
If input Mod 2 <> 0 Then Return False
result = input \ 2
Return True
End Function
Sub Main()
Dim number As Integer = 21
Twice(number)
Console.WriteLine(number)
Dim half As Integer
If TryHalve(10, half) Then Console.WriteLine(half)
End Sub
End Moduleprogram ByRefDemo;
procedure Twice(var Value: Integer);
begin
Value := Value * 2;
end;
function TryHalve(Input: Integer; out Halved: Integer): Boolean;
begin
if Input mod 2 <> 0 then
Exit(False);
Halved := Input div 2;
Result := True;
end;
var
Number, Half: Integer;
begin
Number := 21;
Twice(Number);
WriteLn(Number);
if TryHalve(10, Half) then WriteLn(Half);
end.ByRef becomes var when the value goes in and comes back, and out when it is purely an output — the same distinction C# draws. ByVal is the default and needs no keyword. Unlike C#, the call site writes nothing: Twice(Number) looks like an ordinary call, exactly as in Visual Basic, so you must read the declaration to know. Note div and mod, which the next section covers, and Exit(False) as an early return with a value.Optional parameters and overloading
Both features exist, and one of them has to be asked for explicitly.
Option Strict On
Imports System
Module OverloadDemo
Function Greet(name As String,
Optional greeting As String = "Hello") As String
Return $"{greeting}, {name}"
End Function
Function Area(side As Double) As Double
Return side * side
End Function
Function Area(width As Double, height As Double) As Double
Return width * height
End Function
Sub Main()
Console.WriteLine(Greet("Ada"))
Console.WriteLine(Greet("Grace", "Welcome"))
Console.WriteLine(Area(3))
Console.WriteLine(Area(3, 4))
End Sub
End Moduleprogram OverloadDemo;
uses SysUtils;
function Greet(const Name: string; const Greeting: string = 'Hello'): string;
begin
Result := Greeting + ', ' + Name;
end;
function Area(Side: Double): Double; overload;
begin
Result := Side * Side;
end;
function Area(Width, Height: Double): Double; overload;
begin
Result := Width * Height;
end;
begin
WriteLn(Greet('Ada'));
WriteLn(Greet('Grace', 'Welcome'));
WriteLn(Area(3):0:2);
WriteLn(Area(3, 4):0:2);
end.A default value makes a parameter optional, and the
Optional keyword disappears — the same as everywhere else on this anchor. Overloading works too, but each overload must be marked overload, otherwise the second declaration is an error rather than an overload. Named arguments do not exist: greeting:= has no counterpart, so a middle argument cannot be skipped.Classes & Objects
A class, split into declaration and implementation
Everything you expect is here — and so is a
try...finally around a single object, which the next section explains.Option Strict On
Imports System
Public Class Person
Private ReadOnly _name As String
Private ReadOnly _age As Integer
Public Sub New(name As String, age As Integer)
_name = name
_age = age
End Sub
Public Function Describe() As String
Return $"{_name}, age {_age}"
End Function
End Class
Module ClassDemo
Sub Main()
Dim person As New Person("Ada", 36)
Console.WriteLine(person.Describe())
End Sub
End Moduleprogram ClassDemo;
uses SysUtils;
type
TPerson = class
private
FName: string;
FAge: Integer;
public
constructor Create(const AName: string; AAge: Integer);
function Describe: string;
end;
constructor TPerson.Create(const AName: string; AAge: Integer);
begin
FName := AName;
FAge := AAge;
end;
function TPerson.Describe: string;
begin
Result := Format('%s, age %d', [FName, FAge]);
end;
var
Person: TPerson;
begin
Person := TPerson.Create('Ada', 36);
try
WriteLn(Person.Describe);
finally
Person.Free;
end;
end.A class declares its members in the
type block and implements them afterwards, which is more typing than Visual Basic's single block and makes the public shape of a class readable in one place. Public Sub New becomes constructor Create, called as TPerson.Create(...) — on the type, not with a New keyword. Me becomes Self. Visibility sections (private, protected, public, published) group members rather than marking each one.Properties
Real properties, from the same tradition — and this is where Visual Basic and Delphi visibly share ancestry.
Option Strict On
Imports System
Public Class Temperature
Private _celsius As Double
Public Property Celsius As Double
Get
Return _celsius
End Get
Set(value As Double)
_celsius = Math.Max(value, -273.15)
End Set
End Property
Public ReadOnly Property Fahrenheit As Double
Get
Return _celsius * 9.0 / 5.0 + 32
End Get
End Property
End Class
Module PropertyDemo
Sub Main()
Dim reading As New Temperature()
reading.Celsius = 100.0
Console.WriteLine(reading.Fahrenheit)
reading.Celsius = -500
Console.WriteLine(reading.Celsius)
End Sub
End Moduleprogram PropertyDemo;
uses Math;
type
TTemperature = class
private
FCelsius: Double;
procedure SetCelsius(AValue: Double);
function GetFahrenheit: Double;
public
property Celsius: Double read FCelsius write SetCelsius;
property Fahrenheit: Double read GetFahrenheit;
end;
procedure TTemperature.SetCelsius(AValue: Double);
begin
FCelsius := Max(AValue, -273.15);
end;
function TTemperature.GetFahrenheit: Double;
begin
Result := FCelsius * 9.0 / 5.0 + 32;
end;
var
Reading: TTemperature;
begin
Reading := TTemperature.Create;
try
Reading.Celsius := 100.0;
WriteLn(Reading.Fahrenheit:0:2);
Reading.Celsius := -500;
WriteLn(Reading.Celsius:0:2);
finally
Reading.Free;
end;
end.A
property declares what to read and what to write, and either may be a field directly or a method. So read FCelsius write SetCelsius is a plain read with a validated write, and a property with only read is ReadOnly Property. Callers write Reading.Celsius := 100, identical to Visual Basic. There is a fourth visibility, published, which makes a property visible to the Lazarus form designer — the mechanism the object inspector is built on.Inheritance and interfaces
Every keyword has a counterpart, and they are almost the same words.
Option Strict On
Imports System
Public MustInherit Class Shape
Public MustOverride Function Area() As Double
Public Overridable Function Describe() As String
Return $"area {Area():F2}"
End Function
End Class
Public Class Circle
Inherits Shape
Private ReadOnly _radius As Double
Public Sub New(radius As Double)
_radius = radius
End Sub
Public Overrides Function Area() As Double
Return Math.PI * _radius * _radius
End Function
Public Overrides Function Describe() As String
Return "circle: " & MyBase.Describe()
End Function
End Class
Module InheritanceDemo
Sub Main()
Dim shape As Shape = New Circle(2.0)
Console.WriteLine(shape.Describe())
End Sub
End Moduleprogram InheritanceDemo;
uses SysUtils, Math;
type
TShape = class
public
function Area: Double; virtual; abstract;
function Describe: string; virtual;
end;
TCircle = class(TShape)
private
FRadius: Double;
public
constructor Create(ARadius: Double);
function Area: Double; override;
function Describe: string; override;
end;
function TShape.Describe: string;
begin
Result := Format('area %.2f', [Area]);
end;
constructor TCircle.Create(ARadius: Double);
begin
FRadius := ARadius;
end;
function TCircle.Area: Double;
begin
Result := Pi * FRadius * FRadius;
end;
function TCircle.Describe: string;
begin
Result := 'circle: ' + inherited Describe;
end;
var
Shape: TShape;
begin
Shape := TCircle.Create(2.0);
try
WriteLn(Shape.Describe);
finally
Shape.Free;
end;
end.Inherits Shape becomes class(TShape), Overridable becomes virtual, Overrides becomes override, MustOverride becomes virtual; abstract, and MyBase becomes inherited. As in .NET a method is not overridable unless marked, and a class extends one class. Interfaces exist too — IGreeter = interface and class(TObject, IGreeter) — and are reference-counted, which interacts with the memory rules in the next section.Memory: There Is No Collector
Every object you create, you free
This is the single biggest change in the whole move, and it is not a syntax difference.
Option Strict On
Imports System
Imports System.Text
Module MemoryDemo
Sub Main()
Dim builder As New StringBuilder()
builder.Append("first")
builder.Append(" second")
Console.WriteLine(builder.ToString())
' The garbage collector reclaims it, eventually, unprompted
End Sub
End Moduleprogram MemoryDemo;
uses Classes;
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
Lines.Add('first');
Lines.Add('second');
WriteLn(Lines.Text);
finally
Lines.Free; { REQUIRED — nothing does this for you }
end;
end.There is no garbage collector. Every object created with
Create must be released with Free, and forgetting is a memory leak rather than an error. The universal idiom is the one above: Create, then try ... finally Free, so the object is released even if the block raises. Records, strings, dynamic arrays and interfaces are all managed automatically — it is only class instances you own. A Visual Basic programmer who has never thought about lifetime has to start, and it is the main thing to budget learning time for.Letting an owner do the freeing
A collection holds references, not ownership — and that distinction is what a .NET programmer has never had to think about.
Option Strict On
Imports System
Imports System.Collections.Generic
Public Class Item
Public Property Name As String = ""
End Class
Module OwnerDemo
Sub Main()
Dim items As New List(Of Item) From {
New Item With {.Name = "first"},
New Item With {.Name = "second"}
}
For Each item As Item In items
Console.WriteLine(item.Name)
Next
' Nothing to release — the collector handles all of it
End Sub
End Moduleprogram OwnerDemo;
uses Classes, SysUtils;
type
TItem = class
public
Name: string;
constructor Create(const AName: string);
end;
constructor TItem.Create(const AName: string);
begin
Name := AName;
end;
var
Items: TList;
Index: Integer;
begin
Items := TList.Create;
try
Items.Add(TItem.Create('first'));
Items.Add(TItem.Create('second'));
for Index := 0 to Items.Count - 1 do
WriteLn(TItem(Items[Index]).Name);
finally
for Index := 0 to Items.Count - 1 do
TItem(Items[Index]).Free; { the list does NOT free its contents }
Items.Free;
end;
end.Freeing the list does not free what is in it. Either release each item yourself, as above, or use a container that owns its contents:
TObjectList.Create(True) takes an OwnsObjects flag and frees each item when it is freed, and the Lazarus component model does the same — a control added to a form is freed with the form. The discipline is to decide, for every object, who owns it, and to write that down. It is the habit that makes the absence of a collector manageable rather than frightening.Error Handling
Try/Catch becomes try/except
Almost a direct translation, with one structural wrinkle:
finally needs its own block.Option Strict On
Imports System
Module TryDemo
Sub Main()
Try
Dim value As Integer = Integer.Parse("not a number")
Console.WriteLine(value)
Catch error_ As FormatException
Console.WriteLine($"Bad format: {error_.Message}")
Catch error_ As Exception
Console.WriteLine($"Something else: {error_.Message}")
Finally
Console.WriteLine("always runs")
End Try
End Sub
End Moduleprogram TryDemo;
uses SysUtils;
begin
try
try
WriteLn(StrToInt('not a number'));
except
on E: EConvertError do
WriteLn('Bad format: ', E.Message);
on E: Exception do
WriteLn('Something else: ', E.Message);
end;
finally
WriteLn('always runs');
end;
end.Try→try, Catch e As T→on E: T do, End Try→end. Order still matters, most specific first, and E.Message reads the same. The wrinkle: Pascal has no try...except...finally in one block, so a routine needing both nests a try..except inside a try..finally, as above. That is why the memory section's try..finally Free is always its own block.Raising your own exception
Defining an exception is the inheritance you already saw, applied to one base class — and note who frees it.
Option Strict On
Imports System
Public Class InsufficientFundsException
Inherits Exception
Public ReadOnly Property Shortfall As Decimal
Public Sub New(shortfall As Decimal)
MyBase.New($"Short by {shortfall}")
Me.Shortfall = shortfall
End Sub
End Class
Module RaiseDemo
Sub Withdraw(balance As Decimal, amount As Decimal)
If amount > balance Then
Throw New InsufficientFundsException(amount - balance)
End If
End Sub
Sub Main()
Try
Withdraw(50D, 75D)
Catch error_ As InsufficientFundsException
Console.WriteLine($"{error_.Message} (short {error_.Shortfall})")
End Try
End Sub
End Moduleprogram RaiseDemo;
uses SysUtils;
type
EInsufficientFunds = class(Exception)
public
Shortfall: Currency;
constructor Create(AShortfall: Currency);
end;
constructor EInsufficientFunds.Create(AShortfall: Currency);
begin
inherited CreateFmt('Short by %.2f', [AShortfall]);
Shortfall := AShortfall;
end;
procedure Withdraw(Balance, Amount: Currency);
begin
if Amount > Balance then
raise EInsufficientFunds.Create(Amount - Balance);
end;
begin
try
Withdraw(50, 75);
except
on E: EInsufficientFunds do
WriteLn(E.Message, ' (short ', E.Shortfall:0:2, ')');
end;
end.Throw New becomes raise ...Create(...), and Inherits Exception becomes class(Exception) from SysUtils. MyBase.New becomes inherited Create, or CreateFmt for a formatted message. Convention prefixes exception class names with E rather than suffixing Exception. The one thing to know given the previous section: a raised exception object is freed automatically when the handler finishes, so this is the one Create you must not Free.Units, Lazarus & Deployment
Modules become units
A unit is a file with a public half and a private half, declared separately.
Option Strict On
Imports System
Imports System.Math
Namespace Geometry
Public Module Area
Public Function Rectangle(width As Double, height As Double) As Double
Return width * height
End Function
End Module
End Namespace
Module UnitDemo
Sub Main()
Console.WriteLine(Geometry.Area.Rectangle(3, 4))
End Sub
End Moduleprogram UnitDemo;
uses SysUtils, Math;
{ In a real project this would be its own file, geometry.pas:
unit Geometry;
interface
function RectangleArea(Width, Height: Double): Double;
implementation
function RectangleArea(Width, Height: Double): Double;
begin
Result := Width * Height;
end;
end.
}
function RectangleArea(Width, Height: Double): Double;
begin
Result := Width * Height;
end;
begin
WriteLn(RectangleArea(3, 4):0:2);
WriteLn(Max(3, 7));
end.Imports becomes uses, and names come into scope unqualified — Max, not Math.Max — with later units in the list winning a name clash. A unit has an interface section listing what the outside world may use and an implementation section holding the bodies plus anything private. That split is more explicit than Public/Private on each member, and it gives you a readable summary of a unit's API at the top of its own file.Lazarus is the reason you are reading this page
The part no other target on this anchor can offer: the workflow survives.
Option Strict On
Imports System
Imports System.Collections.Generic
Module LazarusDemo
Sub Main()
' In WinForms the designer generates the form class and
' controls are fields on it:
' Label1.Caption = "Hello"
' Button1.OnClick = AddressOf Button1_Click
Dim story As New Dictionary(Of String, String) From {
{"designer", "WinForms / WPF"},
{"packages", "NuGet"},
{"output", "exe plus a runtime"}
}
For Each entry In story
Console.WriteLine($"{entry.Key}: {entry.Value}")
Next
End Sub
End Moduleprogram LazarusDemo;
{ In Lazarus the designer generates the same shape:
procedure TForm1.Button1Click(Sender: TObject);
begin
Label1.Caption := 'Hello';
end;
}
type
TPair = record
Key, Value: string;
end;
var
Story: array of TPair;
Index: Integer;
begin
SetLength(Story, 3);
Story[0].Key := 'designer'; Story[0].Value := 'Lazarus LCL';
Story[1].Key := 'packages'; Story[1].Value := 'Online Package Manager';
Story[2].Key := 'output'; Story[2].Value := 'one native binary, no runtime';
for Index := Low(Story) to High(Story) do
WriteLn(Story[Index].Key, ': ', Story[Index].Value);
end.Lazarus is a free, open-source IDE with a drag-and-drop form designer, an object inspector, and double-click-to-write-an-event-handler — the VB6 and WinForms workflow, on a compiler nobody owns. The LCL is the control library;
Label1.Caption := 'Hello' is Label1.Text = "Hello". It cross-compiles to Windows, macOS, Linux and Raspberry Pi from the same source, and the output is one native binary with no runtime to install. Packages come from the Online Package Manager rather than NuGet.⚠ Gotchas for Visual Basic Programmers
⚠ / always produces a Real
The one page on this anchor where the division rule needs no warning at all.
Option Strict On
Imports System
Module DivisionGotcha
Sub Main()
Dim quotient As Integer = 17 \ 5
Dim exact As Double = 17 / 5
Dim remainder As Integer = 17 Mod 5
Console.WriteLine($"{quotient} {exact} {remainder}")
End Sub
End Moduleprogram DivisionGotcha;
var
Quotient, Remainder: Integer;
Exact: Double;
begin
Quotient := 17 div 5;
Exact := 17 / 5;
Remainder := 17 mod 5;
WriteLn(Quotient, ' ', Exact:0:1, ' ', Remainder);
end.Pascal makes exactly the distinction Visual Basic makes:
/ always produces a Real, and div is integer division. So \ becomes div, / stays /, and Mod becomes mod — no truncation trap, no silently changed meaning. Assigning the result of / to an Integer is a compile error rather than a rounding, which is Option Strict On behaviour by default. Every other target on this anchor except Python gets this wrong for a Visual Basic reader; Pascal does not.⚠ The semicolon is a separator
The rule that produces the most baffling early compiler errors, and it takes one sentence to state.
Option Strict On
Imports System
Module SemicolonGotcha
Sub Main()
Dim value As Integer = 5
If value > 3 Then
Console.WriteLine("big")
Else
Console.WriteLine("small")
End If
End Sub
End Moduleprogram SemicolonGotcha;
var
Value: Integer;
begin
Value := 5;
if Value > 3 then
WriteLn('big') { NO semicolon here }
else
WriteLn('small');
if Value > 3 then
begin
WriteLn('also big');
end { and none here either }
else
WriteLn('small');
end.A semicolon separates statements rather than terminating them, so there is never one immediately before
else — the if...then...else is a single statement and a semicolon would end it early. The same applies before until and before the final end, though a semicolon there is harmless because it separates from an empty statement. Free Pascal's error for this is Fatal: Syntax error, ";" expected but "ELSE" found, which is the compiler being about as helpful as it can be.⚠ Forgetting Free is a leak, not an error
The consequence of no garbage collector, seen in the shape a loop has to take.
Option Strict On
Imports System
Imports System.Collections.Generic
Module LeakGotcha
Sub Main()
For index As Integer = 1 To 3
Dim items As New List(Of Integer) From {index}
Console.WriteLine(items.Count)
Next
' Three lists allocated, all reclaimed automatically
End Sub
End Moduleprogram LeakGotcha;
uses Classes;
var
Index: Integer;
Items: TStringList;
begin
for Index := 1 to 3 do
begin
Items := TStringList.Create;
try
Items.Add('value');
WriteLn(Items.Count);
finally
Items.Free; { omit this and the loop leaks three objects }
end;
end;
end.Leaving out the
Free compiles cleanly, runs correctly, and leaks — three objects here, thousands in a long-running service. Nothing warns you at build time. Two things make this tractable: the try ... finally Free idiom applied without exception, and Free Pascal's -gh heaptrc option, which prints every unfreed block with its allocation stack when the program exits. Turn it on in debug builds from day one; it is the closest thing to a collector you get.⚠ Strings are 1-based, dynamic arrays are 0-based
Two indexing conventions in one language, and the compiler will not tell you which one you meant.
Option Strict On
Imports System
Module IndexGotcha
Sub Main()
Dim text As String = "Visual"
Dim values() As Integer = {10, 20, 30}
' Both zero-based
Console.WriteLine(text(0))
Console.WriteLine(values(0))
End Sub
End Moduleprogram IndexGotcha;
var
Text: string;
Values: array of Integer;
Index: Integer;
begin
Text := 'Visual';
Values := [10, 20, 30];
WriteLn(Text[1]); { strings start at 1 }
WriteLn(Values[0]); { dynamic arrays start at 0 }
{ Low/High work on ARRAYS. On a string they report the
ShortString capacity range, NOT the live indices: }
WriteLn(Low(Values), ' ', High(Values));
WriteLn(Low(Text), ' ', High(Text));
for Index := 1 to Length(Text) do
Write(Text[Index]);
WriteLn;
end.A Pascal
string is indexed from 1, a dynamic array from 0, and a fixed array from whatever bounds you declared. That inconsistency is the source of most off-by-one bugs in Pascal code, and there is a second trap on top of it: Low and High are reliable on arrays and misleading on strings — the output above shows them reporting 0 and 255, the ShortString capacity range, rather than 1 and the length. So use Low/High for arrays and 1 to Length(Text) for strings. (In Delphi's mobile compilers strings became 0-based, which caused enough trouble that Free Pascal did not follow.)⚠ No My namespace, and no .NET at all
The conveniences have counterparts — what is gone is the entire .NET base class library.
Option Strict On
Imports System
Module PlatformGotcha
Sub Main()
Console.WriteLine(Environment.MachineName.Length > 0)
Console.WriteLine(IsNumeric("42"))
Console.WriteLine(Now.Year > 2000)
End Sub
End Moduleprogram PlatformGotcha;
uses SysUtils, DateUtils;
var
Parsed: Integer;
begin
WriteLn(Length(GetEnvironmentVariable('HOME')) >= 0);
WriteLn(TryStrToInt('42', Parsed));
WriteLn(YearOf(Now) > 2000);
end.IsNumeric becomes TryStrToInt, Now is Now (the same name, from SysUtils), My.Computer.FileSystem becomes SysUtils and FileUtil. The larger point: there is no .NET. No System.Text.Json, no HttpClient, no LINQ, no async/await, no NuGet. Free Pascal's RTL and the Lazarus component library are broad and mature, but they are a different library with different names, and any code that leans on a .NET-specific type has to be rewritten rather than translated.