PONY λ M2 Modula-2

Pascal.CodeCompared.To/C#

An interactive executable cheatsheet comparing Pascal and C#

Free Pascal 3.2.2 C# 13
Hello World & Structure
Hello, World
program HelloWorld; begin writeln('Hello, World!'); end.
Console.WriteLine("Hello, World!");
C# 9 introduced top-level statements, so a complete program can be a single line. Pascal has always required a program Name; header and an explicit begin...end. block. The trailing period after end is Pascal's traditional program terminator — a full-stop that marks the end of the source file.
Comments
program CommentsDemo; begin // Single-line comment (Free Pascal extension) { Brace comment — traditional Pascal style } (* Parenthesis-star comment — also traditional *) writeln('done'); end.
// Single-line comment /* Multi-line block comment */ /// <summary>XML documentation comment</summary> Console.WriteLine("done");
Pascal offers three comment syntaxes. The { } brace style is the most traditional; (* *) is equally standard and predates brace comments; // is a Free Pascal extension from C++. C# uses the same // and /* */ as C, plus /// triple-slash XML doc comments that generate documentation automatically.
Unit / Namespace Imports
program ImportsDemo; uses SysUtils, Math; begin writeln(Format('Pi = %.4f', [Pi])); writeln(Max(10, 20)); end.
using static System.Math; Console.WriteLine($"Pi = {PI:F4}"); Console.WriteLine(Max(10, 20));
Pascal's uses clause imports entire units; the identifiers they export become directly available. C# uses using directives for namespaces, and using static to import static class members without qualification — the closest equivalent to Pascal's unit import model. The runner already includes using System;, so most standard library types are available without any directive.
Declaration Order
program StructureDemo; uses SysUtils; const Limit = 10; type Count = Integer; var result: Count; function Double(value: Integer): Integer; begin Result := value * 2; end; begin result := Double(5); writeln(result); end.
const int limit = 10; int Double(int value) => value * 2; int result = Double(5); Console.WriteLine(result);
Pascal enforces a strict top-down declaration order: usesconsttypevar → subprogram definitions → main begin...end.. Every identifier must be declared before use. C# imposes no such ordering; local functions, constants, and variables can appear in any order in a top-level program, and the compiler resolves references across the file.
Variables & Types
Variable Declaration
program VarDemo; var count: Integer; message: string; price: Double; active: Boolean; begin count := 10; message := 'hello'; price := 9.99; active := True; writeln(count, ' ', message, ' ', price:0:2, ' ', active); end.
int count = 10; string message = "hello"; double price = 9.99; bool active = true; Console.WriteLine($"{count} {message} {price:F2} {active}");
Pascal requires every variable to be declared in a var block before any code executes, with an explicit type. C# allows inline declarations anywhere a statement is legal. Pascal uses := for assignment and = for comparison; C# flips this — = assigns and == compares. Pascal's Boolean literals are True/False (capitalised); C#'s are lowercase true/false.
Type Inference
program TypesDemo; var temperature: Double; greeting: string; items: Integer; begin temperature := 36.6; greeting := 'Pascal requires explicit types'; items := 42; writeln(temperature:0:1); writeln(greeting); writeln(items); end.
var temperature = 36.6; // inferred as double var greeting = "C# can infer types"; // inferred as string var items = new List<int> { 1, 2, 3 }; // inferred as List<int> Console.WriteLine(temperature); Console.WriteLine(greeting); Console.WriteLine(items.Count);
Pascal requires an explicit type for every variable — there is no inference. C#'s var keyword lets the compiler infer the type from the right-hand side of the initialiser. The type is still fixed at compile time; var is not dynamic typing. Use explicit types in C# for clarity in public APIs and var for local variables where the type is obvious from context.
Constants
program ConstantsDemo; const MaxItems = 100; Version = '3.2'; Gravity = 9.81; begin writeln(MaxItems); writeln(Version); writeln(Gravity:0:2); end.
const int maxItems = 100; const string version = "13"; const double gravity = 9.81; Console.WriteLine(maxItems); Console.WriteLine(version); Console.WriteLine($"{gravity:F2}");
Both languages have true compile-time constants. Pascal groups them in a const block before the main body; C# uses the const keyword inline. Pascal constants are untyped by default — the compiler infers the type from the literal. C# const requires an explicit type. Both reject any attempt to assign to a constant after declaration.
Integer Types
program IntegerTypes; var small: ShortInt; { -128..127 } medium: Integer; { -2147483648..2147483647 } large: Int64; { 64-bit signed } unsigned: Cardinal; { 0..4294967295 } begin small := 127; medium := 2147483647; large := 9223372036854775807; unsigned := 4294967295; writeln(small); writeln(medium); writeln(large); writeln(unsigned); end.
sbyte small = 127; // -128..127 int medium = 2_147_483_647; // 32-bit signed long large = 9_223_372_036_854_775_807; // 64-bit signed uint unsigned = 4_294_967_295; // 0..4294967295 Console.WriteLine(small); Console.WriteLine(medium); Console.WriteLine(large); Console.WriteLine(unsigned);
Both languages offer the same set of fixed-width integer types; the names just differ. Pascal's ShortInt becomes C#'s sbyte; Integer becomes int; Int64 becomes long; Cardinal becomes uint. C# supports digit separators (2_147_483_647) to improve readability of large literals; Pascal does not.
Float Types
program FloatTypes; uses SysUtils; var temperature: Single; { 32-bit } ratio: Double; { 64-bit — preferred } begin temperature := 36.6; ratio := 1.0 / 3.0; writeln(temperature:0:1); writeln(Format('%.6f', [ratio])); end.
float temperature = 36.6f; // 32-bit — note the 'f' suffix double ratio = 1.0 / 3.0; // 64-bit — preferred Console.WriteLine($"{temperature:F1}"); Console.WriteLine($"{ratio:F6}");
Pascal's Double maps to C#'s double; Single maps to float. In C#, a floating-point literal without a suffix is double by default — adding f makes it float. Pascal uses value:width:decimals format specifiers inside writeln; C# uses composite format strings like {value:F2} inside interpolated strings or Console.WriteLine.
Type Conversion
program TypeConversion; uses SysUtils; var number: Integer; text: string; value: Double; begin number := 42; text := IntToStr(number); writeln(text + ' items'); writeln(StrToInt('99') * 2); value := 3.14; writeln(Trunc(value)); writeln(Round(value)); end.
int number = 42; string text = number.ToString(); Console.WriteLine(text + " items"); Console.WriteLine(int.Parse("99") * 2); double value = 3.14; Console.WriteLine((int)value); // truncate toward zero Console.WriteLine((int)Math.Round(value)); // round
Pascal uses procedural conversion functions from SysUtils: IntToStr, StrToInt, Trunc, Round. C# uses method calls (.ToString(), int.Parse()) and explicit cast syntax (int)value. Both languages reject implicit numeric-to-string coercion — you must convert explicitly. C#'s int.TryParse is the safe alternative to int.Parse when the input might be invalid.
Strings
Concatenation
program StringConcat; uses SysUtils; var first: string; second: string; count: Integer; begin first := 'Hello'; second := 'World'; count := 42; writeln(first + ', ' + second + '!'); writeln('Count: ' + IntToStr(count)); end.
string first = "Hello"; string second = "World"; int count = 42; Console.WriteLine(first + ", " + second + "!"); Console.WriteLine("Count: " + count); // int auto-converts to string
Both languages use + for string concatenation. Unlike Pascal, C# automatically converts numbers and other types to strings when they appear in a concatenation expression with a string — no explicit IntToStr call needed. For more complex formatting, C# string interpolation ($"...") is generally preferred over concatenation.
String Formatting / Interpolation
program StringFormat; uses SysUtils; var name: string; age: Integer; price: Double; begin name := 'Alice'; age := 30; price := 9.99; writeln(Format('%s is %d years old', [name, age])); writeln(Format('Price: $%.2f', [price])); end.
string name = "Alice"; int age = 30; double price = 9.99; Console.WriteLine($"{name} is {age} years old"); Console.WriteLine($"Price: {price:F2}");
Pascal has no string interpolation syntax; building formatted strings requires the Format function (from SysUtils) with printf-style placeholders. C#'s interpolated string syntax ($"...{expression}...") embeds expressions directly in string literals. Format specifiers like :F2 or :N0 inside the braces control number formatting.
String Methods
program StringMethods; uses SysUtils; var text: string; begin text := ' Hello, World! '; writeln(Trim(text)); writeln(UpperCase(text)); writeln(LowerCase(text)); writeln(Length(Trim(text))); end.
string text = " Hello, World! "; Console.WriteLine(text.Trim()); Console.WriteLine(text.ToUpper()); Console.WriteLine(text.ToLower()); Console.WriteLine(text.Trim().Length);
Pascal string functions are free-standing procedures and functions (from SysUtils): Trim(s), UpperCase(s), Length(s). C# follows the object-oriented model: strings have methods called with dot notation — s.Trim(), s.ToUpper(), s.Length (a property, not a method). C#'s string type also has Contains, StartsWith, EndsWith, Replace, Split, and many more.
Search & Substring
program StringSearch; var text: string; position: Integer; begin text := 'Hello, World!'; position := Pos('World', text); writeln(position); { 8 — 1-indexed } writeln(position > 0); { TRUE if found } writeln(Copy(text, 1, 5)); { "Hello" } writeln(Copy(text, 8, 6)); { "World!" } end.
string text = "Hello, World!"; int position = text.IndexOf("World"); Console.WriteLine(position); // 7 — 0-indexed Console.WriteLine(position >= 0); // true if found Console.WriteLine(text.Substring(0, 5)); // "Hello" Console.WriteLine(text[7..]); // "World!" — C# 8 range syntax
Pascal's Pos returns a 1-based position (0 means not found); C#'s IndexOf returns a 0-based position (-1 means not found). Pascal's Copy(str, start, count) extracts a substring with a 1-based start; C# offers Substring(start, length) (0-based) and the modern range syntax str[start..end]. The off-by-one difference between 1-based and 0-based indexing is the most common source of bugs when translating Pascal logic to C#.
Split & Join
{$H+} program StringSplit; uses SysUtils, StrUtils, Types; var sentence: string; words: TStringDynArray; index: Integer; begin sentence := 'one two three'; words := SplitString(sentence, ' '); for index := 0 to Length(words) - 1 do writeln(words[index]); end.
string sentence = "one two three"; string[] words = sentence.Split(' '); foreach (string word in words) Console.WriteLine(word); string joined = string.Join(", ", words); Console.WriteLine(joined);
Pascal's SplitString (from StrUtils) returns a TStringDynArray, a zero-indexed dynamic array of strings. C#'s string.Split returns a string[] and integrates naturally with foreach and LINQ. C# also provides string.Join to re-combine an array with a separator — Pascal has no direct equivalent and requires a manual loop.
Collections
Fixed-Size Array
program FixedArray; var numbers: array[1..5] of Integer; index: Integer; begin numbers[1] := 10; numbers[2] := 20; numbers[3] := 30; numbers[4] := 40; numbers[5] := 50; for index := 1 to 5 do writeln(numbers[index]); end.
int[] numbers = { 10, 20, 30, 40, 50 }; foreach (int number in numbers) Console.WriteLine(number); Console.WriteLine(numbers.Length);
Pascal fixed arrays use a 1-based index by default (array[1..5]) though you can start at any ordinal value. C# arrays are always 0-based. Pascal array declarations specify the index range; C# array declarations specify only the element type and the size is implicit from the initialiser. C# arrays have a Length property; Pascal uses the global Length(arr) function.
Dynamic Array / List
program DynamicArray; var numbers: array of Integer; begin SetLength(numbers, 3); numbers[0] := 10; numbers[1] := 20; numbers[2] := 30; SetLength(numbers, 4); numbers[3] := 40; writeln(Length(numbers)); writeln(numbers[2]); end.
var numbers = new List<int> { 10, 20, 30 }; numbers.Add(40); Console.WriteLine(numbers.Count); Console.WriteLine(numbers[2]); numbers.Remove(20); Console.WriteLine(numbers.Count);
Pascal's dynamic array (array of T) is resized with SetLength — there is no built-in Add method. C# List<T> is the standard growable collection with Add, Remove, Insert, Count, and full LINQ support. Anders Hejlsberg brought the concept of type-safe generic collections from Delphi to C# — Pascal programmers will find List<T> immediately intuitive despite the different syntax.
Dictionary / Map
program DictionaryDemo; type TEntry = record key: string; value: Integer; end; var scores: array[1..3] of TEntry; index: Integer; begin scores[1].key := 'Alice'; scores[1].value := 95; scores[2].key := 'Bob'; scores[2].value := 87; scores[3].key := 'Carol'; scores[3].value := 92; for index := 1 to 3 do writeln(scores[index].key, ': ', scores[index].value); end.
var scores = new Dictionary<string, int> { { "Alice", 95 }, { "Bob", 87 }, { "Carol", 92 }, }; Console.WriteLine(scores["Alice"]); scores["Dave"] = 78; foreach (var entry in scores) Console.WriteLine($"{entry.Key}: {entry.Value}");
Standard Pascal has no built-in associative map. The idiomatic workaround is an array of records with key and value fields, searched linearly. C#'s Dictionary<TKey, TValue> provides O(1) hash-map lookups and is one of the most frequently used collection types. Free Pascal's TFPGMap from the fgl unit is the closest equivalent, but it sorts by key and uses a binary search rather than hashing.
Iteration Patterns
program Iteration; var fruits: array[1..4] of string; index: Integer; begin fruits[1] := 'apple'; fruits[2] := 'banana'; fruits[3] := 'cherry'; fruits[4] := 'date'; for index := 1 to Length(fruits) do writeln(index, ': ', fruits[index]); end.
var fruits = new List<string> { "apple", "banana", "cherry", "date" }; foreach (string fruit in fruits) Console.WriteLine(fruit); for (int i = 0; i < fruits.Count; i++) Console.WriteLine($"{i + 1}: {fruits[i]}");
Pascal has no foreach — iterating a collection always uses a numeric for loop with an explicit index. C# provides foreach for element-at-a-time iteration over any IEnumerable, which is more expressive for the common case where the index is not needed. The traditional indexed for loop (for (int i = 0; ...)) is still available when the index matters.
Control Flow
If / Else
program IfElse; var score: Integer; begin score := 85; if score >= 90 then writeln('A') else if score >= 80 then writeln('B') else if score >= 70 then writeln('C') else writeln('Below C'); end.
int score = 85; if (score >= 90) Console.WriteLine("A"); else if (score >= 80) Console.WriteLine("B"); else if (score >= 70) Console.WriteLine("C"); else Console.WriteLine("Below C");
The if/else structure is nearly identical between the two languages. The key differences are: Pascal uses then after the condition; C# uses parentheses around the condition and curly braces (or single-statement bodies without braces). Pascal places a semicolon before else only when there is no begin...end block — a common source of Pascal syntax errors.
Case / Switch Expression
program CaseDemo; var dayNumber: Integer; dayName: string; begin dayNumber := 3; case dayNumber of 1: dayName := 'Monday'; 2: dayName := 'Tuesday'; 3: dayName := 'Wednesday'; 4, 5: dayName := 'Thursday or Friday'; 6, 7: dayName := 'Weekend'; else dayName := 'Unknown'; end; writeln(dayName); end.
int dayNumber = 3; string dayName = dayNumber switch { 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 or 5 => "Thursday or Friday", 6 or 7 => "Weekend", _ => "Unknown", }; Console.WriteLine(dayName);
Pascal's case...of is a statement matching ordinal values. C# 8 introduced switch expressions, which are expressions that return a value — the entire construct can be used on the right side of an assignment. Multiple values share a branch in Pascal with a comma list; C# uses the or pattern (4 or 5 =>). Neither language falls through between branches by default.
Pattern Matching
program PatternDemo; var value: Integer; begin value := 42; if value > 0 then writeln('positive') else if value = 0 then writeln('zero') else writeln('negative'); end.
object value = 42; string description = value switch { int n when n > 100 => "large number", int n when n > 0 => "positive number", 0 => "zero", string s => $"string: {s}", _ => "something else", }; Console.WriteLine(description);
C# has substantially richer pattern matching than Pascal. C# switch expressions can match on type (int n), value, and guard conditions (when n > 0) in a single construct, and can also discriminate on the runtime type of an object. Pascal's case statement matches only on ordinal values — there is no type-testing pattern. C#'s pattern matching is one of the features that has no Pascal equivalent.
Loops
For Loop
program ForLoops; var counter: Integer; begin for counter := 1 to 5 do writeln(counter); writeln('---'); for counter := 5 downto 1 do writeln(counter); end.
for (int counter = 1; counter <= 5; counter++) Console.WriteLine(counter); Console.WriteLine("---"); for (int counter = 5; counter >= 1; counter--) Console.WriteLine(counter);
Pascal's for counter := 1 to 5 do always steps by exactly 1; downto steps by -1. There is no step-size option. C#'s for loop is more general: any initialiser, condition, and increment expression are allowed, so any step size is possible. C# also has foreach for iterating collections without an index.
While Loop
program WhileLoop; var count: Integer; begin count := 1; while count <= 5 do begin writeln(count); count := count + 1; end; end.
int count = 1; while (count <= 5) { Console.WriteLine(count); count++; }
The while loop is structurally identical in both languages. Pascal wraps a multi-statement body in begin...end; C# uses braces. Pascal has no ++ or += operators — increment is written as count := count + 1 or using the built-in Inc(count) procedure. C# supports count++, count += 1, and ++count.
Repeat-Until / Do-While
program RepeatUntil; var number: Integer; begin number := 0; repeat number := number + 1; writeln(number); until number >= 5; end.
int number = 0; do { number++; Console.WriteLine(number); } while (number < 5);
Both languages provide a post-test loop that always executes the body at least once. Pascal's repeat...until condition exits when the condition becomes true. C#'s do...while (condition) exits when the condition becomes false — the exit condition is the inverse. This is a subtle but important difference: converting a repeat...until to do...while requires negating the condition.
Break & Continue
program BreakContinue; var number: Integer; begin for number := 1 to 10 do begin if number mod 2 = 0 then Continue; if number > 7 then Break; writeln(number); end; end.
for (int number = 1; number <= 10; number++) { if (number % 2 == 0) continue; if (number > 7) break; Console.WriteLine(number); }
Pascal uses Break and Continue (capitalised, as they are built-in procedures); C# uses lowercase break and continue (reserved keywords). The semantics are identical: break exits the innermost loop; continue skips the rest of the current iteration and begins the next. Pascal's modulo operator is mod; C# uses the % operator.
Procedures & Methods
Procedure (Void Method)
program ProcedureDemo; procedure Greet(name: string); begin writeln('Hello, ', name, '!'); end; begin Greet('World'); Greet('Pascal'); end.
void Greet(string name) { Console.WriteLine($"Hello, {name}!"); } Greet("World"); Greet("C#");
Pascal distinguishes between procedures (which return nothing) and functions (which return a value). C# unifies them: a method that returns nothing uses the void
Function (Return Value)
program FunctionDemo; function Square(number: Integer): Integer; begin Result := number * number; end; function Greet(name: string): string; begin Result := 'Hello, ' + name + '!'; end; begin writeln(Square(7)); writeln(Greet('World')); end.
int Square(int number) => number * number; string Greet(string name) => $"Hello, {name}!"; Console.WriteLine(Square(7)); Console.WriteLine(Greet("World"));
Pascal functions use Result := value to set the return value (a special built-in variable in Free Pascal). Standard Pascal used the function's own name as the return assignment target (Square := n * n). C# uses return value or, for single-expression methods, expression-body syntax (=> expression). There is no separate procedure/function keyword in C# — the return type alone makes the distinction.
Pass by Reference
program VarParameter; procedure Swap(var first: Integer; var second: Integer); var temporary: Integer; begin temporary := first; first := second; second := temporary; end; var alpha, beta: Integer; begin alpha := 10; beta := 20; Swap(alpha, beta); writeln(alpha, ' ', beta); end.
void Swap(ref int first, ref int second) { int temporary = first; first = second; second = temporary; } int alpha = 10, beta = 20; Swap(ref alpha, ref beta); Console.WriteLine($"{alpha} {beta}");
Pascal's var parameter modifier passes arguments by reference; changes inside the procedure affect the caller's variable. C# uses the ref keyword at both the declaration site and the call site (Swap(ref alpha, ref beta)). C# also has out for parameters that are only assigned (not read) inside the method, which Pascal approximates with uninitialized var parameters.
Default / Optional Parameters
program DefaultParams; procedure Log(message: string; prefix: string = 'INFO'); begin writeln('[' + prefix + '] ' + message); end; begin Log('Server started'); Log('Something failed', 'ERROR'); end.
void Log(string message, string prefix = "INFO") { Console.WriteLine($"[{prefix}] {message}"); } Log("Server started"); Log("Something failed", "ERROR");
Both languages support optional parameters with default values using identical syntax — the default value follows a = sign in the parameter list. Parameters with defaults must appear after required parameters in both languages. C# additionally supports named arguments at the call site (Log(message: "text", prefix: "WARN")), which Pascal does not.
Overloading
program Overloading; uses SysUtils; function Describe(value: Integer): string; overload; begin Result := 'Integer: ' + IntToStr(value); end; function Describe(value: Double): string; overload; begin Result := 'Double: ' + FloatToStr(value); end; function Describe(value: string): string; overload; begin Result := 'String: ' + value; end; begin writeln(Describe(42)); writeln(Describe(3.14)); writeln(Describe('hello')); end.
Console.WriteLine(Formatter.Describe(42)); Console.WriteLine(Formatter.Describe(3.14)); Console.WriteLine(Formatter.Describe("hello")); // Local functions cannot be overloaded — use a static helper class static class Formatter { public static string Describe(int value) => $"Integer: {value}"; public static string Describe(double value) => $"Double: {value}"; public static string Describe(string value) => $"String: {value}"; }
Both languages support method overloading — multiple definitions with the same name but different parameter types. Free Pascal requires the overload directive on each overloaded variant. C# infers overloading automatically from the parameter signatures, with no keyword needed. The compiler selects the best matching overload at the call site in both languages.
Classes & OOP
Class Definition
program ClassDemo; uses SysUtils; type TAnimal = class private FName: string; public constructor Create(aName: string); function Speak: string; property Name: string read FName; end; constructor TAnimal.Create(aName: string); begin FName := aName; end; function TAnimal.Speak: string; begin Result := FName + ' makes a sound'; end; var animal: TAnimal; begin animal := TAnimal.Create('Dog'); writeln(animal.Speak); writeln(animal.Name); animal.Free; end.
var animal = new Animal("Dog"); Console.WriteLine(animal.Speak()); Console.WriteLine(animal.Name); class Animal { public string Name { get; } public Animal(string name) => Name = name; public virtual string Speak() => $"{Name} makes a sound"; }
Pascal class bodies separate declaration (in the type block) from implementation (the method bodies written after the type block). C# defines declaration and implementation together inside the class braces. Pascal private fields use the F prefix by convention (FName). Pascal requires manual memory management with Free; C# has garbage collection. Properties in Pascal use explicit read/write field references; C# auto-properties do this in one line.
Inheritance
program InheritanceDemo; type TShape = class public function Area: Double; virtual; abstract; procedure Describe; end; TCircle = class(TShape) private FRadius: Double; public constructor Create(radius: Double); function Area: Double; override; end; procedure TShape.Describe; begin writeln('Area = ', Area:0:2); end; constructor TCircle.Create(radius: Double); begin FRadius := radius; end; function TCircle.Area: Double; begin Result := 3.14159 * FRadius * FRadius; end; var circle: TCircle; begin circle := TCircle.Create(5.0); circle.Describe; circle.Free; end.
var circle = new Circle(5.0); circle.Describe(); abstract class Shape { public abstract double Area(); public void Describe() => Console.WriteLine($"Area = {Area():F2}"); } class Circle(double radius) : Shape { public override double Area() => Math.PI * radius * radius; }
Inheritance syntax differs significantly. Pascal uses class(ParentClass) for the base class and the virtual; abstract; directive pair for abstract methods; overrides require override. C# uses : ParentClass, abstract on the method, and override on the implementation. C# 12 primary constructors (class Circle(double radius)) eliminate the constructor boilerplate entirely.
Interfaces
program InterfaceDemo; type IGreeter = interface function Greet(name: string): string; end; TFormalGreeter = class(TInterfacedObject, IGreeter) public function Greet(name: string): string; end; function TFormalGreeter.Greet(name: string): string; begin Result := 'Good day, ' + name + '.'; end; var greeter: IGreeter; begin greeter := TFormalGreeter.Create; writeln(greeter.Greet('Alice')); end.
IGreeter greeter = new FormalGreeter(); Console.WriteLine(greeter.Greet("Alice")); interface IGreeter { string Greet(string name); } class FormalGreeter : IGreeter { public string Greet(string name) => $"Good day, {name}."; }
Interfaces work the same in both languages. Pascal requires class implementations to inherit from TInterfacedObject (which provides COM-compatible reference counting) when implementing an interface. C# classes implement interfaces simply by listing them after :. C# interfaces can also define default implementations for methods (a C# 8 feature), which Pascal interfaces cannot.
Records
Record Type
program RecordDemo; type TPoint = record X: Double; Y: Double; end; var point1, point2: TPoint; begin point1.X := 3.0; point1.Y := 4.0; point2 := point1; { value copy } point2.X := 10.0; writeln(point1.X:0:1, ', ', point1.Y:0:1); { 3.0, 4.0 — unchanged } writeln(point2.X:0:1, ', ', point2.Y:0:1); { 10.0, 4.0 } end.
var point1 = new Point(3.0, 4.0); var point2 = point1 with { X = 10.0 }; // non-destructive update Console.WriteLine($"{point1.X}, {point1.Y}"); // 3, 4 — unchanged Console.WriteLine($"{point2.X}, {point2.Y}"); // 10, 4 record Point(double X, double Y);
Pascal records are value types: assigning one record to another copies all fields, and modifying the copy does not affect the original. C# records (introduced in C# 9) are also value-semantic by default and support non-destructive mutation via the with expression. C# positional records (record Point(double X, double Y)) declare both the type and a primary constructor in one line. C# record structs exist too, explicitly stack-allocated like Pascal records.
Value Equality
program RecordEquality; type TPoint = record X, Y: Integer; end; var alpha, beta: TPoint; begin alpha.X := 1; alpha.Y := 2; beta.X := 1; beta.Y := 2; writeln(alpha.X = beta.X); { field-by-field comparison } writeln(alpha.Y = beta.Y); { Pascal has no built-in record equality operator } end.
var alpha = new Point(1, 2); var beta = new Point(1, 2); Console.WriteLine(alpha == beta); // True — value equality Console.WriteLine(alpha.Equals(beta)); // True record Point(int X, int Y);
Pascal has no built-in equality operator for records — you must compare each field individually. C# records automatically synthesise == and Equals based on all properties, providing structural equality out of the box. This is one of the most significant quality-of-life improvements C# records offer over Pascal records for immutable data modelling.
Error Handling
Try / Except (Catch)
program TryExcept; uses SysUtils; var result: Integer; begin try result := StrToInt('not a number'); writeln(result); except on error: EConvertError do writeln('Conversion error: ', error.Message); on error: Exception do writeln('Unknown error: ', error.Message); end; end.
try { int result = int.Parse("not a number"); Console.WriteLine(result); } catch (FormatException error) { Console.WriteLine($"Conversion error: {error.Message}"); } catch (Exception error) { Console.WriteLine($"Unknown error: {error.Message}"); }
The structure is nearly identical. Pascal uses try...except with on ErrorVariable: ExceptionType do for typed catches; C# uses try...catch (ExceptionType variable). Pascal's except can also contain an untyped else clause for a catch-all. Exception class names in Pascal typically start with E (e.g., EConvertError); C# uses a Exception suffix (e.g., FormatException). The inheritance hierarchy is structurally the same.
Finally Block
program TryFinally; uses SysUtils; var resource: string; begin resource := 'open'; try try writeln('Using resource'); raise Exception.Create('Something went wrong'); finally resource := 'closed'; writeln('Resource: ', resource); end; except on error: Exception do writeln('Caught: ', error.Message); end; end.
string resource = "open"; try { Console.WriteLine("Using resource"); throw new Exception("Something went wrong"); } catch (Exception error) { Console.WriteLine($"Caught: {error.Message}"); } finally { resource = "closed"; Console.WriteLine($"Resource: {resource}"); }
Both languages support finally blocks that execute regardless of whether an exception was raised. Pascal uses raise ExceptionClass.Create(message) to throw; C# uses throw new ExceptionClass(message). Pascal's try...finally is separate from try...except — you cannot combine them in one block as C# allows with catch + finally together.
Custom Exception
program CustomException; uses SysUtils; type EValidationError = class(Exception) private FFieldName: string; public constructor Create(fieldName, messageText: string); property FieldName: string read FFieldName; end; constructor EValidationError.Create(fieldName, messageText: string); begin inherited Create(messageText); FFieldName := fieldName; end; begin try raise EValidationError.Create('email', 'Invalid email address'); except on error: EValidationError do writeln('Field "', error.FieldName, '": ', error.Message); end; end.
try { throw new ValidationException("email", "Invalid email address"); } catch (ValidationException error) { Console.WriteLine($"Field '{error.FieldName}': {error.Message}"); } class ValidationException(string fieldName, string message) : Exception(message) { public string FieldName { get; } = fieldName; }
Custom exceptions follow the same pattern in both languages: inherit from the base exception class and add properties. Pascal convention names exception classes with an E prefix; C# convention uses an Exception suffix. C# 12 primary constructors make the declaration much more compact. Both languages call the parent constructor to set the Message property.
LINQ & Functional
Filtering a Collection
program FilterDemo; var numbers: array[1..8] of Integer; evens: array of Integer; count: Integer; index: Integer; begin numbers[1] := 1; numbers[2] := 2; numbers[3] := 3; numbers[4] := 4; numbers[5] := 5; numbers[6] := 6; numbers[7] := 7; numbers[8] := 8; count := 0; for index := 1 to 8 do if numbers[index] mod 2 = 0 then Inc(count); SetLength(evens, count); count := 0; for index := 1 to 8 do if numbers[index] mod 2 = 0 then begin evens[count] := numbers[index]; Inc(count); end; for index := 0 to Length(evens) - 1 do writeln(evens[index]); end.
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 }; var evens = numbers.Where(n => n % 2 == 0).ToList(); foreach (int number in evens) Console.WriteLine(number);
Pascal has no built-in functional collection operations. Filtering requires a manual loop with an accumulator. C#'s LINQ Where method accepts a lambda predicate and returns an IEnumerable<T> — a one-liner that replaces a dozen lines of Pascal. LINQ was directly influenced by Hejlsberg's earlier work on database query integration in Delphi (ClientDataSet, DataSnap) and is one of the most cited features C# programmers miss in Pascal.
Mapping / Transforming
program MapDemo; uses SysUtils; var names: array[1..3] of string; greetings: array[1..3] of string; index: Integer; begin names[1] := 'Alice'; names[2] := 'Bob'; names[3] := 'Carol'; for index := 1 to 3 do greetings[index] := 'Hello, ' + names[index] + '!'; for index := 1 to 3 do writeln(greetings[index]); end.
var names = new List<string> { "Alice", "Bob", "Carol" }; var greetings = names.Select(name => $"Hello, {name}!").ToList(); foreach (string greeting in greetings) Console.WriteLine(greeting);
LINQ's Select applies a transformation function to every element — equivalent to map/collect in other languages. Pascal requires a manual loop. LINQ operations are composable and lazy: Where and Select can be chained before calling ToList(), and the actual iteration happens only once at the end.
Lambda Expressions
program LambdaDemo; type TIntOp = function(value: Integer): Integer; function ApplyTwice(operation: TIntOp; value: Integer): Integer; begin Result := operation(operation(value)); end; function Double(value: Integer): Integer; begin Result := value * 2; end; begin writeln(ApplyTwice(@Double, 3)); { 12 } end.
Func<int, int> doubleIt = n => n * 2; Func<int, int> square = n => n * n; int ApplyTwice(Func<int, int> operation, int value) => operation(operation(value)); Console.WriteLine(ApplyTwice(doubleIt, 3)); // 12 Console.WriteLine(ApplyTwice(square, 3)); // 81
Pascal supports passing functions as parameters using function-type variables (TIntOp = function(...): ...). Named functions are referenced with @. C# uses Func<T, TResult> and Action<T> delegate types, and supports inline lambda expressions (n => n * 2) — anonymous functions defined at the point of use. C# lambdas can capture variables from the enclosing scope (closures); Pascal function-type variables cannot.
LINQ Chaining
program ChainDemo; uses SysUtils; var scores: array[1..5] of Integer; total: Integer; count: Integer; index: Integer; begin scores[1] := 45; scores[2] := 82; scores[3] := 91; scores[4] := 63; scores[5] := 77; total := 0; count := 0; for index := 1 to 5 do if scores[index] >= 70 then begin total := total + scores[index]; Inc(count); end; if count > 0 then writeln(Format('Average of passing scores: %.1f', [total / count])); end.
var scores = new List<int> { 45, 82, 91, 63, 77 }; double average = scores .Where(score => score >= 70) .Average(); Console.WriteLine($"Average of passing scores: {average:F1}");
LINQ operations chain together fluently: Where filters, then Average computes the mean of the remaining elements — what would be a 10-line Pascal loop becomes two readable lines. Other useful LINQ aggregates include Sum, Min, Max, Count, Any, All, and First/FirstOrDefault. The entire LINQ library is one of the most celebrated features in modern C#.
Type System
Nullable Types
program NullableDemo; var value: Integer; hasValue: Boolean; begin { Pascal has no null for value types; use a flag variable as a sentinel } hasValue := False; value := 0; if hasValue then writeln(value) else writeln('no value'); hasValue := True; value := 42; if hasValue then writeln(value); end.
int? value = null; // nullable int Console.WriteLine(value.HasValue); // False Console.WriteLine(value ?? -1); // -1 (null-coalescing) value = 42; Console.WriteLine(value.HasValue); // True Console.WriteLine(value.Value); // 42 Console.WriteLine(value ?? -1); // 42
Pascal value types (integers, booleans, records) cannot be null — the programmer must use a Boolean sentinel flag to represent "no value." C#'s int? (equivalent to Nullable<int>) gives any value type a null state, accessed via .HasValue and .Value. The null-coalescing operator (??) returns the right operand when the left is null, making null handling concise.
Enumerations
program EnumDemo; type TDirection = (North, South, East, West); var direction: TDirection; begin direction := North; writeln(Ord(direction)); { 0 } writeln(direction = North); { TRUE } if direction = North then writeln('Heading north'); for direction := North to West do writeln(Ord(direction)); end.
Direction direction = Direction.North; Console.WriteLine((int)direction); // 0 Console.WriteLine(direction == Direction.North); // True Console.WriteLine(direction); // North — name printed if (direction == Direction.North) Console.WriteLine("Heading north"); enum Direction { North, South, East, West }
Both languages have first-class enumeration types. Pascal enumerations print their ordinal value; C# enumerations print their name by default. Pascal enum members are accessed without qualification (North); C# requires the type name (Direction.North). C# also supports enum types with explicit underlying integer values (enum Status { Active = 1, Inactive = 2 }) and "flags enums" using [Flags].
Generics
program GenericsDemo; uses SysUtils; generic function MaxOf<T>(first, second: T): T; begin { In practice, Pascal generics need constraints } Result := first; end; type generic TStack<T> = class private FItems: array of T; public procedure Push(item: T); function Pop: T; function Count: Integer; end; procedure TStack.Push(item: T); begin SetLength(FItems, Length(FItems) + 1); FItems[High(FItems)] := item; end; function TStack.Pop: T; begin Result := FItems[High(FItems)]; SetLength(FItems, Length(FItems) - 1); end; function TStack.Count: Integer; begin Result := Length(FItems); end; var stack: specialize TStack<Integer>; begin stack := specialize TStack<Integer>.Create; stack.Push(10); stack.Push(20); writeln(stack.Pop); writeln(stack.Count); stack.Free; end.
var stack = new Stack<int>(); stack.Push(10); stack.Push(20); Console.WriteLine(stack.Pop()); // 20 Console.WriteLine(stack.Count); // 1 class Stack<T> { private readonly List<T> items = []; public void Push(T item) => items.Add(item); public T Pop() { T item = items[^1]; items.RemoveAt(items.Count - 1); return item; } public int Count => items.Count; }
Both languages support generics, but the syntax differs substantially. Free Pascal uses generic and specialize keywords; C# uses angle-bracket notation consistently. C# generics were designed by Hejlsberg based on his experience with Delphi templates — they are arguably cleaner because the constraint system (where T : IComparable<T>) is more expressive. C#'s items[^1] uses the index-from-end operator introduced in C# 8.