1. Вступ: Для чого це потрібно?
У світі існує безліч книг про Pascal, однак, багато з них говорять про "застарілий" Pascal, без класів, модулів, дженериків.
Тож я написав цей короткий вступ до того, що я називаю сучасний Object Pascal. Більшість програмістів, які його використовують, насправді не називають його "сучасний Object Pascal", ми просто називаємо його "наш Pascal". Але, представляючи мову, я вважаю важливим підкреслити, що це сучасна, об’єктно-орієнтована мова. Він багато еволюціонував після старого (Turbo) Pascal, який багато людей вивчали колись в школах. За функціями він дуже схожий на C++, Java або C#.
-
Pascal має всі сучасні особливості, які можна очікувати - класи, модульну структуру, інтерфейси, дженеріки…
-
Pascal компілюється в швидкий машинний код.
-
Pascal є типобезпечною мовою, що в деяких випадках суттєво спрощує налагодження.
-
В основному Pascal - високорівнева мова, проте він має можливість використовувати низькорівневі підходи, якщо це необхідно.
Він також має чудовий кросплатформовий компілятор з відкритим вихідним кодом: Free Pascal Compiler (http://freepascal.org/). Для нього існує декілька IDE (включаючих в себе редактор, відлагоджувач, бібліотеки компонентів, дизайнер форм), одна з найвідоміших називається Lazarus (http://lazarus.freepascal.org/). Автор цієї книги є творцем Castle Game Engine (https://castle-engine.io/) - котрий є потужним дво- і тримірним ігровим рушієм, що використовує цю мову, для створення ігор під багато платформ: Windows, Linux, MacOSX, Android, iOS, Nintendo Switch; також очікується WebGL.
Цей вступ здебільшого спрямований на програмістів, які вже мають досвід роботи з іншими мовами. Ми не розглядатимемо значення деяких універсальних понять, таких як "що таке клас", ми лише покажемо, як це зробити в Pascal.
Прим.перекл. Тут і далі ми будемо використовувати оригінальні англомовні терміни, такі як unit, constructor, override, reference-counting у випадках, якщо вони є ключовими словами мови або не мають загальноприйнятих перекладів на українську мову. Також часто надається перевага використанню оригінального англійського слова його транслітерації, як у випадку з поняттям "generic" вище.
2. Основи
2.1. Програма "Привіт, світ!"
За доброю традицією знайомство з мовою починають із самої базової та простої програми "Привіт, світ!". На Pascal вона виглядає так:
{$mode objfpc}{$H+}{$J-} // Просто використовуйте цей рядок у всіх сучасних сирцях
program MyProgram; // Збережіть цей файл як myprogram.lpr
begin
WriteLn('Привіт, світ!');
end.
Це повноцінна програма, яку можна компілювати та запускати.
-
Якщо Ви користуєтесь компілятором FPC з командного рядка, просто створіть новий файл
myprogram.lpr
з кодом програми всередині й виконайте командуfpc myprogram.lpr
. -
Якщо Ви використовуєте Lazarus, створіть новий проект (меню Проєкт → Новий проєкт → Проста програма). Збережіть його як
myprogram
і вставте цей вихідний код як основний файл. Скомпілюйте за допомогою пункту меню Виконання → Компілювати. -
Це програма командного рядка, тому в будь-якому випадку просто запустіть скомпільований виконуваний файл із командного рядка.
Решта цієї книги розповідає про мову Object Pascal, тому не очікуйте побачити щось більш привабливе, ніж речі командного рядка. Якщо Ви хочете побачити щось класне, просто створіть новий проект GUI у Lazarus (Проєкт → Новий проєкт → Застосунок). Вуаля — робоча програма з графічним інтерфейсом користувача, кросплатформна, з рідним виглядом усюди, використовуючи зручну бібліотеку візуальних компонентів. Lazarus і Free Pascal Compiler постачаються з великою кількістю готових модулів для роботи в мережі, GUI, бази даних, форматів файлів (XML, json, зображення…), потоків і всього іншого, що може знадобитися. Я вже згадував свій крутий Castle Game Engine раніше :)
2.2. Functions, procedures, primitive types
{$mode objfpc}{$H+}{$J-}
program MyProgram;
procedure MyProcedure(const A: Integer);
begin
WriteLn('A + 10 is: ', A + 10);
end;
function MyFunction(const S: string): string;
begin
Result := S + 'рядки керуються автоматично';
end;
var
X: Single;
begin
WriteLn(MyFunction('Примітка: '));
MyProcedure(5);
// Ділення за допомогою "/" завжди дає результат з плаваючою точкою, використовуйте "div" для цілочисельного ділення
X := 15 / 5;
WriteLn('X is now: ', X); // науковий запис
WriteLn('X is now: ', X:1:2); // 2 десяткові позиції
end.
Щоб повернути значення з функції, присвойте щось магічній змінні Result
. Можна вільно читати і встановлювати Result
, наче локальній змінні.
function MyFunction(const S: string): string;
begin
Result := S + 'щось';
Result := Result + ' щось більше!';
Result := Result + ' і більше!';
end;
Також можна розглядати ім’я функції (наприклад, MyFunction
у прикладі вище) як змінну, якій можна присвоїти значення. Але я б не рекомендував це використовувати в новому коді, оскільки коли ім’я функції використовується в правій частині виразу присвоєння, це виглядає підозрілим.
Якщо потрібно викликати саму функцію рекурсивно, Ви, звичайно, можете це зробити. Якщо Ви викликаєте функцію без параметрів рекурсивно, обов’язково вкажть дужки "()" після назви функці (хоча в Паскалі Ви зазвичай можете опускати дужки для функцій без параметрів), це робить рекурсивний виклик функції без параметрів, що відрізняється від доступу до поточного результату цієї функції. Ось так:
function SumIntegersUntilZero: Integer;
var
I: Integer;
begin
Readln(I);
Result := I;
if I <> 0 then
Result := Result + SumIntegersUntilZero();
end;
Ви можете викликати Exit
, щоб завершити виконання процедури або функції до того, як вона досягне остаточного end;
. Якщо ви викликаєте Exit
без параметрів у функції, вона поверне останнє, що ви встановили як Result
. Ви також можете використовувати конструкцію Exit(X)
, щоб установити результат функції та вийти зараз — це так само, як конструкція return X
у C-подібних мовах.
function AddName(const ExistingNames, NewName: string): string;
begin
if ExistingNames = '' then
Exit(NewName);
Result := ExistingNames + ', ' + NewName;
end;
Зверніть увагу, що результат функції можна відхилити. Будь-яку функцію можна використовувати як процедуру. Це має сенс, якщо функція має якийсь побічний ефект (наприклад, вона змінює глобальну змінну), крім обчислення результату. Наприклад:
var
Count: Integer;
MyCount: Integer;
function CountMe: Integer;
begin
Inc(Count);
Result := Count;
end;
begin
Count := 10;
CountMe; // результат функції відкидається, але функція виконується, Count тепер дорівнює 11
MyCount := CountMe; // використовуйте результат функції, MyCount дорівнює Count, який тепер дорівнює 12
end.
2.3. Тестування (if)
Використовуйте кострукції if .. then
чи if .. then .. else
щоб виконати певний код коли задоволена певна умова. На відміну від C-подібних мов, у Pascal вам не потрібно закривати умову в дужки.
var
A: Integer;
B: boolean;
begin
if A > 0 then
DoSomething;
if A > 0 then
begin
DoSomething;
AndDoSomethingMore;
end;
if A > 10 then
DoSomething
else
DoSomethingElse;
// еквівалент вище
B := A > 10;
if B then
DoSomething
else
DoSomethingElse;
end;
"else" поєднується з останнім "if". Отже, це працює так, як ви очікуєте:
if A <> 0 then
if B <> 0 then
AIsNonzeroAndBToo
else
AIsNonzeroButBIsZero;
Хоча наведений вище приклад із вкладеним if
правильний, у таких випадках часто краще розмістити вкладений if
всередині блоку begin
… end
. Це робить код більш очевидним для читача, і він залишатиметься очевидним, навіть якщо ви зіпсуєте відступ. Покращена версія прикладу наведена нижче. Коли ви додаєте або видаляєте речення else
у наведеному нижче коді, очевидно, до якої умови воно застосовуватиметься (до тесту A
чи B
), тому воно менш схильне до помилок.
if A <> 0 then
begin
if B <> 0 then
AIsNonzeroAndBToo
else
AIsNonzeroButBIsZero;
end;
2.4. Логічні, реляційні та побітові оператори
Логічні оператори називаються and
, or
, not
, xor
. Їх сенс, мабуть, очевидний (пошукайте "exclusive or" якщо Ви не впевннені що виконує xor :)). Вони приймають логічні аргументи, і повертають результат типу boolean. Вони також можуть виконувати роль побітових операторів у випадку коли обидва аргументи - цілочисельні значення, у цьому випадку вони повертають ціле число.
Оператори відношення (порівняння) наступні: =
, <>
, >
, <
, <=
, >=
. Якщо ви звикли до C-подібних мов, зауважте, що в Pascal ви порівнюєте два значення (перевіряєте, чи вони рівні) за допомогою одного символу рівності A = B
(на відміну від C, де ви використовуєте A == B
). Спеціальним оператором присвоєння в Pascal є :=
.
Логічні (або побітові) оператори мають вищий пріоритет, ніж оператори відношення. Вам може знадобитися використовувати дужки навколо деяких виразів, щоб мати потрібний порядок обчислень.
Ось, наприклад, помилка компіляції:
var
A, B: Integer;
begin
if A = 0 and B <> 0 then ... // НЕПРАВИЛЬНИЙ приклад
Наведене вище не компілюється, тому що компілятор спочатку хоче виконати побітове and
в середині виразу: (0 and B)
. Це побітова операція, яка повертає ціле значення. Потім компілятор застосовує оператор =
, який дає логічне значення A = (0 and B)
. І, нарешті, помилка "невідповідність типу" виникає після спроби порівняти логічне значення A = (0 and B)
і ціле значення 0
.
Ось це правильно:
var
A, B: Integer;
begin
if (A = 0) and (B <> 0) then ...
Використовується оцінка короткого замикання. Розглянемо такий вираз:
if MyFunction(X) and MyOtherFunction(Y) then...
-
Гарантується, що спершу буде обчислено функцію
MyFunction(X)
. -
І якщо
MyFunction(X)
повернеfalse
, тоді значення виразу відоме (значенняfalse and що-завгодно
завжди єfalse
), іMyOtherFunction(Y)
не буде виконано взагалі. -
Аналогічне правило для виразу
або
. Там, якщо відомо, що вираз має значенняtrue
(оскільки 1-й операнд має значенняtrue
), другий операнд не обчислюється. -
Це особливо корисно під час написання таких виразів, як
if (A <> nil) and A.IsValid then...
Це буде нормально, навіть якщо
A
дорівнюєnil
. Ключове словоnil
— це вказівник, що дорівнює нулю (коли представлено у вигляді числа). Це називається нульовим покажчиком у багатьох інших мовах програмування.
2.5. Перевірка одного виразу для кількох значень (case)
Якщо необхідно виконати іншу дію залежно від значення якогось виразу, тоді корисним буде оператор case .. of .. end
.
case SomeValue of
0: DoSomething;
1: DoSomethingElse;
2: begin
IfItsTwoThenDoThis;
AndAlsoDoThis;
end;
3..10: DoSomethingInCaseItsInThisRange;
11, 21, 31: AndDoSomethingForTheseSpecialValues;
else DoSomethingInCaseOfUnexpectedValue;
end;
Речення else
є необов’язковим (і відповідає default
у C-подібних мовах). Якщо жодна умова не відповідає, і немає else
, нічого не відбувається.
Якщо ви походите з C-подібних мов, і порівняйте це з інструкцією switch
у цих мовах, ви помітите, що немає автоматичного провалу. Це свідоме благословення в Паскалі. Вам не потрібно пам’ятати про розміщення інструкцій break
. Під час кожного виконання виконується щонайбільше одна гілка case
, і все.
2.6. Перелічувані та порядкові типи, множини та масиви постійної довжини
Перелічуваний тип у Паскалі — дуже гарний, непрозорий тип. Ймовірно, ви використовуватимете його набагато частіше, ніж переліки в інших мовах:)
type
TAnimalKind = (akDuck, akCat, akDog);
Згідно з домовленістю до імен enum ставиться дволітерне скорочення імені типу, отже ak
= скорочення для "Animal Kind". Це корисна домовленість, оскільки імена enum знаходяться в просторі імен модуля (глобальному). Таким чином, додавши до них префікс ak
, ви мінімізуєте ймовірність конфліктів з іншими ідентифікаторами.
Зауваження
|
Зіткнення в іменах не перешкода. Це нормально, якщо різні модулі визначають той самий ідентифікатор. Але було б гарною ідеєю все одно намагатися уникнути колізій, щоб код був простим для розуміння та пошуку. |
Зауваження
|
Ви можете уникнути розміщення імен enum у глобальному просторі імен за допомогою директиви компілятора {$scopedenums on} . Це означає, що вам доведеться отримати до них доступ, визначивши назву типу, як-от TAnimalKind.akDuck . У цій ситуації потреба в префіксі ak зникає, і ви, ймовірно, просто називатимете переліки Duck, Cat, Dog . Це схоже на перерахування C#.
|
Той факт, що перерахований тип є непрозорим, означає, що його не можна просто призначити цілому числу. Однак для спеціального використання ви можете використати Ord(MyAnimalKind)
для примусового перетворення enum на int або приведення типу TAnimalKind(MyInteger)
для примусового перетворення int на enum. В останньому випадку переконайтеся, що спершу перевірте, чи MyInteger
знаходиться в належному діапазоні (від 0 до Ord(High(TAnimalKind))
).
Перелічувані та порядкові типи можуть використовуватися як індекси масиву:
type
TArrayOfTenStrings = array [0..9] of string;
TArrayOfTenStrings1Based = array [1..10] of string;
TMyNumber = 0..9;
TAlsoArrayOfTenStrings = array [TMyNumber] of string;
TAnimalKind = (akDuck, akCat, akDog);
TAnimalNames = array [TAnimalKind] of string;
Вони також можуть бути використані для створення наборів (внутрішніх бітових полів):
type
TAnimalKind = (akDuck, akCat, akDog);
TAnimals = set of TAnimalKind;
var
A: TAnimals;
begin
A := [];
A := [akDuck, akCat];
A := A + [akDog];
A := A * [akCat, akDog];
Include(A, akDuck);
Exclude(A, akDuck);
end;
2.7. Цикли (for, while, repeat, for .. in)
{$mode objfpc}{$H+}{$J-}
{$R+} // перевірку діапазону ввімкнено – добре для налагодження
var
MyArray: array [0..9] of Integer;
I: Integer;
begin
// ініціалізувати
for I := 0 to 9 do
MyArray[I] := I * I;
// показати
for I := 0 to 9 do
WriteLn('Квадрати: ', MyArray[I]);
// виконує те саме, що й вище
for I := Low(MyArray) to High(MyArray) do
WriteLn('Квадрати: ', MyArray[I]);
// виконує те саме, що й вище
I := 0;
while I < 10 do
begin
WriteLn('Квадрати: ', MyArray[I]);
I := I + 1; // or "I += 1", or "Inc(I)"
end;
// виконує те саме, що й вище
I := 0;
repeat
WriteLn('Квадоати: ', MyArray[I]);
Inc(I);
until I = 10;
// виконує те саме, що й вище
// зауваження: тут я перераховую значення MyArray, а не індекси
for I in MyArray do
WriteLn('Квадрати: ', I);
end.
Про цикли repeat
і while
:
There are two differences between these loop types:
-
The loop condition has an opposite meaning. In
while .. do
you tell it when to continue, but inrepeat .. until
you tell it when to stop. -
In case of
repeat
, the condition is not checked at the beginning. So therepeat
loop always runs at least once.
About the for I := …
loops`:
The for I := .. to .. do …
construction it similar to the C-like for
loop. However, it’s more constrained, as you cannot specify arbitrary actions/tests to control the loop iteration. This is strictly for iterating over a consecutive numbers (or other ordinal types). The only flexibility you have is that you can use downto
instead of to
, to make numbers go downward.
In exchange, it looks clean, and is very optimized in execution. In particular, the expressions for the lower and higher bound are only calculated once, before the loop starts.
Note that the value of the loop counter variable (I
in this example) should be considered undefined after the loop has finished, due to possible optimizations. Accessing the value of I
after the loop may cause a compiler warning. Unless you exit the loop prematurely by Break
or Exit
: in such case, the counter variable is guaranteed to retain the last value.
About the for I in …
loops:
The for I in .. do ..
is similar to foreach
construct in many modern languages. It works intelligently on many built-in types:
-
It can iterate over all values in the array (example above).
-
It can iterate over all possible values of an enumerated type:
var AK: TAnimalKind; begin for AK in TAnimalKind do...
-
It can iterate over all items included in the set:
var Animals: TAnimals; AK: TAnimalKind; begin Animals := [akDog, akCat]; for AK in Animals do ...
-
And it works on custom list types, generic or not, like
TObjectList
orTFPGObjectList
.{$mode objfpc}{$H+}{$J-} uses SysUtils, FGL; type TMyClass = class I, Square: Integer; end; TMyClassList = specialize TFPGObjectList<TMyClass>; var List: TMyClassList; C: TMyClass; I: Integer; begin List := TMyClassList.Create(true); // true = володіє дітьми try for I := 0 to 9 do begin C := TMyClass.Create; C.I := I; C.Square := I * I; List.Add(C); end; for C in List do WriteLn('Квадрат ', C.I, ' є ', C.Square); finally FreeAndNil(List); end; end.
We didn’t yet explain the concept of classes, so the last example may not be obvious to you yet — just carry on, it will make sense later:)
2.8. Output, logging
To simply output strings in Pascal, use the Write
or WriteLn
routine. The latter automatically adds a newline at the end.
This is a "magic" routine in Pascal. It takes a variable number of arguments and they can have any type. They are all converted to strings when displaying, with a special syntax to specify padding and number precision.
WriteLn('Hello world!');
WriteLn('You can output an integer: ', 3 * 4);
WriteLn('You can pad an integer: ', 666:10);
WriteLn('You can output a float: ', Pi:1:4);
To explicitly use newline in the string, use the LineEnding
constant (from FPC RTL). (The Castle Game Engine defines also a shorter NL
constant.) Pascal strings do not interpret any special backslash sequences, so writing
WriteLn('One line.\nSecond line.'); // INCORRECT example
doesn’t work like some of you would think. This will work:
WriteLn('One line.' + LineEnding + 'Second line.');
or just this:
WriteLn('One line.');
WriteLn('Second line.');
Note that this will only work in console applications. Make sure you have {$apptype CONSOLE}
(and not {$apptype GUI}
) defined in your main program file. On some operating systems it actually doesn’t matter and will work always (Unix), but on some operating systems trying to write something from a GUI application is an error (Windows).
In the Castle Game Engine: use WriteLnLog
or WriteLnWarning
, never WriteLn
, to print debug information. They will be always directed to some useful output. On Unix, standard output. On Windows GUI application, log file. On Android, the Android logging facility (visible when you use adb logcat
). The use of WriteLn
should be limited to the cases when you write a command-line application (like a 3D model converter / generator) and you know that the standard output is available.
2.9. Converting to a string
To convert an arbitrary number of arguments to a string (instead of just directly outputting them), you have a couple of options.
-
You can convert particular types to strings using specialized functions like
IntToStr
andFloatToStr
. Furthermore, you can concatenate strings in Pascal simply by adding them. So you can create a string like this:'My int number is ' + IntToStr(MyInt) + ', and the value of Pi is ' + FloatToStr(Pi)
.-
Advantage: Absolutely flexible. There are many
XxxToStr
overloaded versions and friends (likeFormatFloat
), covering many types. Most of them are in theSysUtils
unit. -
Another advantage: Consistent with the reverse functions. To convert a string (for example, user input) back to an integer or float, you use
StrToInt
,StrToFloat
and friends (likeStrToIntDef
). -
Disadvantage: A long concatenation of many
XxxToStr
calls and strings doesn’t look nice.
-
-
The
Format
function, used likeFormat('%d %f %s', [MyInt, MyFloat, MyString])
. This is likesprintf
function in the C-like languages. It inserts the arguments into the placeholders in the pattern. The placeholders may use special syntax to influence formatting, e.g.%.4f
results in a floating-point format with 4 digits after the decimal point.-
Advantage: The separation of pattern string from arguments looks clean. If you need to change the pattern string without touching the arguments (e.g. when translating), you can do it easily.
-
Another advantage: No compiler magic. You can use the same syntax to pass any number of arguments of an arbitrary type in your own routines (declare parameter as an
array of const
). You can then pass these arguments downward toFormat
, or deconstruct the list of parameters and do anything you like with them. -
Disadvantage: Compiler does not check whether the pattern matches the arguments. Using a wrong placeholder type will result in an exception at runtime (
EConvertError
exception, not anything nasty like a segmentation fault).
-
-
WriteStr(TargetString, …)
routine behaves much likeWrite(…)
, except that the result is saved to theTargetString
.-
Advantage: It supports all the features of
Write
, including the special syntax for formatting likePi:1:4
. -
Disadvantage: The special syntax for formatting is a "compiler magic", implemented specifically for routines like this. This is sometimes troublesome, e.g. you cannot create your own routine
MyStringFormatter(…)
that would also allow the special syntax likePi:1:4
. For this reason (and also because it wasn’t implemented for a long time in major Pascal compilers), this construction is not very popular.
-
3. Units
Units allow you to group common stuff (anything that can be declared), for usage by other units and programs. They are equivalent to modules and packages in other languages. They have an interface section, where you declare what is available for other units and programs, and then the implementation. Save unit MyUnit
as myunit.pas
(lowercase with .pas
extension).
{$mode objfpc}{$H+}{$J-}
unit MyUnit;
interface
procedure MyProcedure(const A: Integer);
function MyFunction(const S: string): string;
implementation
procedure MyProcedure(const A: Integer);
begin
WriteLn('A + 10 is: ', A + 10);
end;
function MyFunction(const S: string): string;
begin
Result := S + 'strings are automatically managed';
end;
end.
Final programs are saved as myprogram.lpr
files (lpr
= Lazarus program file; in Delphi you would use .dpr
). Note that other conventions are possible here, e.g. some projects just use .pas
for main program file, some use .pp
for units or programs. I advise using .pas
for units and .lpr
for FPC/Lazarus programs.
A program can use a unit by a uses
keyword:
{$mode objfpc}{$H+}{$J-}
program MyProgram;
uses
MyUnit;
begin
WriteLn(MyFunction('Note: '));
MyProcedure(5);
end.
A unit may also contain initialization
and finalization
sections. This is the code executed when the program starts and ends.
{$mode objfpc}{$H+}{$J-}
unit initialization_finalization;
interface
implementation
initialization
WriteLn('Привіт, світ!');
finalization
WriteLn('Бувай, світ!');
end.
3.1. Units using each other
One unit can also use another unit. Another unit can be used in the interface section, or only in the implementation section. The former allows to define new public stuff (procedures, types…) on top of another unit’s stuff. The latter is more limited (if you use a unit only in the implementation section, you can use its identifiers only in your implementation).
{$mode objfpc}{$H+}{$J-}
unit AnotherUnit;
interface
uses
Classes;
{ Тип (клас) "TComponent" визначається в модулі Classes.
Ось чому нам довелося використати модуль Classes вище. }
procedure DoSomethingWithComponent(var C: TComponent);
implementation
uses SysUtils;
procedure DoSomethingWithComponent(var C: TComponent);
begin
{ Процедура FreeAndNil визначена в модулі SysUtils.
Оскільки ми посилаємося лише на його назву в реалізації,
модуль SysUtils можна використовувати у розділі «implementation». }
FreeAndNil(C);
end;
end.
It is not allowed to have circular unit dependencies in the interface. That is, two units cannot use each other in the interface section.
The reason is that in order to "understand"
the interface section of a unit, the compiler must first "understand" all the units it uses in the interface section. Pascal language follows this rule strictly, and it allows a fast compilation and fully automatic detection on the compiler side what units need to be recompiled. There is no need to use complicated Makefile
files for a simple task of compilation in Pascal, and there is no need to recompile everything just to make sure that all dependencies are updated correctly.
It is OK to make a circular dependency between units when at least one "usage" is only in the implementation. So it’s OK for unit A
to use unit B
in the interface, and then unit B
to use unit A
in the implementation.
3.2. Qualifying identifiers with unit name
Different units may define the same identifier. To keep the code simple to read and search, you should usually avoid it, but it’s not always possible.
In such cases, the last unit on the uses
clause "wins", which means that the identifiers it introduces hide the same identifiers introduced by earlier units.
You can always explicitly define a unit of a given identifier, by using it like MyUnit.MyIdentifier
. This is the usual solution when the identifier you want to use from MyUnit
is hidden by another unit. Of course you can also rearrange the order of units on your uses clause, although this can affect other declarations than the one you’re trying to fix.
{$mode objfpc}{$H+}{$J-}
program showcolor;
// Both Graphics and GoogleMapsEngine units define TColor type.
uses Graphics, GoogleMapsEngine;
var
{ This doesn't work like we want, as TColor ends up
being defined by GoogleMapsEngine. }
// Color: TColor;
{ This works Ok. }
Color: Graphics.TColor;
begin
Color := clYellow;
WriteLn(Red(Color), ' ', Green(Color), ' ', Blue(Color));
end.
In case of units, remember that they have two uses
clauses: one in the interface, and another one in the implementation. The rule later units hide the stuff from earlier units is applied here consistently, which means that units used in the implementation section can hide identifiers from units used in the interface section. However, remember that when reading the interface
section, only the units used in the interface matter. This may create a confusing situation, where two seemingly-equal declarations are considered different by the compiler:
{$mode objfpc}{$H+}{$J-}
unit UnitUsingColors;
// INCORRECT example
interface
uses Graphics;
procedure ShowColor(const Color: TColor);
implementation
uses GoogleMapsEngine;
procedure ShowColor(const Color: TColor);
begin
// WriteLn(ColorToString(Color));
end;
end.
The unit Graphics
(from Lazarus LCL) defines the TColor
type. But the compiler will fail to compile the above unit, claiming that you don’t implement a procedure ShowColor
that matches the interface declaration. The problem is that unit GoogleMapsEngine
also defines a TColor
type. And it is used only in the implementation
section, therefore it shadows the TColor
definition only in the implementation. The equivalent version of the above unit, where the error is obvious, looks like this:
{$mode objfpc}{$H+}{$J-}
unit UnitUsingColors;
// INCORRECT example.
// This is what the compiler "sees" when trying to compile previous example
interface
uses Graphics;
procedure ShowColor(const Color: Graphics.TColor);
implementation
uses GoogleMapsEngine;
procedure ShowColor(const Color: GoogleMapsEngine.TColor);
begin
// WriteLn(ColorToString(Color));
end;
end.
The solution is trivial in this case, just change the implementation to explicitly use TColor
from Graphics
unit. You could fix it also by moving the GoogleMapsEngine
usage, to the interface section and earlier than Graphics
, although this could result in other consequences in real-world cases, when UnitUsingColors
would define more things.
{$mode objfpc}{$H+}{$J-}
unit UnitUsingColors;
interface
uses Graphics;
procedure ShowColor(const Color: TColor);
implementation
uses GoogleMapsEngine;
procedure ShowColor(const Color: Graphics.TColor);
begin
// WriteLn(ColorToString(Color));
end;
end.
3.3. Exposing one unit identifiers from another
Sometimes you want to take an identifier from one unit, and expose it in a new unit. The end result should be that using the new unit will make the identifier available in the namespace.
Sometimes this is necessary to preserve backward compatibility with previous unit versions. Sometimes it’s nice to "hide" an internal unit this way.
This can be done by redefining the identifier in your new unit.
{$mode objfpc}{$H+}{$J-}
unit MyUnit;
interface
uses Graphics;
type
{ Expose TColor from Graphics unit as TMyColor. }
TMyColor = TColor;
{ Alternatively, expose it under the same name.
Qualify with unit name in this case, otherwise
we would refer to ourselves with "TColor = TColor" definition. }
TColor = Graphics.TColor;
const
{ This works with constants too. }
clYellow = Graphics.clYellow;
clBlue = Graphics.clBlue;
implementation
end.
Note that this trick cannot be done as easily with global procedures, functions and variables. With procedures and functions, you could expose a constant pointer to a procedure in another unit (see Callbacks (aka events, aka pointers to functions, aka procedural variables)), but that looks quite dirty.
The usual solution is then to create a trivial "wrapper" functions that underneath simply call the functions from the internal unit, passing the parameters and return values around.
To make this work with global variables, one can use global (unit-level) properties, see Properties.
4. Classes
4.1. Basics
We have classes. At the basic level, a class is just a container for
-
fields (which is fancy name for "a variable inside a class"),
-
methods (which is fancy name for "a procedure or function inside a class"),
-
and properties (which is a fancy syntax for something that looks like a field, but is in fact a pair of methods to get and set something; more in Properties).
-
Actually, there are more possibilities, described in More stuff inside classes and nested classes.
type
TMyClass = class
MyInt: Integer; // this is a field
property MyIntProperty: Integer read MyInt write MyInt; // this is a property
procedure MyMethod; // this is a method
end;
procedure TMyClass.MyMethod;
begin
WriteLn(MyInt + 10);
end;
4.2. Inheritance, is, as
We have inheritance and virtual methods.
{$mode objfpc}{$H+}{$J-}
program MyProgram;
uses
SysUtils;
type
TMyClass = class
MyInt: Integer;
procedure MyVirtualMethod; virtual;
end;
TMyClassDescendant = class(TMyClass)
procedure MyVirtualMethod; override;
end;
procedure TMyClass.MyVirtualMethod;
begin
WriteLn('TMyClass покаже MyInt + 10: ', MyInt + 10);
end;
procedure TMyClassDescendant.MyVirtualMethod;
begin
WriteLn('TMyClassDescendant покаже MyInt + 20: ', MyInt + 20);
end;
var
C: TMyClass;
begin
C := TMyClass.Create;
try
C.MyVirtualMethod;
finally
FreeAndNil(C);
end;
C := TMyClassDescendant.Create;
try
C.MyVirtualMethod;
finally
FreeAndNil(C);
end;
end.
By default methods are not virtual, declare them with virtual
to make them. Overrides must be marked with override
, otherwise you will get a warning. To hide a method without overriding (usually you don’t want to do this, unless you now what you’re doing) use reintroduce
.
To test the class of an instance at runtime, use the is
operator. To typecast the instance to a specific class, use the as
operator.
{$mode objfpc}{$H+}{$J-}
program is_as;
uses
SysUtils;
type
TMyClass = class
procedure MyMethod;
end;
TMyClassDescendant = class(TMyClass)
procedure MyMethodInDescendant;
end;
procedure TMyClass.MyMethod;
begin
WriteLn('MyMethod');
end;
procedure TMyClassDescendant.MyMethodInDescendant;
begin
WriteLn('MyMethodInDescendant');
end;
var
Descendant: TMyClassDescendant;
C: TMyClass;
begin
Descendant := TMyClassDescendant.Create;
try
Descendant.MyMethod;
Descendant.MyMethodInDescendant;
{ Descendant has all functionality expected of
the TMyClass, so this assignment is OK }
C := Descendant;
C.MyMethod;
{ this cannot work, since TMyClass doesn't define this method }
//C.MyMethodInDescendant;
if C is TMyClassDescendant then
(C as TMyClassDescendant).MyMethodInDescendant;
finally
FreeAndNil(Descendant);
end;
end.
Instead of casting using X as TMyClass
, you can also use the unchecked typecast TMyClass(X)
. This is faster, but results in an undefined behavior if the X
is not, in fact, a TMyClass
descendant. So don’t use the TMyClass(X)
typecast, or use it only in a code where it’s blindingly obvious that it’s correct, for example right after testing with is
:
if A is TMyClass then
(A as TMyClass).CallSomeMethodOfMyClass;
// below is marginally faster
if A is TMyClass then
TMyClass(A).CallSomeMethodOfMyClass;
4.3. Properties
Properties are a very nice "syntax sugar" to
-
Make something that looks like a field (can be read and set) but underneath is realized by calling a getter and setter methods. The typical usage is to perform some side-effect (e.g. redraw the screen) each time some value changes.
-
Make something that looks like a field, but is read-only. In effect, it’s like a constant or a parameter-less function.
type
TWebPage = class
private
FURL: string;
FColor: TColor;
function SetColor(const Value: TColor);
public
{ No way to set it directly.
Call the Load method, like Load('http://www.freepascal.org/'),
to load a page and set this property. }
property URL: string read FURL;
procedure Load(const AnURL: string);
property Color: TColor read FColor write SetColor;
end;
procedure TWebPage.Load(const AnURL: string);
begin
FURL := AnURL;
NetworkingComponent.LoadWebPage(AnURL);
end;
function TWebPage.SetColor(const Value: TColor);
begin
if FColor <> Value then
begin
FColor := Value;
// for example, cause some update each time value changes
Repaint;
// as another example, make sure that some underlying instance,
// like a "RenderingComponent" (whatever that is),
// has a synchronized value of Color.
RenderingComponent.Color := Value;
end;
end;
Note that instead of specifying a method, you can also specify a field (typically a private field) to directly get or set. In the example above, the Color
property uses a setter method SetColor
. But for getting the value, the Color
property refers directly to the private field FColor
. Directly referring to a field is faster than implementing trivial getter or setter methods (faster for you, and faster at execution).
When declaring a property you specify:
-
Whether it can be read, and how (by directly reading a field, or by using a "getter" method).
-
And, in a similar manner, whether it can be set, and how (by directly writing to a designated field, or by calling a "setter" method).
The compiler checks that the types and parameters of indicated fields and methods match with the property type. For example, to read an Integer
property you have to either provide an Integer
field, or a parameter-less method that returns an Integer
.
Technically, for the compiler, the "getter" and "setter" methods are just normal methods and they can do absolutely anything (including side-effects or randomization). But it’s a good convention to design properties to behave more-or-less like fields:
-
The getter function should have no visible side-effects (e.g. it should not read some input from file / keyboard). It should be deterministic (no randomization, not even pseudo-randomization :). Reading a property many times should be valid, and return the same value, if nothing changed in-between.
Note that it’s OK for getter to have some invisible side-effect, for example to cache a value of some calculation (known to produce the same results for given instance), to return it faster next time. This is in fact one of the cool possibilities of a "getter" function.
-
The setter function should always set the requested value, such that calling the getter yields it back. Do not reject invalid values silently in the "setter" (raise an exception if you must). Do not convert or scale the requested value. The idea is that after
MyClass.MyProperty := 123;
the programmer can expect thatMyClass.MyProperty = 123
. -
The read-only properties are often used to make some field read-only from the outside. Again, the good convention is to make it behave like a constant, at least constant for this object instance with this state. The value of the property should not change unexpectedly. Make it a function, not a property, if using it has a side effect or returns something random.
-
The "backing" field of a property is almost always private, since the idea of a property is to encapsulate all outside access to it.
-
It’s technically possible to make set-only properties, but I have not yet seen a good example of such thing:)
Зауваження
|
Properties can also be defined outside of class, at a unit level. They serve an analogous purpose then: look like a global variable, but are backed by a getter and setter routines. |
4.3.1. Serialization of properties
Published properties are the basis of a serialization (also known as streaming components) in Pascal. Serialization means that the instance data is recorded into a stream (like a file), from which it can be later restored.
Serialization is what happens when Lazarus reads (or writes) the component state from an xxx.lfm
file. (In Delphi, the equivalent file has .dfm
extension.) You can also use this mechanism explicitly, using routines like ReadComponentFromTextStream
from the LResources
unit. You can also use other serialization algorithms, e.g. FpJsonRtti
unit (serializing to JSON).
In the Castle Game Engine: Use the CastleComponentSerialize
unit (based on FpJsonRtti
) to serialize our user-interface and transformation component hierarchies.
At each property, you can declare some additional things that will be helpful for any serialization algorithm:
-
You can specify the property default value (using the
default
keyword). Note that you are still required to initialize the property in the constructor to this exact default value (it is not done automatically). Thedefault
declaration is merely an information to the serialization algorithm: "when the constructor finishes, the given property has the given value". -
Whether the property should be stored at all (using the
stored
keyword).
4.4. Exceptions - Quick Example
We have exceptions. They can be caught with try … except … end
clauses, and we have finally sections like try … finally … end
.
{$mode objfpc}{$H+}{$J-}
program MyProgram;
uses
SysUtils;
type
TMyClass = class
procedure MyMethod;
end;
procedure TMyClass.MyMethod;
begin
if Random > 0.5 then
raise Exception.Create('Підняття винятку!');
end;
var
C: TMyClass;
begin
Randomize;
C := TMyClass.Create;
try
C.MyMethod;
finally
FreeAndNil(C);
end;
end.
Note that the finally
clause is executed even if you exit the block using the Exit
(from function / procedure / method) or Break
or Continue
(from loop body).
See the Exceptions chapter for more in-depth description of exceptions.
4.5. Visibility specifiers
As in most object-oriented languages, we have visibility specifiers to hide fields / methods / properties.
The basic visibility levels are:
public
-
everyone can access it, including the code in other units.
private
-
only accessible in this class.
protected
-
only accessible in this class and descendants.
The explanation of private
and protected
visibility above is not precisely true. The code in the same unit can overcome their limits, and access the private
and protected
stuff freely. Sometimes this is a nice feature, allows you to implement tightly-connected classes. Use strict private
or strict protected
to secure your classes more tightly. See the Private and strict private.
By default, if you don’t specify the visibility, then the visibility of declared stuff is public
. The exception is for classes compiled with {$M+}
, or descendants of classes compiled with {$M+}
, which includes all descendants of TPersistent
, which also includes all descendants of TComponent
(since TComponent
descends from TPersistent
). For them, the default visibility specifier is published
, which is like public
, but in addition the streaming system knows to handle this.
Not every field and property type is allowed in the published
section (not every type can be streamed, and only classes can be streamed from simple fields). Just use public
if you don’t care about streaming but want something available to all users.
4.6. Default ancestor
If you don’t declare the ancestor type, every class
inherits from TObject
.
4.7. Self
The special keyword Self
can be used within the class implementation to explicitly refer to your own instance. It is equivalent to this
from C++, Java and similar languages.
4.8. Calling inherited method
Within a method implementation, if you call another method, then by default you call the method of your own class. In the example code below, TMyClass2.MyOtherMethod
calls MyMethod
, which ends up calling TMyClass2.MyMethod
.
{$mode objfpc}{$H+}{$J-}
uses SysUtils;
type
TMyClass1 = class
procedure MyMethod;
end;
TMyClass2 = class(TMyClass1)
procedure MyMethod;
procedure MyOtherMethod;
end;
procedure TMyClass1.MyMethod;
begin
Writeln('TMyClass1.MyMethod');
end;
procedure TMyClass2.MyMethod;
begin
Writeln('TMyClass2.MyMethod');
end;
procedure TMyClass2.MyOtherMethod;
begin
MyMethod; // this calls TMyClass2.MyMethod
end;
var
C: TMyClass2;
begin
C := TMyClass2.Create;
try
C.MyOtherMethod;
finally FreeAndNil(C) end;
end.
If the method is not defined in a given class, then it calls the method of an ancestor class. In effect, when you call MyMethod
on an instance of TMyClass2
, then
-
The compiler looks for
TMyClass2.MyMethod
. -
If not found, it looks for
TMyClass1.MyMethod
. -
If not found, it looks for
TObject.MyMethod
. -
if not found, then the compilation fails.
You can test it by commenting out the TMyClass2.MyMethod
definition in the example above. In effect, TMyClass1.MyMethod
will be called by TMyClass2.MyOtherMethod
.
Sometimes, you don’t want to call the method of your own class. You want to call the method of an ancestor (or ancestor’s ancestor, and so on). To do this, add the keyword inherited
before the call to MyMethod
, like this:
inherited MyMethod;
This way you force the compiler to start searching from an ancestor class. In our example, it means that compiler is searching for MyMethod
inside TMyClass1.MyMethod
, then TObject.MyMethod
, and then gives up. It does not even consider using the implementation of TMyClass2.MyMethod
.
Підказка
|
Go ahead, change the implementation of TMyClass2.MyOtherMethod above to use inherited MyMethod , and see the difference in the output.
|
The inherited
call is often used to call the ancestor method of the same name. This way the descendants can enhance the ancestors (keeping the ancestor functionality, instead of replacing the ancestor functionality). Like in the example below.
{$mode objfpc}{$H+}{$J-}
uses SysUtils;
type
TMyClass1 = class
constructor Create;
procedure MyMethod(const A: Integer);
end;
TMyClass2 = class(TMyClass1)
constructor Create;
procedure MyMethod(const A: Integer);
end;
constructor TMyClass1.Create;
begin
inherited Create; // this calls TObject.Create
Writeln('TMyClass1.Create');
end;
procedure TMyClass1.MyMethod(const A: Integer);
begin
Writeln('TMyClass1.MyMethod ', A);
end;
constructor TMyClass2.Create;
begin
inherited Create; // this calls TMyClass1.Create
Writeln('TMyClass2.Create');
end;
procedure TMyClass2.MyMethod(const A: Integer);
begin
inherited MyMethod(A); // this calls TMyClass1.MyMethod
Writeln('TMyClass2.MyMethod ', A);
end;
var
C: TMyClass2;
begin
C := TMyClass2.Create;
try
C.MyMethod(123);
finally FreeAndNil(C) end;
end.
Since using inherited
to call a method with the same name, with the same arguments, is a very often case, there is a special shortcut for it: you can just write inherited;
(inherited
keyword followed immediately by a semicolon, instead of a method name). This means "call an inherited method with the same name, passing it the same arguments as the current method".
Підказка
|
In the above example, all the inherited …; calls could be replaced by a simple inherited; .
|
Note 1: The inherited;
is really just a shortcut for calling the ancestor’s method with the same variables passed in. If you have modified your own parameter (which is possible, if the parameter is not const
), then the ancestor’s method can receive different input values from your descendant. Consider this:
procedure TMyClass2.MyMethod(A: Integer);
begin
Writeln('TMyClass2.MyMethod beginning ', A);
A := 456;
{ This calls TMyClass1.MyMethod with A = 456,
regardless of the A value passed to this method (TMyClass2.MyMethod). }
inherited;
Writeln('TMyClass2.MyMethod ending ', A);
end;
Note 2: You usually want to make the MyMethod
virtual when many classes (along the "inheritance chain") define it. More about the virtual methods in the section below. But the inherited
keyword works regardless if the method is virtual or not. The inherited
always means that the compiler starts searching for the method in an ancestor, and it makes sense for both virtual and not virtual methods.
4.9. Virtual methods, override and reintroduce
By default, the methods are not virtual. This is similar to C++, and unlike Java.
When a method is not virtual, the compiler determines which method to call based on the currently declared class type, not based on the actually created class type. The difference seems subtle, but it’s important when your variable is declared to have a class like TFruit
, but it may be in fact a descendant class like TApple
.
The idea of the object-oriented programming is that the descendant class is always as good as the ancestor, so the compiler allows to use a descendant class always when the ancestor is expected. When your method is not virtual, this can have undesired consequences. Consider the example below:
{$mode objfpc}{$H+}{$J-}
uses SysUtils;
type
TFruit = class
procedure Eat;
end;
TApple = class(TFruit)
procedure Eat;
end;
procedure TFruit.Eat;
begin
Writeln('Eating a fruit');
end;
procedure TApple.Eat;
begin
Writeln('Eating an apple');
end;
procedure DoSomethingWithAFruit(const Fruit: TFruit);
begin
Writeln('We have a fruit with class ', Fruit.ClassName);
Writeln('We eat it:');
Fruit.Eat;
end;
var
Apple: TApple; // Note: you could as well declare "Apple: TFruit" here
begin
Apple := TApple.Create;
try
DoSomethingWithAFruit(Apple);
finally FreeAndNil(Apple) end;
end.
This example will print
We have a fruit with class TApple We eat it: Eating a fruit
In effect, the call Fruit.Eat
called the TFruit.Eat
implementation, and nothing calls the TApple.Eat
implementation.
If you think about how the compiler works, this is natural: when you wrote the Fruit.Eat
, the Fruit
variable was declared to hold a class TFruit
. So the compiler was searching for the method called Eat
within the TFruit
class. If the TFruit
class would not contain such method, the compiler would search within an ancestor (TObject
in this case). But the compiler cannot search within descendants (like TApple
), as it doesn’t know whether the actual class of Fruit
is TApple
, TFruit
, or some other TFruit
descendant (like a TOrange
, not shown in the example above).
In other words, the method to be called is determined at compile-time.
Using the virtual methods changes this behavior. If the Eat
method would be virtual (an example of it is shown below), then the actual implementation to be called is determined at runtime. If the Fruit
variable will hold an instance of the class TApple
(even if it’s declared as TFruit
), then the Eat
method will be searched within the TApple
class first.
In Object Pascal, to define a method as virtual, you need to
-
Mark its first definition (in the top-most ancestor) with the
virtual
keyword. -
Mark all the other definitions (in the descendants) with the
override
keyword. All the overridden versions must have exactly the same parameters (and return the same types, in case of functions).
{$mode objfpc}{$H+}{$J-}
uses SysUtils;
type
TFruit = class
procedure Eat; virtual;
end;
TApple = class(TFruit)
procedure Eat; override;
end;
procedure TFruit.Eat;
begin
Writeln('Eating a fruit');
end;
procedure TApple.Eat;
begin
Writeln('Eating an apple');
end;
procedure DoSomethingWithAFruit(const Fruit: TFruit);
begin
Writeln('We have a fruit with class ', Fruit.ClassName);
Writeln('We eat it:');
Fruit.Eat;
end;
var
Apple: TApple; // Note: you could as well declare "Apple: TFruit" here
begin
Apple := TApple.Create;
try
DoSomethingWithAFruit(Apple);
finally FreeAndNil(Apple) end;
end.
This example will print
We have a fruit with class TApple We eat it: Eating an apple
Internally, virtual methods work by having so-called virtual method table associated with each class. This table is a list of pointers to the implementations of virtual methods for this class. When calling the Eat
method, the compiler looks into a virtual method table associated with the actual class of Fruit
, and uses a pointer to the Eat
implementation stored there.
If you don’t use the override
keyword, the compiler will warn you that you’re hiding (obscuring) the virtual method of an ancestor with a non-virtual definition. If you’re sure that this is what you want, you can add a reintroduce
keyword. But in most cases, you will rather want to keep the method virtual, and add the override
keyword, thus making sure that it’s always invoked correctly.
5. Freeing classes
5.1. Remember to free the class instances
The class instances have to be manually freed, otherwise you get memory leaks. I advise using FPC -gl -gh
options to detect memory leaks (see https://castle-engine.io/manual_optimization.php#section_memory ).
Note that this doesn’t concern raised exceptions. Although you do create a class when raising an exception (and it’s a perfectly normal class, and you can create your own classes for this purpose too). But this class instance is freed automatically.
5.2. How to free
To free the class instance, it’s best to call FreeAndNil(A)
from SysUtils
unit on your class instance. It checks whether A
is nil
, if not — calls its destructor, and sets A
to nil
. So calling it many times in a row is not an error.
It is more-or-less a shortcut for
if A <> nil then
begin
A.Destroy;
A := nil;
end;
Actually, that’s an oversimplification, as FreeAndNil
does a useful trick and sets the variable A
to nil
before calling the destructor on a suitable reference. This helps to prevent a certain class of bugs — the idea is that the "outside" code should never access a half-destructed instance of the class.
Often you will also see people using the A.Free
method, which is like doing
if A <> nil then
A.Destroy;
This frees the A
, unless it’s nil
.
Note that in normal circumstances, you should never call a method on an instance which may be nil
. So the call A.Free
may look suspicious at the first sight, if A
can be nil
. However, the Free
method is an exception to this rule. It does something dirty in the implementation — namely, checks whether Self <> nil
. This dirty trick works only in non-virtual methods (that don’t call any virtual methods and don’t access any fields).
I advise using FreeAndNil(A)
always, without exceptions, and never to call directly the Free
method or Destroy
destructor. The Castle Game Engine does it like that. It helps to keep a nice assertion that all references are either nil, or point to valid instances.
5.3. Manual and automatic freeing
In many situations, the need to free the instance is not much problem. You just write a destructor, that matches a constructor, and deallocates everything that was allocated in the constructor (or, more completely, in the whole lifetime of the class). Be careful to only free each thing once. Usually it’s a good idea to set the freed reference to nil
, usually it’s most comfortable to do it by calling the FreeAndNil(A)
.
So, like this:
uses SysUtils;
type
TGun = class
end;
TPlayer = class
Gun1, Gun2: TGun;
constructor Create;
destructor Destroy; override;
end;
constructor TPlayer.Create;
begin
inherited;
Gun1 := TGun.Create;
Gun2 := TGun.Create;
end;
destructor TPlayer.Destroy;
begin
FreeAndNil(Gun1);
FreeAndNil(Gun2);
inherited;
end;
To avoid the need to explicitly free the instance, one can also use the TComponent
feature of "ownership". An object that is owned will be automatically freed by the owner. The mechanism is smart and it will never free an already freed instance (so things will also work correctly if you manually free the owned object earlier). We can change the previous example to this:
uses SysUtils, Classes;
type
TGun = class(TComponent)
end;
TPlayer = class(TComponent)
Gun1, Gun2: TGun;
constructor Create(AOwner: TComponent); override;
end;
constructor TPlayer.Create(AOwner: TComponent);
begin
inherited;
Gun1 := TGun.Create(Self);
Gun2 := TGun.Create(Self);
end;
Note that we need to override a virtual TComponent
constructor here. So we cannot change the constructor parameters. (Actually, you can — declare a new constructor with reintroduce
. But be careful, as some functionality, e.g. streaming, will still use the virtual constructor, so make sure it works right in either case.)
Note that you can always use nil
value for the owner. This way the "ownership" mechanism will not be used for this component. It makes sense if you need to use the TComponent
descendant, but you want to always manually free it. To do this, you would create a component descendant like this: ManualGun := TGun.Create(nil);
.
Another mechanism for automatic freeing is the OwnsObjects
functionality (by default already true
!) of list-classes like TFPGObjectList
or TObjectList
. So we can also write:
uses SysUtils, Classes, FGL;
type
TGun = class
end;
TGunList = specialize TFPGObjectList<TGun>;
TPlayer = class
Guns: TGunList;
Gun1, Gun2: TGun;
constructor Create;
destructor Destroy; override;
end;
constructor TPlayer.Create;
begin
inherited;
// Actually, the parameter true (OwnsObjects) is already the default
Guns := TGunList.Create(true);
Gun1 := TGun.Create;
Guns.Add(Gun1);
Gun2 := TGun.Create;
Guns.Add(Gun2);
end;
destructor TPlayer.Destroy;
begin
{ We have to take care to free the list.
It will automatically free its contents. }
FreeAndNil(Guns);
{ No need to free the Gun1, Gun2 anymore. It's a nice habit to set to "nil"
their references now, as we know they are freed. In this simple class,
with so simple destructor, it's obvious that they cannot be accessed
anymore -- but doing this pays off in case of larger and more complicated
destructors.
Alternatively, we could avoid declaring Gun1 and Gun2,
and instead use Guns[0] and Guns[1] in own code.
Or create a method like Gun1 that returns Guns[0]. }
Gun1 := nil;
Gun2 := nil;
inherited;
end;
Beware that the list classes "ownership" mechanism is simple, and you will get an error if you free the instance using some other means, while it’s also contained within a list. Use the Extract
method to remove something from a list without freeing it, thus taking the responsibility to free it yourself.
In the Castle Game Engine: The descendants of TX3DNode
have automatic memory management when inserted as children of another TX3DNode
. The root X3D node, TX3DRootNode
, is in turn usually owned by TCastleSceneCore
. Some other things also have a simple ownership mechanism — look for parameters and properties called OwnsXxx
.
5.4. The virtual destructor called Destroy
As you saw in the examples above, when the class is destroyed, its destructor
called Destroy
is called.
In theory, you could have multiple destructors, but in practice it’s almost never a good idea. It’s much easier to have only one destructor called Destroy
, which is in turn called by the Free
method, which is in turn called by the FreeAndNil
procedure.
The Destroy
destructor in the TObject
is defined as a virtual method, so you should always mark it with the override
keyword in all your classes (since all classes descend from TObject
). This makes the Free
method work correctly. Recall how the virtual methods work from the Virtual methods, override and reintroduce.
Зауваження
|
This information about destructors is, indeed, inconsistent with the constructors. It’s normal that a class has multiple constructors. Usually they are all called Also, the This all gives you a bit of extra flexibility when defining constructors. It is often not necessary to make them virtual, so by default you’re not forced to do it. Note, however, that this changes for |
5.5. Free notification
If you copy a reference to the instance, such that you have two references to the same memory, and then one of them is freed — the other one becomes a "dangling pointer". It should not be accessed, as it points to a memory that is no longer allocated. Accessing it may result in a runtime error, or garbage being returned (as the memory may be reused for other stuff in your program).
Using the FreeAndNil
to free the instance doesn’t help here. FreeAndNil
sets to nil
only the reference it got — there’s no way for it to set all other references automatically. Consider this code:
var
Obj1, Obj2: TObject;
begin
Obj1 := TObject.Create;
Obj2 := Obj1;
FreeAndNil(Obj1);
// what happens if we access Obj1 or Obj2 here?
end;
-
At the end of this block, the
Obj1
isnil
. If some code has to access it, it can reliably useif Obj1 <> nil then …
to avoid calling methods on a freed instance, likeif Obj1 <> nil then WriteLn(Obj1.ClassName);
Trying to access a field of a
nil
instance results in a predictable exception at runtime. So even if some code will not checkObj1 <> nil
, and will blindly accessObj1
field, you will get a clear exception at runtime.Same goes for calling a virtual method, or calling a non-virtual method that accessed a field of a
nil
instance. -
With
Obj2
, things are less predictable. It’s notnil
, but it’s invalid. Trying to access a field of a non-nil invalid instance results in an unpredictable behavior — maybe an access violation exception, maybe a garbage data returned.
There are various solutions to it:
-
One solution is to, well, be careful and read the documentation. Don’t assume anything about the lifetime of the reference, if it’s created by other code. If a class
TCar
has a field pointing to some instance ofTWheel
, it’s a convention that the reference to wheel is valid as long as the reference to car exists, and the car will free its wheels inside its destructor. But that’s just a convention, the documentation should mention if there’s something more complicated going on. -
In the above example, right after freeing the
Obj1
instance, you can simply set theObj2
variable explicitly tonil
. That’s trivial in this simple case. -
The most future-proof solution is to use
TComponent
class "free notification" mechanism. One component can be notified when another component is freed, and thus set its reference tonil
.Thus you get something like a weak reference. It can cope with various usage scenarios, for example you can let the code from outside of the class to set your reference, and the outside code can also free the instance at anytime.
This requires both classes to descend from
TComponent
. Using it in general boils down to callingFreeNotification
,RemoveFreeNotification
, and overridingNotification
.Here’s a complete example, showing how to use this mechanism, together with constructor / destructor and a setter property. Sometimes it can be done simpler, but this is the full-blown version that is always correct:)
type TControl = class(TComponent) end; TContainer = class(TComponent) private FSomeSpecialControl: TControl; procedure SetSomeSpecialControl(const Value: TControl); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; public destructor Destroy; override; property SomeSpecialControl: TControl read FSomeSpecialControl write SetSomeSpecialControl; end; implementation procedure TContainer.Notification(AComponent: TComponent; Operation: TOperation); begin inherited; if (Operation = opRemove) and (AComponent = FSomeSpecialControl) then { set to nil by SetSomeSpecialControl to clean nicely } SomeSpecialControl := nil; end; procedure TContainer.SetSomeSpecialControl(const Value: TControl); begin if FSomeSpecialControl <> Value then begin if FSomeSpecialControl <> nil then FSomeSpecialControl.RemoveFreeNotification(Self); FSomeSpecialControl := Value; if FSomeSpecialControl <> nil then FSomeSpecialControl.FreeNotification(Self); end; end; destructor TContainer.Destroy; begin { set to nil by SetSomeSpecialControl, to detach free notification } SomeSpecialControl := nil; inherited; end;
5.6. Free notification observer (Castle Game Engine)
In Castle Game Engine we encourage to use TFreeNotificationObserver
from CastleClassUtils
unit instead of directly calling FreeNotification
, RemoveFreeNotification
and overriding Notification
.
In general using TFreeNotificationObserver
looks a bit simpler than using FreeNotification
mechanism directly (though I admit it is a matter of taste). But in particular when the same class instance must be observed because of multiple reasons then TFreeNotificationObserver
is much simpler to use (directly using FreeNotification
in this case can get complicated, as you have to watch to not unregister the notification too soon).
This is the example code using TFreeNotificationObserver
, to achieve the same effect as example in the previous section:
type
TControl = class(TComponent)
end;
TContainer = class(TComponent)
private
FSomeSpecialControlObserver: TFreeNotificationObserver;
FSomeSpecialControl: TControl;
procedure SetSomeSpecialControl(const Value: TControl);
procedure SomeSpecialControlFreeNotification(const Sender: TFreeNotificationObserver);
public
constructor Create(AOwner: TComponent); override;
property SomeSpecialControl: TControl
read FSomeSpecialControl write FSomeSpecialControl;
end;
implementation
uses CastleComponentSerialize;
constructor TContainer.Create(AOwner: TComponent);
begin
inherited;
FSomeSpecialControlObserver := TFreeNotificationObserver.Create(Self);
FSomeSpecialControlObserver.OnFreeNotification := {$ifdef FPC}@{$endif} SomeSpecialControlFreeNotification;
end;
procedure TContainer.SetSomeSpecialControl(const Value: TControl);
begin
if FSomeSpecialControl <> Value then
begin
FSomeSpecialControl := Value;
FSomeSpecialControlObserver.Observed := Value;
end;
end;
procedure TContainer.SomeSpecialControlFreeNotification(const Sender: TFreeNotificationObserver);
begin
// set property to nil when the referenced component is freed
SomeSpecialControl := nil;
end;
6. Exceptions
6.1. Overview
Exceptions allow to interrupt the normal execution of the code.
-
At any point within the program, you can raise an exception using the
raise
keyword. In effect the lines of code following theraise …
call will not execute. -
An exception may be caught using a
try … except … end
construction. Catching an exception means that you somehow "deal" with exception, and the following code should execute as usual, the exception is no longer propagated upward.Note: If an exception is raised but never caught, it will cause the entire application to stop with an error.
-
But in LCL applications, the exceptions are always caught around events (and cause LCL dialog box) if you don’t catch them earlier.
-
In Castle Game Engine applications using
CastleWindow
, we similarly always catch exceptions around your events (and display proper dialog box). -
So it is not so easy to make an exception that is not caught anywhere (not caught in your code, LCL code, CGE code…).
-
-
Although an exception breaks the execution, you can use the
try … finally … end
construction to execute some code always, even if the code was interrupted by an exception.The
try … finally … end
construction also works when code is interrupted byBreak
orContinue
orExit
keywords. The point is to always execute code in thefinally
section.
An "exception" is, in general, any class instance.
-
The compiler does not enforce any particular class. You just must call
raise XXX
whereXXX
is an instance of any class. Any class (so, anything descending fromTObject
) is fine. -
It is a standard convention for exception classes to descend from a special
Exception
class. TheException
class extendsTObject
, adding a stringMessage
property and a constructor to easily set this property. All exceptions raised by the standard library descend fromException
. We advise to follow this convention. -
Exception classes (by convention) have names that start with
E
, notT
. LikeESomethingBadHappened
. -
The compiler will automatically free exception object when it is handled. Don’t free it yourself.
In most cases, you just construct the object at the same time when you call
raise
, likeraise ESomethingBadHappened.Create('Description of what bad thing happened.')
.
6.2. Raising
If you want to raise your own exception, declare it and call raise …
when appropriate:
type
EInvalidParameter = class(Exception);
function ReadParameter: String;
begin
Result := Readln;
if Pos(' ', Result) <> 0 then
raise EInvalidParameter.Create('Invalid parameter, space is not allowed');
end;
Note that the expression following the raise
should be a valid class instance to raise. You will almost always create the exception instance here.
You can also use the CreateFmt
constructor, which is a comfortable shortcut to Create(Format(MessageFormat, MessageArguments))
. This is a common way to provide more information to the exception message. We can improve the previous example like this:
type
EInvalidParameter = class(Exception);
function ReadParameter: String;
begin
Result := Readln;
if Pos(' ', Result) <> 0 then
raise EInvalidParameter.CreateFmt('Invalid parameter %s, space is not allowed', [Result]);
end;
6.3. Catching
You can catch an exception like this:
var
Parameter1, Parameter2, Parameter3: String;
begin
try
Writeln('Input 1st parameter:');
Parameter1 := ReadParameter;
Writeln('Input 2nd parameter:');
Parameter2 := ReadParameter;
Writeln('Input 3rd parameter:');
Parameter3 := ReadParameter;
except
// capture EInvalidParameter raised by one of the above ReadParameter calls
on EInvalidParameter do
Writeln('EInvalidParameter exception occurred');
end;
end;
To improve the above example, we can declare the name for the exception instance (we will use E
in the example). This way we can print the exception message:
try
...
except
on E: EInvalidParameter do
Writeln('EInvalidParameter exception occurred with message: ' + E.Message);
end;
One could also test for multiple exception classes:
try
...
except
on E: EInvalidParameter do
Writeln('EInvalidParameter exception occurred with message: ' + E.Message);
on E: ESomeOtherException do
Writeln('ESomeOtherException exception occurred with message: ' + E.Message);
end;
You can also react to any exception raised, if you don’t use any on
expression:
try
...
except
Writeln('Warning: Some exception occurred');
end;
// WARNING: DO NOT FOLLOW THIS EXAMPLE WITHOUT READING A WARNING BELOW
// ABOUT "CAPTURING ALL EXCEPTIONS"
In general you should only catch exceptions of a specific class, that signal a particular problem that you know what to do with. Be careful with catching exceptions of a general type (like catching any Exception
or any TObject
), as you may easily catch too much, and later cause troubles when debugging other problems. As in all programming languages with exceptions, the good rule to follow is to never capture an exception that you do not know how to handle. In particular, do not capture an exception just as a simple workaround of the problem, without investigating first why the exception occurs.
-
Does the exception indicate a problem in user input? Then you should report it to user.
-
Does the exception indicate a bug in your code? Then you should fix the code, to avoid the exception from happening at all.
Another way to capture all exceptions is to use:
try
...
except
on E: TObject do
Writeln('Warning: Some exception occurred');
end;
// WARNING: DO NOT FOLLOW THIS EXAMPLE WITHOUT READING A WARNING ABOVE
// ABOUT "CAPTURING ALL EXCEPTIONS"
Although usually it is enough to capture Exception
:
try
...
except
on E: Exception do
Writeln('Warning: Some exception occurred: ' + E.ClassName + ', message: ' + E.Message);
end;
// WARNING: DO NOT FOLLOW THIS EXAMPLE WITHOUT READING A WARNING ABOVE
// ABOUT "CAPTURING ALL EXCEPTIONS"
You can "re-raise" the exception in the except … end
block, if you decide so. You can just do raise E
if the exception instance is E
, you can also just use parameter-less raise
. For example:
try
...
except
on E: EInvalidSoundFile do
begin
if E.InvalidUrl = 'http://example.com/blablah.wav' then
Writeln('Warning: loading http://example.com/blablah.wav failed, ignore it')
else
raise;
end;
end;
Note that, although the exception is an instance of an object, you should never manually free it after raising. The compiler will generate proper code that makes sure to free the exception object once it’s handled.
6.4. Finally (doing things regardless if an exception occurred)
Often you use try .. finally .. end
construction to free an instance of some object, regardless if an exception occurred when using this object. The way to write it looks like this:
procedure MyProcedure;
var
MyInstance: TMyClass;
begin
MyInstance := TMyClass.Create;
try
MyInstance.DoSomething;
MyInstance.DoSomethingElse;
finally
FreeAndNil(MyInstance);
end;
end;
This works reliably always, and does not cause memory leaks, even if MyInstance.DoSomething
or MyInstance.DoSomethingElse
raise an exception.
Note that this takes into account that local variables, like MyInstance
above, have undefined values (may contain random "memory garbage") before the first assignment. That is, writing something like this would not be valid:
// INCORRECT EXAMPLE:
procedure MyProcedure;
var
MyInstance: TMyClass;
begin
try
CallSomeOtherProcedure;
MyInstance := TMyClass.Create;
MyInstance.DoSomething;
MyInstance.DoSomethingElse;
finally
FreeAndNil(MyInstance);
end;
end;
The above example is not valid: if an exception occurs within TMyClass.Create
(a constructor may also raise an exception), or within the CallSomeOtherProcedure
, then the MyInstance
variable is not initialized. Calling FreeAndNil(MyInstance)
will try to call destructor of MyInstance
, which will most likely crash with Access Violation (Segmentation Fault). In effect, one exception causes another exception, which will make the error report not very useful: you will not see the message of the original exception.
Sometimes it is justified to fix the above code by first initializing all local variables to nil
(on which calling FreeAndNil
is safe, and will not do anything). This makes sense if you free a lot of class instances. So the two code examples below work equally well:
procedure MyProcedure;
var
MyInstance1: TMyClass1;
MyInstance2: TMyClass2;
MyInstance3: TMyClass3;
begin
MyInstance1 := TMyClass1.Create;
try
MyInstance1.DoSomething;
MyInstance2 := TMyClass2.Create;
try
MyInstance2.DoSomethingElse;
MyInstance3 := TMyClass3.Create;
try
MyInstance3.DoYetAnotherThing;
finally
FreeAndNil(MyInstance3);
end;
finally
FreeAndNil(MyInstance2);
end;
finally
FreeAndNil(MyInstance1);
end;
end;
It is probably more readable in the form below:
procedure MyProcedure;
var
MyInstance1: TMyClass1;
MyInstance2: TMyClass2;
MyInstance3: TMyClass3;
begin
MyInstance1 := nil;
MyInstance2 := nil;
MyInstance3 := nil;
try
MyInstance1 := TMyClass1.Create;
MyInstance1.DoSomething;
MyInstance2 := TMyClass2.Create;
MyInstance2.DoSomethingElse;
MyInstance3 := TMyClass3.Create;
MyInstance3.DoYetAnotherThing;
finally
FreeAndNil(MyInstance3);
FreeAndNil(MyInstance2);
FreeAndNil(MyInstance1);
end;
end;
Зауваження
|
In this simple example, you could also make a valid argument that the code should be split into 3 separate procedures, one calling each other. |
6.5. How the exceptions are displayed by various libraries
-
In case of Lazarus LCL, the exceptions raised during events (various callbacks assigned to
OnXxx
properties of LCL components) will be captured and will result in a nice dialog message, that allows the user to continue and stop the application. This means that your own exceptions do not "get out" fromApplication.ProcessMessages
, so they do not automatically break the application. You can configure what happens usingTApplicationProperties.OnException
. -
Similarly in case of Castle Game Engine with
CastleWindow
: the exception is internally captured and results in nice error message. So exceptions do not "get out" fromApplication.ProcessMessages
. Again, you can configure what happens usingApplication.OnException
. -
Some other GUI libraries may do a similar thing to above.
-
In case of other applications, you can configure how the exception is displayed by assigning a global callback to
OnHaltProgram
.
7. Run-time library
7.1. Input/output using streams
Modern programs should use TStream
class and its many descendants to do input / output. It has many useful descendants, like TFileStream
, TMemoryStream
, TStringStream
.
{$mode objfpc}{$H+}{$J-}
uses
SysUtils, Classes;
var
S: TStream;
InputInt, OutputInt: Integer;
begin
InputInt := 666;
S := TFileStream.Create('my_binary_file.data', fmCreate);
try
S.WriteBuffer(InputInt, SizeOf(InputInt));
finally
FreeAndNil(S);
end;
S := TFileStream.Create('my_binary_file.data', fmOpenRead);
try
S.ReadBuffer(OutputInt, SizeOf(OutputInt));
finally
FreeAndNil(S);
end;
WriteLn('Прочитано з файлу отримано ціле число: ', OutputInt);
end.
In the Castle Game Engine: You should use the Download
function to create a stream that obtains data from any URL. Regular files, HTTP and HTTPS resources, Android assets and more are supported this way. Moreover, to open the resource inside your game data (in the data
subdirectory) use the special castle-data:/xxx
URL. Examples:
EnableNetwork := true;
S := Download('https://castle-engine.io/latest.zip');
S := Download('file:///home/michalis/my_binary_file.data');
S := Download('castle-data:/gui/my_image.png');
To read text files, we advise using the TTextReader
class. It provides a line-oriented API, and wraps a TStream
inside. The TTextReader
constructor can take a ready URL, or you can pass there your custom TStream
source.
Text := TTextReader.Create('castle-data:/my_data.txt');
try
while not Text.Eof do
WriteLnLog('NextLine', Text.ReadLn);
finally
FreeAndNil(Text);
end;
7.2. Containers (lists, dictionaries) using generics
The language and run-time library offer various flexible containers. There are a number of non-generic classes (like TList
and TObjectList
from the Contnrs
unit), there are also dynamic arrays (array of TMyType
). But to get the most flexibility and type-safety, I advise using generic containers for most of your needs.
The generic containers give you a lot of helpful methods to add, remove, iterate, search, sort… The compiler also knows (and checks) that the container holds only items of the appropriate type.
There are three libraries providing generics containers in FPC now:
-
Generics.Collections
unit and friends (since FPC >= 3.2.0) -
FGL
unit -
GVector
unit and friends (together infcl-stl
)
We advise using the Generics.Collections
unit. The generic containers it implements are
-
packed with useful features,
-
very efficient (in particular important for accessing dictionaries by keys),
-
compatible between FPC and Delphi,
-
the naming is consistent with other parts of the standard library (like the non-generic containers from the
Contnrs
unit).
In the Castle Game Engine: We use the Generics.Collections
intensively throughout the engine, and advise you to use Generics.Collections
in your applications too!
Most important classes from the Generics.Collections
unit are:
- TList
-
A generic list of types.
- TObjectList
-
A generic list of object instances. It can "own" children, which means that it will free them automatically.
- TDictionary
-
A generic dictionary.
- TObjectDictionary
-
A generic dictionary, that can "own" the keys and/or values.
Here’s how to you use a simple generic TObjectList
:
{$mode objfpc}{$H+}{$J-}
uses SysUtils, Generics.Collections;
type
TApple = class
Name: string;
end;
TAppleList = specialize TObjectList<TApple>;
var
A: TApple;
Apples: TAppleList;
begin
Apples := TAppleList.Create(true);
try
A := TApple.Create;
A.Name := 'моє яблуко';
Apples.Add(A);
A := TApple.Create;
A.Name := 'інше яблуко';
Apples.Add(A);
Writeln('Кількість: ', Apples.Count);
Writeln(Apples[0].Name);
Writeln(Apples[1].Name);
finally FreeAndNil(Apples) end;
end.
Note that some operations require comparing two items, like sorting and searching (e.g. by Sort
and IndexOf
methods). The Generics.Collections
containers use for this a comparer. The default comparer is reasonable for all types, even for records (in which case it compares memory contents, which is a reasonable default at least for searching using IndexOf
).
When sorting the list you can provide a custom comparer as a parameter. The comparer is a class implementing the IComparer
interface. In practice, you usually define the appropriate callback, and use TComparer<T>.Construct
method to wrap this callback into an IComparer
instance. An example of doing this is below:
{$mode objfpc}{$H+}{$J-}
{ If GENERICS_CONSTREF is defined, then various routines used with Generics.Collections
(like callbacks we pass to TComparer, or OnNotify callback or Notify virtual method)
should have "constref" parameter, not "const".
This was the case of FPC<= 3.2.0, FPC changed it in
https://gitlab.com/freepascal.org/fpc/source/-/commit/693491048bf2c6f9122a0d8b044ad0e55382354d .
It is also applied to FPC fixes branch 3.2.3. }
{$ifdef VER3_0} {$define GENERICS_CONSTREF} {$endif}
{$ifdef VER3_2_0} {$define GENERICS_CONSTREF} {$endif}
{$ifdef VER3_2_2} {$define GENERICS_CONSTREF} {$endif}
uses SysUtils, Generics.Defaults, Generics.Collections;
type
TApple = class
Name: string;
end;
TAppleList = specialize TObjectList<TApple>;
function CompareApples(
{$ifdef GENERICS_CONSTREF}constref{$else}const{$endif}
Left, Right: TApple): Integer;
begin
Result := AnsiCompareStr(Left.Name, Right.Name);
end;
type
TAppleComparer = specialize TComparer<TApple>;
var
A: TApple;
L: TAppleList;
begin
L := TAppleList.Create(true);
try
A := TApple.Create;
A.Name := '11';
L.Add(A);
A := TApple.Create;
A.Name := '33';
L.Add(A);
A := TApple.Create;
A.Name := '22';
L.Add(A);
L.Sort(TAppleComparer.Construct(@CompareApples));
Writeln('Кількість: ', L.Count);
Writeln(L[0].Name);
Writeln(L[1].Name);
Writeln(L[2].Name);
finally FreeAndNil(L) end;
end.
The TDictionary
class implements a dictionary, also known as a map (key → value), also known as an associative array. Its API is a bit similar to the C# TDictionary
class. It has useful iterators for keys, values, and pairs of key→value.
An example code using a dictionary:
{$mode objfpc}{$H+}{$J-}
uses SysUtils, Generics.Collections;
type
TApple = class
Name: string;
end;
TAppleDictionary = specialize TDictionary<string, TApple>;
var
Apples: TAppleDictionary;
A, FoundA: TApple;
ApplePair: TAppleDictionary.TDictionaryPair;
AppleKey: string;
begin
Apples := TAppleDictionary.Create;
try
A := TApple.Create;
A.Name := 'моє яблуко';
Apples.AddOrSetValue('яблучний ключ 1', A);
if Apples.TryGetValue('яблучний ключ 1', FoundA) then
Writeln('Знайдено яблуко під ключем "яблучний ключ 1" з назвою: ' +
FoundA.Name);
for AppleKey in Apples.Keys do
Writeln('Знайдено яблучний ключ: ' + AppleKey);
for A in Apples.Values do
Writeln('Знайдено яблучне значення: ' + A.Name);
for ApplePair in Apples do
Writeln('Знайдено яблучні ключ->значення: ' +
ApplePair.Key + '->' + ApplePair.Value.Name);
{ Рядок нижче також працює, але його можна використовувати
лише для встановлення *існуючого* ключа словника.
Замість цього зазвичай використовуйте AddOrSetValue,
щоб установити або додати новий ключ, якщо це необхідно. }
// Apples['яблучний ключ 1'] := ... ;
Apples.Remove('яблучний ключ 1');
{ Зверніть увагу, що TDictionary не володіє елементами,
вам потрібно звільнити їх самостійно.
Ми могли б використовувати TObjectDictionary,
щоб мати автоматичний механізм власності. }
A.Free;
finally FreeAndNil(Apples) end;
end.
The TObjectDictionary
can additionally own the dictionary keys and/or values, which means that they will be automatically freed. Be careful to only own keys and/or values if they are object instances. If you set to "owned" some other type, like an Integer
(for example, if your keys are Integer
, and you include doOwnsKeys
), you will get a nasty crash when the code executes.
An example code using the TObjectDictionary
is below. Compile this example with memory leak detection, like fpc -gl -gh generics_object_dictionary.lpr
, to see that everything is freed when program exits.
{$mode objfpc}{$H+}{$J-}
uses SysUtils, Generics.Collections;
type
TApple = class
Name: string;
end;
TAppleDictionary = specialize TObjectDictionary<string, TApple>;
var
Apples: TAppleDictionary;
A: TApple;
ApplePair: TAppleDictionary.TDictionaryPair;
begin
Apples := TAppleDictionary.Create([doOwnsValues]);
try
A := TApple.Create;
A.Name := 'моє яблуко';
Apples.AddOrSetValue('яблучний ключ 1', A);
for ApplePair in Apples do
Writeln('Знайдено яблучний ключ->значення: ' +
ApplePair.Key + '->' + ApplePair.Value.Name);
Apples.Remove('яблучний ключ 1');
finally FreeAndNil(Apples) end;
end.
If you prefer using the FGL
unit instead of Generics.Collections
, the most important classes from the FGL
unit are:
- TFPGList
-
A generic list of types.
- TFPGObjectList
-
A generic list of object instances. It can "own" children.
- TFPGMap
-
A generic dictionary.
In FGL
unit, the TFPGList
can be only used for types for which the equality operator (=) is defined. For TFPGMap
the "greater than" (>) and "less than" (<) operators must be defined for the key type. If you want to use these lists with types that don’t have built-in comparison operators (e.g. with records), you have to overload their operators as shown in the Operator overloading.
In the Castle Game Engine we include a unit CastleGenericLists
that adds TGenericStructList
and TGenericStructMap
classes. They are similar to TFPGList
and TFPGMap
, but they do not require a definition of the comparison operators for the appropriate type (instead, they compare memory contents, which is often appropriate for records or method pointers). But the CastleGenericLists
unit is deprecated since the engine version 6.3, as we advise using Generics.Collections
instead.
If you want to know more about the generics, see Generics.
7.3. Cloning: TPersistent.Assign
Copying the class instances by a simple assignment operator copies the reference.
var
X, Y: TMyObject;
begin
X := TMyObject.Create;
Y := X;
// X and Y are now two pointers to the same data
Y.MyField := 123; // this also changes X.MyField
FreeAndNil(X);
end;
To copy the class instance contents, the standard approach is to derive your class from TPersistent
, and override its Assign
method. Once it’s implemented properly in TMyObject
, you use it like this:
var
X, Y: TMyObject;
begin
X := TMyObject.Create;
Y := TMyObject.Create;
Y.Assign(X);
Y.MyField := 123; // this does not change X.MyField
FreeAndNil(X);
FreeAndNil(Y);
end;
To make it work, you need to implement the Assign
method to actually copy the fields you want. You should carefully implement the Assign
method, to copy from a class that may be a descendant of the current class.
{$mode objfpc}{$H+}{$J-}
uses
SysUtils, Classes;
type
TMyClass = class(TPersistent)
public
MyInt: Integer;
procedure Assign(Source: TPersistent); override;
end;
TMyClassDescendant = class(TMyClass)
public
MyString: string;
procedure Assign(Source: TPersistent); override;
end;
procedure TMyClass.Assign(Source: TPersistent);
var
SourceMyClass: TMyClass;
begin
if Source is TMyClass then
begin
SourceMyClass := TMyClass(Source);
MyInt := SourceMyClass.MyInt;
// Xxx := SourceMyClass.Xxx; // add new fields here
end else
{ Since TMyClass is a direct TPersistent descendant,
it calls inherited ONLY when it cannot handle Source class.
See comments below. }
inherited Assign(Source);
end;
procedure TMyClassDescendant.Assign(Source: TPersistent);
var
SourceMyClassDescendant: TMyClassDescendant;
begin
if Source is TMyClassDescendant then
begin
SourceMyClassDescendant := TMyClassDescendant(Source);
MyString := SourceMyClassDescendant.MyString;
// Xxx := SourceMyClassDescendant.Xxx; // add new fields here
end;
{ Since TMyClassDescendant has an ancestor that already overrides
Assign (in TMyClass.Assign), it calls inherited ALWAYS,
to allow TMyClass.Assign to handle remaining fields.
See comments below for a detailed reasoning. }
inherited Assign(Source);
end;
var
C1, C2: TMyClass;
CD1, CD2: TMyClassDescendant;
begin
// test TMyClass.Assign
C1 := TMyClass.Create;
C2 := TMyClass.Create;
try
C1.MyInt := 666;
C2.Assign(C1);
WriteLn('C2 state: ', C2.MyInt);
finally
FreeAndNil(C1);
FreeAndNil(C2);
end;
// test TMyClassDescendant.Assign
CD1 := TMyClassDescendant.Create;
CD2 := TMyClassDescendant.Create;
try
CD1.MyInt := 44;
CD1.MyString := 'blah';
CD2.Assign(CD1);
WriteLn('CD2 state: ', CD2.MyInt, ' ', CD2.MyString);
finally
FreeAndNil(CD1);
FreeAndNil(CD2);
end;
end.
Sometimes it’s more comfortable to alternatively override the AssignTo
method in the source class, instead of overriding the Assign
method in the destination class.
Be careful when you call inherited
in the overridden Assign
implementation. There are two situations:
- Your class is a direct descendant of the
TPersistent
class. (Or, it’s not a direct descendant ofTPersistent
, but no ancestor has overridden theAssign
method.) -
In this case, your class should use the
inherited
keyword (to call theTPersistent.Assign
) only if you cannot handle the assignment in your code. - Your class descends from some class that has already overridden the
Assign
method. -
In this case, your class should always use the
inherited
keyword (to call the ancestorAssign
). In general, callinginherited
in overridden methods is usually a good idea.
To understand the reason behind the above rule (when you should call, and when you should not call inherited
from the Assign
implementation), and how it relates to the AssignTo
method, let’s look at the TPersistent.Assign
and TPersistent.AssignTo
implementations:
procedure TPersistent.Assign(Source: TPersistent);
begin
if Source <> nil then
Source.AssignTo(Self)
else
raise EConvertError...
end;
procedure TPersistent.AssignTo(Destination: TPersistent);
begin
raise EConvertError...
end;
Зауваження
|
This is not the exact implementation of TPersistent . I copied the FPC standard library code, but then I simplified it to hide unimportant details about the exception message.
|
The conclusions you can get from the above are:
-
If neither
Assign
norAssignTo
are overridden, then calling them will result in an exception. -
Also, note that there is no code in
TPersistent
implementation that automatically copies all the fields (or all the published fields) of the classes. That’s why you need to do that yourself, by overridingAssign
in all the classes. You can use RTTI (runtime type information) for that, but for simple cases you will probably just list the fields to be copied manually.
When you have a class like TApple
, your TApple.Assign
implementation usually deals with copying fields that are specific to the TApple
class (not to the TApple
ancestor, like TFruit
). So, the TApple.Assign
implementation usually checks whether Source is TApple
at the beginning, before copying apple-related fields. Then, it calls inherited
to allow TFruit
to handle the rest of the fields.
Assuming that you implemented TFruit.Assign
and TApple.Assign
following the standard pattern (as shown in the example above), the effect is like this:
-
If you pass
TApple
instance toTApple.Assign
, it will work and copy all the fields. -
If you pass
TOrange
instance toTApple.Assign
, it will work and only copy the common fields shared by bothTOrange
andTApple
. In other words, the fields defined atTFruit
. -
If you pass
TWerewolf
instance toTApple.Assign
, it will raise an exception (becauseTApple.Assign
will callTFruit.Assign
which will callTPersistent.Assign
which raises an exception).
Зауваження
|
Remember that when descending from TPersistent , the default visibility specifier is published , to allow streaming of TPersistent descendants. Not all field and property types are allowed in the published section. If you get errors related to it, and you don’t care about streaming, just change the visibility to public . See the Visibility specifiers.
|
8. Various language features
8.1. Local (nested) routines
Inside a larger routine (function, procedure, method) you can define a helper routine.
The local routine can freely access (read and write) all the parameters of a parent, and all the local variables of the parent that were declared above it. This is very powerful. It often allows to split long routines into a couple of small ones without much effort (as you don’t have to pass around all the necessary information in the parameters). Be careful to not overuse this feature — if many nested functions use (and even change) the same variable of the parent, the code may get hard to follow.
These two examples are equivalent:
function SumOfSquares(const N: Integer): Integer;
function Square(const Value: Integer): Integer;
begin
Result := Value * Value;
end;
var
I: Integer;
begin
Result := 0;
for I := 0 to N do
Result := Result + Square(I);
end;
Another version, where we let the local routine Square
to access I
directly:
function SumOfSquares(const N: Integer): Integer;
var
I: Integer;
function Square: Integer;
begin
Result := I * I;
end;
begin
Result := 0;
for I := 0 to N do
Result := Result + Square;
end;
Local routines can go to any depth — which means that you can define a local routine within another local routine. So you can go wild (but please don’t go too wild, or the code will get unreadable:).
8.2. Callbacks (aka events, aka pointers to functions, aka procedural variables)
They allow to call a function indirectly, through to a variable. The variable can be assigned at runtime to point to any function with matching parameter types and return types.
The callback can be:
-
Normal, which means it can point to any normal routine (not a method, not local).
{$mode objfpc}{$H+}{$J-} function Add(const A, B: Integer): Integer; begin Result := A + B; end; function Multiply(const A, B: Integer): Integer; begin Result := A * B; end; type TMyFunction = function (const A, B: Integer): Integer; function ProcessTheList(const F: TMyFunction): Integer; var I: Integer; begin Result := 1; for I := 2 to 10 do Result := F(Result, I); end; var SomeFunction: TMyFunction; begin SomeFunction := @Add; WriteLn('1 + 2 + 3 ... + 10 = ', ProcessTheList(SomeFunction)); SomeFunction := @Multiply; WriteLn('1 * 2 * 3 ... * 10 = ', ProcessTheList(SomeFunction)); end.
-
A method: declare with
of object
at the end.{$mode objfpc}{$H+}{$J-} uses SysUtils; type TMyMethod = procedure (const A: Integer) of object; TMyClass = class CurrentValue: Integer; procedure Add(const A: Integer); procedure Multiply(const A: Integer); procedure ProcessTheList(const M: TMyMethod); end; procedure TMyClass.Add(const A: Integer); begin CurrentValue := CurrentValue + A; end; procedure TMyClass.Multiply(const A: Integer); begin CurrentValue := CurrentValue * A; end; procedure TMyClass.ProcessTheList(const M: TMyMethod); var I: Integer; begin CurrentValue := 1; for I := 2 to 10 do M(I); end; var C: TMyClass; begin C := TMyClass.Create; try C.ProcessTheList(@C.Add); WriteLn('1 + 2 + 3 ... + 10 = ', C.CurrentValue); C.ProcessTheList(@C.Multiply); WriteLn('1 * 2 * 3 ... * 10 = ', C.CurrentValue); finally FreeAndNil(C); end; end.
Note that you cannot pass global procedures / functions as methods. They are incompatible. If you have to provide an
of object
callback, but don’t want to create a dummy class instance, you can pass Class methods as methods.type TMyMethod = function (const A, B: Integer): Integer of object; TMyClass = class class function Add(const A, B: Integer): Integer class function Multiply(const A, B: Integer): Integer end; var M: TMyMethod; begin M := @TMyClass(nil).Add; M := @TMyClass(nil).Multiply; end;
Unfortunately, you need to write ugly
@TMyClass(nil).Add
instead of just@TMyClass.Add
. -
A (possibly) local routine: declare with
is nested
at the end, and make sure to use{$modeswitch nestedprocvars}
directive for the code. These go hand-in-hand with Local (nested) routines.
8.3. Generics
A powerful feature of any modern language. The definition of something (typically, of a class) can be parameterized with another type. The most typical example is when you need to create a container (a list, dictionary, tree, graph…): you can define a list of type T, and then specialize it to instantly get a list of integers, a list of strings, a list of TMyRecord, and so on.
The generics in Pascal work much like generics in C++. Which means that they are "expanded" at the specialization time, a little like macros (but much safer than macros; for example, the identifiers are resolved at the time of generic definition, not at specialization, so you cannot "inject" any unexpected behavior when specializing the generic). In effect this means that they are very fast (can be optimized for each particular type) and work with types of any size. You can use a primitive type (integer, float) as well as a record, as well as a class when specializing a generic.
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
type
generic TMyCalculator<T> = class
Value: T;
procedure Add(const A: T);
end;
procedure TMyCalculator.Add(const A: T);
begin
Value := Value + A;
end;
type
TMyFloatCalculator = specialize TMyCalculator<Single>;
TMyStringCalculator = specialize TMyCalculator<string>;
var
FloatCalc: TMyFloatCalculator;
StringCalc: TMyStringCalculator;
begin
FloatCalc := TMyFloatCalculator.Create;
try
FloatCalc.Add(3.14);
FloatCalc.Add(1);
WriteLn('FloatCalc: ', FloatCalc.Value:1:2);
finally
FreeAndNil(FloatCalc);
end;
StringCalc := TMyStringCalculator.Create;
try
StringCalc.Add('щось');
StringCalc.Add(' більше');
WriteLn('StringCalc: ', StringCalc.Value);
finally
FreeAndNil(StringCalc);
end;
end.
Generics are not limited to classes, you can have generic functions and procedures as well:
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
{ Примітка: для цього прикладу потрібен FPC 3.1.1 (не компілюється з FPC 3.0.0 або старішим). }
generic function Min<T>(const A, B: T): T;
begin
if A < B then
Result := A else
Result := B;
end;
begin
WriteLn('Min (1, 0): ', specialize Min<Integer>(1, 0));
WriteLn('Min (3.14, 5): ', specialize Min<Single>(3.14, 5):1:2);
WriteLn('Min (''a'', ''b''): ', specialize Min<string>('a', 'b'));
end.
See also the Containers (lists, dictionaries) using generics about important standard classes using generics.
8.4. Overloading
Methods (and global functions and procedures) with the same name are allowed, as long as they have different parameters. At compile time, the compiler detects which one you want to use, knowing the parameters you pass.
By default, the overloading uses the FPC approach, which means that all the methods in given namespace (a class or a unit) are equal, and hide the other methods in namespaces with less priority. For example, if you define a class with methods Foo(Integer)
and Foo(string)
, and it descends from a class with method Foo(Float)
, then the users of your new class will not be able to access the method Foo(Float)
easily (they still can --- if they typecast the class to its ancestor type). To overcome this, use the overload
keyword.
8.5. Preprocessor
You can use simple preprocessor directives for
-
conditional compilation (code depending on platform, or some custom switches),
-
to include one file in another,
-
you can also use parameter-less macros.
Note that macros with parameters are not allowed. In general, you should avoid using the preprocessor stuff… unless it’s really justified. The preprocessing happens before parsing, which means that you can "break" the normal syntax of the Pascal language. This is a powerful, but also somewhat dirty, feature.
{$mode objfpc}{$H+}{$J-}
unit PreprocessorStuff;
interface
{$ifdef FPC}
{ This is only defined when compiled by FPC, not other compilers (like Delphi). }
procedure Foo;
{$endif}
{ Define a NewLine constant. Here you can see how the normal syntax of Pascal
is "broken" by preprocessor directives. When you compile on Unix
(includes Linux, Android, Mac OS X), the compiler sees this:
const NewLine = #10;
When you compile on Windows, the compiler sees this:
const NewLine = #13#10;
On other operating systems, the code will fail to compile,
because a compiler sees this:
const NewLine = ;
It's a *good* thing that the compilation fails in this case -- if you
will have to port the program to an OS that is not Unix, not Windows,
you will be reminded by a compiler to choose the newline convention
on that system. }
const
NewLine =
{$ifdef UNIX} #10 {$endif}
{$ifdef MSWINDOWS} #13#10 {$endif} ;
{$define MY_SYMBOL}
{$ifdef MY_SYMBOL}
procedure Bar;
{$endif}
{$define CallingConventionMacro := unknown}
{$ifdef UNIX}
{$define CallingConventionMacro := cdecl}
{$endif}
{$ifdef MSWINDOWS}
{$define CallingConventionMacro := stdcall}
{$endif}
procedure RealProcedureName; CallingConventionMacro; external 'some_external_library';
implementation
{$include some_file.inc}
// $I is just a shortcut for $include
{$I some_other_file.inc}
end.
Include files have commonly the .inc
extension, and are used for two purposes:
-
The include file may only contain other compiler directives, that "configure" your source code. For example you could create a file
myconfig.inc
with these contents:{$mode objfpc} {$H+} {$J-} {$modeswitch advancedrecords} {$ifndef VER3} {$error This code can only be compiled using FPC version at least 3.x.} {$endif}
Now you can include this file using
{$I myconfig.inc}
in all your sources. -
The other common use is to split a large unit into many files, while still keeping it a single unit as far as the language rules are concerned. Do not overuse this technique — your first instinct should be to split a single unit into multiple units, not to split a single unit into multiple include files. Never the less, this is a useful technique.
-
It allows to avoid "exploding" the number of units, while still keeping your source code files short. For example, it may be better to have a single unit with "commonly used UI controls" than to create one unit for each UI control class, as the latter approach would make the typical "uses" clause long (since a typical UI code will depend on a couple of UI classes). But placing all these UI classes in a single
myunit.pas
file would make it a long file, unhandy to navigate, so splitting it into multiple include files may make sense. -
It allows to have a cross-platform unit interface with platform-dependent implementation easily. Basically you can do
{$ifdef UNIX} {$I my_unix_implementation.inc} {$endif} {$ifdef MSWINDOWS} {$I my_windows_implementation.inc} {$endif}
Sometimes this is better than writing a long code with many
{$ifdef UNIX}
,{$ifdef MSWINDOWS}
intermixed with normal code (variable declarations, routine implementation). The code is more readable this way. You can even use this technique more aggressively, by using the-Fi
command-line option of FPC to include some subdirectories only for specific platforms. Then you can have many version of include file{$I my platform_specific_implementation.inc}
and you simply include them, letting the compiler find the correct version.
-
8.6. Records
Record is just a container for other variables. It’s like a much, much simplified class: there is no inheritance or virtual methods. It is like a structure in C-like languages.
If you use the {$modeswitch advancedrecords}
directive, records can have methods and visibility specifiers. In general, language features that are available for classes, and do not break the simple predictable memory layout of a record, are then possible.
{$mode objfpc}{$H+}{$J-}
{$modeswitch advancedrecords}
type
TMyRecord = record
public
I, Square: Integer;
procedure WriteLnDescription;
end;
procedure TMyRecord.WriteLnDescription;
begin
WriteLn('Square of ', I, ' is ', Square);
end;
var
A: array [0..9] of TMyRecord;
R: TMyRecord;
I: Integer;
begin
for I := 0 to 9 do
begin
A[I].I := I;
A[I].Square := I * I;
end;
for R in A do
R.WriteLnDescription;
end.
In modern Object Pascal, your first instinct should be to design a class
, not a record
— because classes are packed with useful features, like constructors and inheritance.
But records are still very useful when you need speed or a predictable memory layout:
-
Records do not have any constructor or destructor. You just define a variable of a record type. It has undefined contents (memory garbage) at the beginning (except auto-managed types, like strings; they are guaranteed to be initialized to be empty, and finalized to free the reference count). So you have to be more careful when dealing with records, but it gives you some performance gain.
-
Arrays of records are nicely linear in memory, so they are cache-friendly.
-
The memory layout of records (size, padding between fields) is clearly defined in some situations: when you request the C layout, or when you use
packed record
. This is useful:-
to communicate with libraries written in other programming languages, when they expose an API based on records,
-
to read and write binary files,
-
to make dirty low-level tricks (like unsafe typecasting one type to another, being aware of their memory representation).
-
-
Records can also have
case
parts, which work like unions in C-like languages. They allows to treat the same memory piece as a different type, depending on your needs. As such, this allows for greater memory efficiency in some cases. And it allows for more dirty, low-level unsafe tricks:)
8.7. Old-style objects
In the old days, Turbo Pascal introduced another syntax for class-like functionality, using the object
keyword. It’s somewhat of a blend between the concept of a record
and a modern class
.
-
The old-style objects can be allocated / freed, and during that operation you can call their constructor / destructor.
-
But they can also be simply declared and used, like records. A simple
record
orobject
type is not a reference (pointer) to something, it’s simply the data. This makes them comfortable for small data, where calling allocation / free would be bothersome. -
Old-style objects offer inheritance and virtual methods, although with small differences from the modern classes. Be careful — bad things will happen if you try to use an object without calling its constructor, and the object has virtual methods.
It’s discouraged to use the old-style objects in most cases. Modern classes provide much more functionality. And when needed, records (including advanced records) can be used for performance. These concepts are usually a better idea than old-style objects.
8.8. Pointers
You can create a pointer to any other type. The pointer to type TMyRecord
is declared as ^TMyRecord
, and by convention is called PMyRecord
. This is a traditional example of a linked list of integers using records:
type
PMyRecord = ^TMyRecord;
TMyRecord = record
Value: Integer;
Next: PMyRecord;
end;
Note that the definition is recursive (type PMyRecord
is defined using type TMyRecord
, while TMyRecord
is defined using PMyRecord
). It is allowed to define a pointer type to a not-yet-defined type, as long as it will be resolved within the same type
block.
You can allocate and free pointers using the New
/ Dispose
methods, or (more low-level, not type-safe) GetMem
/ FreeMem
methods. You dereference the pointer (to access the stuff pointed by) you append the ^
operator (e.g. MyInteger := MyPointerToInteger^
). To make the inverse operation, which is to get a pointer of an existing variable, you prefix it with @
operator (e.g. MyPointerToInteger := @MyInteger
).
There is also an untyped Pointer
type, similar to void*
in C-like languages. It is completely unsafe, and can be typecasted to any other pointer type.
Remember that a class instance is also in fact a pointer, although it doesn’t require any ^
or @
operators to use it.
A linked list using classes is certainly possible, it would simply be this:
type
TMyClass = class
Value: Integer;
Next: TMyClass;
end;
8.9. Operator overloading
You can override the meaning of many language operators, for example to allow addition and multiplication of your custom types. Like this:
{$mode objfpc}{$H+}{$J-}
uses
StrUtils;
operator* (const S: string; const A: Integer): string;
begin
Result := DupeString(S, A);
end;
begin
WriteLn('bla' * 10);
end.
You can override operators on classes too. Since you usually create new instances of your classes inside the operator function, the caller must remember to free the result.
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
type
TMyClass = class
MyInt: Integer;
end;
operator* (const C1, C2: TMyClass): TMyClass;
begin
Result := TMyClass.Create;
Result.MyInt := C1.MyInt * C2.MyInt;
end;
var
C1, C2: TMyClass;
begin
C1 := TMyClass.Create;
try
C1.MyInt := 12;
C2 := C1 * C1;
try
WriteLn('12 * 12 = ', C2.MyInt);
finally
FreeAndNil(C2);
end;
finally
FreeAndNil(C1);
end;
end.
You can override operators on records too. This is usually easier than overloading them for classes, as the caller doesn’t have to deal then with memory management.
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
type
TMyRecord = record
MyInt: Integer;
end;
operator* (const C1, C2: TMyRecord): TMyRecord;
begin
Result.MyInt := C1.MyInt * C2.MyInt;
end;
var
R1, R2: TMyRecord;
begin
R1.MyInt := 12;
R2 := R1 * R1;
WriteLn('12 * 12 = ', R2.MyInt);
end.
For records, it’s advised to use {$modeswitch advancedrecords}
and override operators as class operator
inside the record. This allows to use generic classes that depend on some operator existence (like TFPGList
, that depends on equality operator being available) with such records. Otherwise the "global" definition of an operator (not inside the record) would not be found (because it’s not available at the code that implements the TFPGList
), and you could not specialize a list like specialize TFPGList<TMyRecord>
.
{$mode objfpc}{$H+}{$J-}
{$modeswitch advancedrecords}
uses
SysUtils, FGL;
type
TMyRecord = record
MyInt: Integer;
class operator+ (const C1, C2: TMyRecord): TMyRecord;
class operator= (const C1, C2: TMyRecord): boolean;
end;
class operator TMyRecord.+ (const C1, C2: TMyRecord): TMyRecord;
begin
Result.MyInt := C1.MyInt + C2.MyInt;
end;
class operator TMyRecord.= (const C1, C2: TMyRecord): boolean;
begin
Result := C1.MyInt = C2.MyInt;
end;
type
TMyRecordList = specialize TFPGList<TMyRecord>;
var
R, ListItem: TMyRecord;
L: TMyRecordList;
begin
L := TMyRecordList.Create;
try
R.MyInt := 1; L.Add(R);
R.MyInt := 10; L.Add(R);
R.MyInt := 100; L.Add(R);
R.MyInt := 0;
for ListItem in L do
R := ListItem + R;
WriteLn('1 + 10 + 100 = ', R.MyInt);
finally
FreeAndNil(L);
end;
end.
9. Advanced classes features
9.1. Private and strict private
The private
visibility specifier means that the field (or method) is not accessible outside of this class. But it allows an exception: all the code defined in the same unit can break this, and access private fields and methods. A C++ programmer would say that in Pascal all classes within a single unit are friends. This is often useful, and doesn’t break your encapsulation, since it’s limited to a unit.
However, if you create larger units, with many classes (that are not tightly integrated with each other), it’s safer to use strict private
. It means that the field (or method) is not accessible outside of this class — period. No exceptions.
In a similar manner, there’s protected
visibility (visible to descendants, or friends in the same unit) and strict protected
(visible to descendants, period).
9.2. More stuff inside classes and nested classes
You can open a section of constants (const
) or types (type
) within a class. This way, you can even define a class within a class. The visibility specifiers work as always, in particular the nested class can be private (not visible to the outside world), which is often useful.
Note that to declare a field after a constant or type you will need to open a var
block.
type
TMyClass = class
private
type
TInternalClass = class
Velocity: Single;
procedure DoSomething;
end;
var
FInternalClass: TInternalClass;
public
const
DefaultVelocity = 100.0;
constructor Create;
destructor Destroy; override;
end;
constructor TMyClass.Create;
begin
inherited;
FInternalClass := TInternalClass.Create;
FInternalClass.Velocity := DefaultVelocity;
FInternalClass.DoSomething;
end;
destructor TMyClass.Destroy;
begin
FreeAndNil(FInternalClass);
inherited;
end;
{ note that method definition is prefixed with
"TMyClass.TInternalClass" below. }
procedure TMyClass.TInternalClass.DoSomething;
begin
end;
9.3. Class methods
These are methods you can call having a class reference (TMyClass
), not necessarily a class instance.
type
TEnemy = class
procedure Kill;
class procedure KillAll;
end;
var
E: TEnemy;
begin
E := TEnemy.Create;
try
E.Kill;
finally FreeAndNil(E) end;
TEnemy.KillAll;
end;
Note that they can be virtual — it makes sense, and is sometimes very useful, when combined with Class references.
The class methods can also be limited by the Visibility specifiers, like private
or protected
. Just like regular methods.
Note that a constructor always acts like a class method when called in a normal fashion (MyInstance := TMyClass.Create(…);
). Although it’s possible to also call a constructor from within the class itself, like a normal method, and then it acts like a normal method. This is a useful feature to "chain" constructors, when one constructor (e.g. overloaded to take an integer parameter) does some job, and then calls another constructor (e.g. parameter-less).
9.4. Class references
Class reference allows you to choose the class at runtime, for example to call a class method or constructor without knowing the exact class at compile-time. It is a type declared as class of TMyClass
.
type
TMyClass = class(TComponent)
end;
TMyClass1 = class(TMyClass)
end;
TMyClass2 = class(TMyClass)
end;
TMyClassRef = class of TMyClass;
var
C: TMyClass;
ClassRef: TMyClassRef;
begin
// Obviously you can do this:
C := TMyClass.Create(nil); FreeAndNil(C);
C := TMyClass1.Create(nil); FreeAndNil(C);
C := TMyClass2.Create(nil); FreeAndNil(C);
// In addition, using class references, you can also do this:
ClassRef := TMyClass;
C := ClassRef.Create(nil); FreeAndNil(C);
ClassRef := TMyClass1;
C := ClassRef.Create(nil); FreeAndNil(C);
ClassRef := TMyClass2;
C := ClassRef.Create(nil); FreeAndNil(C);
end;
Class references can be combined with virtual class methods. This gives a similar effect as using classes with virtual methods — the actual method to be executed is determined at runtime.
type
TMyClass = class(TComponent)
class procedure DoSomething; virtual; abstract;
end;
TMyClass1 = class(TMyClass)
class procedure DoSomething; override;
end;
TMyClass2 = class(TMyClass)
class procedure DoSomething; override;
end;
TMyClassRef = class of TMyClass;
var
C: TMyClass;
ClassRef: TMyClassRef;
begin
ClassRef := TMyClass1;
ClassRef.DoSomething;
ClassRef := TMyClass2;
ClassRef.DoSomething;
{ And this will cause an exception at runtime,
since DoSomething is abstract in TMyClass. }
ClassRef := TMyClass;
ClassRef.DoSomething;
end;
If you have an instance, and you would like to get a reference to its class (not the declared class, but the final descendant class used at its construction), you can use the ClassType
property. The declared type of ClassType
is TClass
, which stands for class of TObject
. Often you can safely typecast it to something more specific, when you know that the instance is something more specific than TObject
.
In particular, you can use the ClassType
reference to call virtual methods, including virtual constructors. This allows you to create a method like Clone
that constructs an instance of the exact run-time class of the current object. You can combine it with Cloning: TPersistent.Assign to have a method that returns a newly-constructed clone of the current instance.
Remember that it only works when the constructor of your class is virtual. For example, it can be used with the standard TComponent
descendants, since they all must override TComponent.Create(AOwner: TComponent)
virtual constructor.
type
TMyClass = class(TComponent)
procedure Assign(Source: TPersistent); override;
function Clone(AOwner: TComponent): TMyClass;
end;
TMyClassRef = class of TMyClass;
function TMyClass.Clone(AOwner: TComponent): TMyClass;
begin
// This would always create an instance of exactly TMyClass:
//Result := TMyClass.Create(AOwner);
// This can potentially create an instance of TMyClass descendant:
Result := TMyClassRef(ClassType).Create(AOwner);
Result.Assign(Self);
end;
9.5. Static class methods
To understand the static class methods, you have to understand how the normal class methods (described in the previous sections) work. Internally, normal class methods receive a class reference of their class (it is passed through a hidden, implicitly added 1st parameter of the method). This class reference can be even accessed explicitly using the Self
keyword inside the class method. Usually, it’s a good thing: this class reference allows you to call virtual class methods (through the virtual method table of the class).
While this is nice, it makes the normal class methods incompatible when assigning to a global procedure pointer. That is, this will not compile:
{$mode objfpc}{$H+}{$J-}
type
TMyCallback = procedure (A: Integer);
TMyClass = class
class procedure Foo(A: Integer);
end;
class procedure TMyClass.Foo(A: Integer);
begin
end;
var
Callback: TMyCallback;
begin
// Error: TMyClass.Foo not compatible with TMyCallback
Callback := @TMyClass(nil).Foo;
end.
Зауваження
|
In the Delphi mode you would be able to write In any case, assigning the |
The above example fails to compile, because the Callback
is incompatible with the class method Foo
. And it’s incompatible because internally the class method has that special hidden implicit parameter to pass a class reference.
One way to fix the above example is to change the definition of TMyCallback
. It will work if it is a method callback, declared as TMyCallback = procedure (A: Integer) of object;
. But sometimes, it’s not desirable.
Here comes the static
class method. It is, in essence, just a global procedure / function, but its namespace is limited inside the class. It does not have any implicit class reference (and so, it cannot be virtual and it cannot call virtual class methods). On the upside, it is compatible with normal (non-object) callbacks. So this will work:
{$mode objfpc}{$H+}{$J-}
type
TMyCallback = procedure (A: Integer);
TMyClass = class
class procedure Foo(A: Integer); static;
end;
class procedure TMyClass.Foo(A: Integer);
begin
end;
var
Callback: TMyCallback;
begin
Callback := @TMyClass.Foo;
end.
9.6. Class properties and variables
A class property is a property that can be accessed through a class reference (it does not need a class instance).
It is quite straightforward analogy of a regular property (see Properties). For a class property, you define a getter and / or a setter. They may refer to a class variable or a static class method.
A class variable is, you guessed it, like a regular field but you don’t need a class instance to access it. In effect, it’s just like a global variable, but with the namespace limited to the containing class. It can be declared within the class var
section of the class. Alternatively
it can be declared by following the normal field definition with the keyword static
.
And a static class method is just like a global procedure / function, but with the namespace limited to the containing class. More about static class methods in the section above, see Static class methods.
{$mode objfpc}{$H+}{$J-}
type
TMyClass = class
strict private
// Альтернатива:
// FMyProperty: Integer; static;
class var
FMyProperty: Integer;
class procedure SetMyProperty(const Value: Integer); static;
public
class property MyProperty: Integer
read FMyProperty write SetMyProperty;
end;
class procedure TMyClass.SetMyProperty(const Value: Integer);
begin
Writeln('MyProperty змінено!');
FMyProperty := Value;
end;
begin
TMyClass.MyProperty := 123;
Writeln('TMyClass.MyProperty тепер ', TMyClass.MyProperty);
end.
9.7. Class helpers
The method is just a procedure or function inside a class. From the outside of the class, you call it with a special syntax MyInstance.MyMethod(…)
. After a while you grow accustomed to thinking that if I want to make action Action on instance X, I write `X.Action(…)`.
But sometimes, you need to implement something that conceptually is an action on class TMyClass without modifying the TMyClass source code. Sometimes it’s because it’s not your source code, and you don’t want to change it. Sometimes it’s because of the dependencies — adding a method like Render
to a class like TMy3DObject
seems like a straightforward idea, but maybe the base implementation of class TMy3DObject
should be kept independent from the rendering code? It would be better to "enhance" an existing class, to add functionality to it without changing its source code.
Simple way to do it is then to create a global procedure that takes an instance of TMy3DObject
as its 1st parameter.
procedure Render(const Obj1: TMy3DObject; const Color: TColor);
var
I: Integer;
begin
for I := 0 to Obj1.ShapesCount - 1 do
RenderMesh(Obj1.Shape[I].Mesh, Color);
end;
This works perfectly, but the downside is that calling it looks a little ugly. While usually you call actions like X.Action(…)
, in this case you have to call them like Render(X, …)
. It would be cool to be able to just write X.Render(…)
, even when Render
is not implemented in the same unit as TMy3DObject
.
And this is where you use class helpers. They are just a way to implement procedures / functions that operate on given class, and that are called like methods, but are not in fact normal methods — they were added outside of the TMy3DObject
definition.
type
TMy3DObjectHelper = class helper for TMy3DObject
procedure Render(const Color: TColor);
end;
procedure TMy3DObjectHelper.Render(const Color: TColor);
var
I: Integer;
begin
{ note that we access ShapesCount, Shape without any qualifiers here }
for I := 0 to ShapesCount - 1 do
RenderMesh(Shape[I].Mesh, Color);
end;
Зауваження
|
The more general concept is "type helper". Using them you can add methods even to primitive types, like integers or enums. You can also add "record helpers" to (you guessed it…) records. See http://lists.freepascal.org/fpc-announce/2013-February/000587.html . |
9.8. Virtual constructors, destructors
Destructor name is always Destroy
, it is virtual (since you can call it without knowing the exact class at compile-time) and parameter-less.
Constructor name is by convention Create
.
You can change this name, although be careful with this — if you define CreateMy
, always redefine also the name Create
, otherwise the user can still access the constructor Create
of the ancestor, bypassing your CreateMy
constructor.
In the base TObject
it is not virtual, and when creating descendants you’re free to change the parameters. The new constructor will hide the constructor in ancestor (note: don’t put here overload
, unless you want to break it).
In the TComponent
descendants, you should override its constructor Create(AOwner: TComponent);
. For streaming functionality, to create a class without knowing its type at compile time, having virtual constructors is very useful (see Class references above).
9.9. An exception in constructor
What happens if an exception happens during a constructor? The line
X := TMyClass.Create;
does not execute to the end in this case, X
cannot be assigned, so who will cleanup after a partially-constructed class?
The solution of Object Pascal is that, in case an exception occurs within a constructor, then the destructor is called. This is a reason why your destructor must be robust, which means it should work in any circumstances, even on a half-created class instance. Usually this is easy if you release everything safely, like by FreeAndNil
.
We also have to depend in such cases that the memory of the class is guaranteed to be zeroed right before the constructor code is executed. So we know that at the beginning, all class references are nil
, all integers are 0
and so on.
So below works without any memory leaks:
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
type
TGun = class
end;
TPlayer = class
Gun1, Gun2: TGun;
constructor Create;
destructor Destroy; override;
end;
constructor TPlayer.Create;
begin
inherited;
Gun1 := TGun.Create;
raise Exception.Create('Підняття винятку з конструктора!');
Gun2 := TGun.Create;
end;
destructor TPlayer.Destroy;
begin
{ у випадку, якщо в конструкторі стався збій, ми можемо
мати Gun1 <> nil і Gun2 = nil зараз. Змиріться з цим.
...Насправді, у цьому випадку FreeAndNil справляється
з цим без будь-яких додаткових зусиль з нашого боку,
тому що FreeAndNil перевіряє, чи є екземпляр нульовим,
перш ніж викликати його деструктор }
FreeAndNil(Gun1);
FreeAndNil(Gun2);
inherited;
end;
begin
try
TPlayer.Create;
except
on E: Exception do
WriteLn('Спіймано ' + E.ClassName + ': ' + E.Message);
end;
end.
10. Interfaces
10.1. Bare (CORBA) interfaces
An interface declares an API, much like a class, but it does not define the implementation. A class can implement many interfaces, but it can only have one ancestor class.
You can cast a class to any interface it supports, and then call the methods through that interface. This allows to treat in a uniform fashion the classes that don’t descend from each other, but still share some common functionality. Useful when a simple class inheritance is not enough.
The CORBA interfaces in Object Pascal work pretty much like interfaces in Java (https://docs.oracle.com/javase/tutorial/java/concepts/interface.html) or C# (https://msdn.microsoft.com/en-us/library/ms173156.aspx).
{$mode objfpc}{$H+}{$J-}
{$interfaces corba}
uses
SysUtils, Classes;
type
IMyInterface = interface
['{79352612-668B-4E8C-910A-26975E103CAC}']
procedure Shoot;
end;
TMyClass1 = class(IMyInterface)
procedure Shoot;
end;
TMyClass2 = class(IMyInterface)
procedure Shoot;
end;
TMyClass3 = class
procedure Shoot;
end;
procedure TMyClass1.Shoot;
begin
WriteLn('TMyClass1.Shoot');
end;
procedure TMyClass2.Shoot;
begin
WriteLn('TMyClass2.Shoot');
end;
procedure TMyClass3.Shoot;
begin
WriteLn('TMyClass3.Shoot');
end;
procedure UseThroughInterface(I: IMyInterface);
begin
Write('Shooting... ');
I.Shoot;
end;
var
C1: TMyClass1;
C2: TMyClass2;
C3: TMyClass3;
begin
C1 := TMyClass1.Create;
C2 := TMyClass2.Create;
C3 := TMyClass3.Create;
try
if C1 is IMyInterface then
UseThroughInterface(C1 as IMyInterface);
if C2 is IMyInterface then
UseThroughInterface(C2 as IMyInterface);
// The "C3 is IMyInterface" below is false,
// so "UseThroughInterface(C3 as IMyInterface)" will not execute.
if C3 is IMyInterface then
UseThroughInterface(C3 as IMyInterface);
finally
FreeAndNil(C1);
FreeAndNil(C2);
FreeAndNil(C3);
end;
end.
10.2. CORBA and COM types of interfaces
- Why are the interfaces (presented above) called "CORBA"?
-
The name CORBA is unfortunate. A better name would be bare interfaces. These interfaces are a "pure language feature". Use them when you want to cast various classes as the same interface, because they share a common API.
While these types of interfaces can be used together with the CORBA (Common Object Request Broker Architecture) technology (see https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture), they are not tied to this technology in any way.
- Is the
{$interfaces corba}
declaration needed? -
Yes, because by default you create COM interfaces. This can be stated explicitly by saying
{$interfaces com}
, but usually it’s not needed since it’s the default state.And I don’t advise using COM interfaces, especially if you’re looking for something equivalent to interfaces from other programming languages. The CORBA interfaces in Pascal are exactly what you expect if you’re looking for something equivalent to the interfaces in C# and Java. While the COM interfaces bring additional features that you possibly don’t want.
Note that the
{$interfaces xxx}
declaration only affects the interfaces that do not have any explicit ancestor (just the keywordinterface
, notinterface(ISomeAncestor)
). When an interface has an ancestor, it has the same type as the ancestor, regardless of the{$interfaces xxx}
declaration. - What are COM interfaces?
-
The COM interface is synonymous with an interface descending from a special
IUnknown
interface. Descending fromIUnknown
:-
Requires that your classes define the
_AddRef
and_ReleaseRef
methods. Proper implementation of these methods can manage the lifetime of your objects using the reference-counting. -
Adds the
QueryInterface
method. -
Allows to interact with the COM (Component Object Model) technology.
-
- Why do you advise to not use the COM interfaces?
-
Because COM interfaces "entangle" two features that should be unrelated (orthogonal) in my view: multiple inheritance and reference counting. Other programming languages rightly use separate concepts for these two features.
To be clear: reference-counting, that provides an automatic memory management (in simple situations, i.e. without cycles), is a very useful concept. But entangling this feature with interfaces (instead of making them orthogonal features) is unclean in my eyes. It definitely doesn’t match my use cases.
-
Sometimes I want to cast my (otherwise unrelated) classes to a common interface.
-
Sometimes I want to manage memory using the reference counting approach.
-
Maybe some day I will want to interact with the COM technology.
But these are all separate, unrelated needs. Entangling them in a single language feature is counter-useful in my experience. It does cause actual problems:
-
If I want the feature of casting classes to a common interface API, but I don’t want the reference-counting mechanism (I want to manually free objects), then the COM interfaces are problematic. Even when reference-counting is disabled by a special
_AddRef
and_ReleaseRef
implementation, you still need to be careful to never have a temporary interface reference hanging, after you have freed the class instance. More details about it in the next section. -
If I want the feature of reference counting, but I have no need for an interface hierarchy to represent something different than the class hierarchy, then I have to duplicate my classes API in interfaces. Thus creating a single interface for each class. This is counter-productive. I would much rather have smart pointers as a separate language feature, not entangled with interfaces (and luckily, it’s coming:).
That is why I advise to use CORBA style interfaces, and the
{$interfaces corba}
directive, in all modern code dealing with interfaces.Only if you need both "reference counting" and "multiple inheritance" at the same time, then use COM interfaces. Also, Delphi has only COM interfaces for now, so you need to use COM interfaces if your code must be compatible with Delphi.
-
- Can we have reference-counting with CORBA interfaces?
-
Yeah. Just add
_AddRef
/_ReleaseRef
methods. There’s no need to descend from theIUnknown
interface. Although in most cases, if you want reference-counting with your interfaces, you may as well just use COM interfaces.
10.3. Interfaces GUIDs
GUIDs are the seemingly random characters ['{ABCD1234-…}']
that you see placed at every interface definition. Yes, they are just random. Unfortunately, they are necessary.
The GUIDs have no meaning if you don’t plan on integrating with communication technologies like COM nor CORBA. But they are necessary, for implementation reasons. Don’t be fooled by the compiler, that unfortunately allows you to declare interfaces without GUIDs.
Without the (unique) GUIDs, your interfaces will be treated equal by the is
operator. In effect, it will return true
if your class supports any of your interfaces. The magic function Supports(ObjectInstance, IMyInterface)
behaves slightly better here, as it refuses to be compiled for interfaces without a GUID. This is true for both CORBA and COM interfaces, as of FPC 3.0.0.
So, to be on the safe side, you should always declare a GUID for your interface. You can use Lazarus GUID generator (Ctrl + Shift + G
shortcut in the editor). Or you can use an online service like https://www.guidgenerator.com/ .
Or you can write your own tool for this, using the CreateGUID
and GUIDToString
functions in RTL. See the example below:
{$mode objfpc}{$H+}{$J-}
uses
SysUtils;
var
MyGuid: TGUID;
begin
Randomize;
CreateGUID(MyGuid);
WriteLn('[''' + GUIDToString(MyGuid) + ''']');
end.
10.4. Reference-counted (COM) interfaces
The COM interfaces bring two additional features:
-
integration with COM (a technology from Windows, also available on Unix through XPCOM, used by Mozilla),
-
reference counting (which gives you automatic destruction when all the interface references go out of scope).
When using COM interfaces, you need to be aware of their automatic destruction mechanism and relation to COM technology.
In practice, this means that:
-
Your class needs to implement a magic
_AddRef
,_Release
, andQueryInterface
methods. Or descend from something that already implements them. A particular implementation of these methods may actually enable or disable the reference-counting feature of COM interfaces (although disabling it is somewhat dangerous — see the next point).-
The standard class
TInterfacedObject
implements these methods to enable the reference-counting. -
The standard class
TComponent
implements these methods to disable the reference-counting. In the Castle Game Engine we give you additional useful ancestorsTNonRefCountedInterfacedObject
andTNonRefCountedInterfacedPersistent
for this purpose, see https://github.com/castle-engine/castle-engine/blob/0519585abc13e8386cdae5f7dfef6f9659dc9b57/src/base/castleinterfaces.pas .
-
-
You need to be careful of freeing the class, when it may be referenced by some interface variables. Because the interface is released using a virtual method (because it may be reference-counted, even if you hack the _AddRef method to not be reference-counted…), you cannot free the underlying object instance as long as some interface variable may point to it. See "7.7 Reference counting" in the FPC manual (http://freepascal.org/docs-html/ref/refse47.html).
The safest approach to using COM interfaces is to
-
accept the fact that they are reference-counted,
-
derive the appropriate classes from
TInterfacedObject
, -
and avoid using the class instance, instead accessing the instance always through the interface, letting reference-counting manage the deallocation.
This is an example of such interface use:
{$mode objfpc}{$H+}{$J-}
{$interfaces com}
uses
SysUtils, Classes;
type
IMyInterface = interface
['{3075FFCD-8EFB-4E98-B157-261448B8D92E}']
procedure Shoot;
end;
TMyClass1 = class(TInterfacedObject, IMyInterface)
procedure Shoot;
end;
TMyClass2 = class(TInterfacedObject, IMyInterface)
procedure Shoot;
end;
TMyClass3 = class(TInterfacedObject)
procedure Shoot;
end;
procedure TMyClass1.Shoot;
begin
WriteLn('TMyClass1.Shoot');
end;
procedure TMyClass2.Shoot;
begin
WriteLn('TMyClass2.Shoot');
end;
procedure TMyClass3.Shoot;
begin
WriteLn('TMyClass3.Shoot');
end;
procedure UseThroughInterface(I: IMyInterface);
begin
Write('Shooting... ');
I.Shoot;
end;
var
C1: IMyInterface; // COM takes care of destruction
C2: IMyInterface; // COM takes care of destruction
C3: TMyClass3; // YOU have to take care of destruction
begin
C1 := TMyClass1.Create as IMyInterface;
C2 := TMyClass2.Create as IMyInterface;
C3 := TMyClass3.Create;
try
UseThroughInterface(C1); // no need to use "as" operator
UseThroughInterface(C2);
if C3 is IMyInterface then
UseThroughInterface(C3 as IMyInterface); // this will not execute
finally
{ C1 and C2 variables go out of scope and will be auto-destroyed now.
In contrast, C3 is a class instance, not managed by an interface,
and it has to be destroyed manually. }
FreeAndNil(C3);
end;
end.
10.5. Using COM interfaces with reference-counting disabled
As mentioned in the previous section, your class can descend from TComponent
(or a similar class like TNonRefCountedInterfacedObject
and TNonRefCountedInterfacedPersistent
) which disables reference-counting for COM interfaces. This allows you to use COM interfaces, and still free the class instance manually.
You need to be careful in this case to not free the class instance when some interface variable may refer to it. Remember that every typecast Cx as IMyInterface
also creates a temporary interface variable, which may be present even until the end of the current procedure. For this reason, the example below uses a UseInterfaces
procedure, and it frees the class instances outside of this procedure (when we can be sure that temporary interface variables are out of scope).
To avoid this mess, it’s usually better to use CORBA interfaces, if you don’t want reference-counting with your interfaces.
{$mode objfpc}{$H+}{$J-}
{$interfaces com}
uses
SysUtils, Classes;
type
IMyInterface = interface
['{3075FFCD-8EFB-4E98-B157-261448B8D92E}']
procedure Shoot;
end;
TMyClass1 = class(TComponent, IMyInterface)
procedure Shoot;
end;
TMyClass2 = class(TComponent, IMyInterface)
procedure Shoot;
end;
TMyClass3 = class(TComponent)
procedure Shoot;
end;
procedure TMyClass1.Shoot;
begin
WriteLn('TMyClass1.Shoot');
end;
procedure TMyClass2.Shoot;
begin
WriteLn('TMyClass2.Shoot');
end;
procedure TMyClass3.Shoot;
begin
WriteLn('TMyClass3.Shoot');
end;
procedure UseThroughInterface(I: IMyInterface);
begin
Write('Shooting... ');
I.Shoot;
end;
var
C1: TMyClass1;
C2: TMyClass2;
C3: TMyClass3;
procedure UseInterfaces;
begin
if C1 is IMyInterface then
//if Supports(C1, IMyInterface) then // equivalent to "is" check above
UseThroughInterface(C1 as IMyInterface);
if C2 is IMyInterface then
UseThroughInterface(C2 as IMyInterface);
if C3 is IMyInterface then
UseThroughInterface(C3 as IMyInterface);
end;
begin
C1 := TMyClass1.Create(nil);
C2 := TMyClass2.Create(nil);
C3 := TMyClass3.Create(nil);
try
UseInterfaces;
finally
FreeAndNil(C1);
FreeAndNil(C2);
FreeAndNil(C3);
end;
end.
10.6. Typecasting interfaces
This section applies to both CORBA and COM interfaces (however, it has some explicit exceptions for CORBA).
-
Casting to an interface type using the
as
operator makes a check at run-time. Consider this code:UseThroughInterface(Cx as IMyInterface);
It works for all
C1
,C2
,C3
instances in the examples in previous sections. If executed, it would make a run-time error in case ofC3
, that does not implementIMyInterface
.Using
as
operator works consistently regardless ifCx
is declared as a class instance (likeTMyClass2
) or interface (likeIMyInterface2
).However, it is not allowed for CORBA interfaces.
-
You can instead cast the instance as an interface implicitly:
UseThroughInterface(Cx);
In this case, the typecast must be valid at compile-time. So this will compile for
C1
andC2
(that are declared as classes that implementIMyInterface
). But it will not compile forC3
.In essence, this typecast looks and works just like for regular classes. Wherever an instance of a class
TMyClass
is required, you can always use there a variable that is declared with a class ofTMyClass
, orTMyClass
descendant. The same rule applies to interfaces. No need for any explicit typecast in such situations. -
You can also typecast using
IMyInterface(Cx)
. Like this:UseThroughInterface(IMyInterface(Cx));
Usually, such typecasting syntax indicates an unsafe, unchecked typecast. Bad things will happen if you cast to an incorrect interface. And that’s true, if you cast a class to a class, or an interface to an interface, using this syntax.
There is a small exception here: if
Cx
is declared as a class (likeTMyClass2
), then this is a typecast that must be valid at compile-time. So casting a class to an interface this way is a safe, fast (checked at compile-time) typecast.
To test it all, play around with this example code:
{$mode objfpc}{$H+}{$J-}
// {$interfaces corba} // зауважте, що приведення типу "as" для CORBA не компілюється
uses Classes;
type
IMyInterface = interface
['{7FC754BC-9CA7-4399-B947-D37DD30BA90A}']
procedure One;
end;
IMyInterface2 = interface(IMyInterface)
['{A72B7008-3F90-45C1-8F4C-E77C4302AA3E}']
procedure Two;
end;
IMyInterface3 = interface(IMyInterface2)
['{924BFB98-B049-4945-AF17-1DB08DB1C0C5}']
procedure Three;
end;
TMyClass = class(TComponent, IMyInterface)
procedure One;
end;
TMyClass2 = class(TMyClass, IMyInterface, IMyInterface2)
procedure One;
procedure Two;
end;
procedure TMyClass.One;
begin
Writeln('TMyClass.One');
end;
procedure TMyClass2.One;
begin
Writeln('TMyClass2.One');
end;
procedure TMyClass2.Two;
begin
Writeln('TMyClass2.Two');
end;
procedure UseInterface2(const I: IMyInterface2);
begin
I.One;
I.Two;
end;
procedure UseInterface3(const I: IMyInterface3);
begin
I.One;
I.Two;
I.Three;
end;
var
My: IMyInterface;
MyClass: TMyClass;
begin
My := TMyClass2.Create(nil);
MyClass := TMyClass2.Create(nil);
// Це не компілюється, оскільки під час компіляції невідомо, чи My є IMyInterface2.
// UseInterface2(My);
// UseInterface2(MyClass);
// Це компілюється та працює нормально.
UseInterface2(IMyInterface2(My));
// Це не компілюється. Приведення InterfaceType(ClassType) перевіряється під час компіляції.
// UseInterface2(IMyInterface2(MyClass));
// Це компілюється та працює нормально.
UseInterface2(My as IMyInterface2);
// Це компілюється та працює нормально.
UseInterface2(MyClass as IMyInterface2);
// Це компілюється, але зазнає збою під час виконання, з потворним «Access Violation».
// UseInterface3(IMyInterface3(My));
// Це не компілюється. Приведення InterfaceType(ClassType) перевіряється під час компіляції.
// UseInterface3(IMyInterface3(MyClass));
// Це компілюється, але зазнає збою під час виконання, з гарним «EInvalidCast: неправильне приведення типу».
// UseInterface3(My as IMyInterface3);
// Це компілюється, але зазнає збою під час виконання, з гарним «EInvalidCast: неправильне приведення типу».
// UseInterface3(MyClass as IMyInterface3);
Writeln('Завершено');
end.
11. About this document
Copyright Michalis Kamburelis.
The source code of this document is in AsciiDoc on https://github.com/michaliskambi/modern-pascal-introduction. Suggestions for corrections and additions, and patches and pull requests, are always very welcome:) You can reach me through GitHub or email [email protected]. My homepage is https://michalis.xyz/. This document is linked under the Documentation section of the Castle Game Engine website https://castle-engine.io/.
You can redistribute and even modify this document freely, under the same licenses as Wikipedia https://en.wikipedia.org/wiki/Wikipedia:Copyrights :
-
Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA)
-
or the GNU Free Documentation License (GFDL) (unversioned, with no invariant sections, front-cover texts, or back-cover texts) .
Thank you for reading!