You are on page 1of 86

Rozdział 2.

Język Object Pascal


Pozostawmy na chwilę wizualne aspekty Delphi i przyjrzyjmy się bliżej stanowiącemu podstawę Delphi
językowi Object Pascal. Ponieważ niniejsza książka przeznaczona jest raczej dla zaawansowanych czytelników,
z jednej strony ograniczyliśmy się jedynie do zestawienia najważniejszych cech tego języka, z drugiej natomiast
wprowadziliśmy pewne porównania jego elementów z innymi językami wysokiego poziomu — jak C++, Visual
Basic i Java — przy założeniu, że Czytelnik posiada o nich podstawową wiedzę. Obecna wersja Object Pascala
różni się znacznie od tej z Delphi 1 czy Delphi 2, zalecamy więc uważne przestudiowanie treści niniejszego
rozdziału — nie da się bowiem w pełni wykorzystać Delphi bez dogłębnej znajomości języka, na bazie którego
zostało zbudowane.

Notatka

Ilekroć wspominamy w niniejszym rozdziale o języku C, mamy na myśli elementy wspólne dla C i C++. Gdy
mowa jest o elementach specyficznych dla C++ zaznaczamy to wyraźnie.

Komentarze
Komentarz jest najprostszym elementem języka programowania — stanowi swobodny tekst mający znaczenie
jedynie dla czytelności programu; przez kompilator jest on całkowicie ignorowany. Object Pascal dopuszcza trzy
rodzaje ograniczników komentarza:
• nawiasy klamrowe { .. }, znane z Turbo Pascala,
• ograniczniki typu „nawias-gwiazdka” (* ... *), również występujące w Turbo Pascalu,
• podwójny ukośnik // (ang. double slash), zapożyczony z języka C++.
Oto przykłady poprawnych komentarzy w Object Pascalu:
{
to jest komentarz języka Object Pascal,
podstawy Delphi 6
}

(* to również jest komentarz,


tylko z innymi ogranicznikami
*)

//Ten komentarz musi zmieścić się w jednej linii

// Ten komentarz został, dla odmiany,


// podzielony pomiędzy kilka linii,
// z których każda traktowana jest przez kompilator jako
// niezależny komentarz,
// choć przecież dla użytkownika nie ma to żadnego znaczenia.

Za koniec komentarza rozpoczynającego się od podwójnego ukośnika przyjmuje się koniec linii.

Ostrzeżenie

Komentarze tej samej postaci nie mogą być zagnieżdżane, gdyż jest to sprzeczne z regułami składniowymi
języka. Na przykład w poniższym przykładzie

{ { próba zagnieżdżenia komentarza } }

początkiem komentarza jest oczywiście pierwszy z nawiasów otwierających, lecz końcem — pierwszy, nie
drugi nawias zamykający. Nie ma jednak żadnych przeszkód, aby zagnieżdżać komentarze różnych typów, na
przykład:

(* Poniższy komentarz {nadmiar} wskazuje błędną


instrukcję *)

{ poniższe komentarze w formie (* *) ograniczają


tekst do usunięcia

W komentarzu rozpoczynającym się od podwójnego ukośnika znaki { } (* *) mogą oczywiście


występować bez żadnych ograniczeń, gdyż końcem komentarza nie jest żaden wyróżniony znak, lecz koniec
linii.
Nowości w zakresie procedur i funkcji
Ponieważ procedury i funkcje stanowią najbardziej uniwersalny element wszystkich języków algorytmicznych,
zrezygnujemy w tym miejscu z ich wyczerpującego opisu (który Czytelnik znaleźć może m.in. w dokumentacji
Delphi), koncentrując się na tych ich cechach, które odróżniają Object Pascal od Turbo Pascala oraz tych, które
pojawiły się w późniejszych wersjach Delphi (począwszy od Delphi 3).

Puste nagłówki wywołań


Wyjątkowo, opisywana w tym miejscu konstrukcja obecna jest w Object Pascalu już od Delphi 2, mimo to jest
jednak na tyle mało popularna, iż zasługuje przynajmniej na krótką wzmiankę. Otóż, wywołując funkcję lub
procedurę nie posiadającą parametrów, możemy użyć pary nawiasów na wzór języka C lub Java, na przykład:
Form1.Show();
...
R := CurrentDispersion();

co oczywiście jest równoważne


Form1.Show;
...
R := CurrentDispersion;

Nie jest to może żadna rewelacja językowa, lecz z pewnością drobne ułatwienie życia programistom, którzy
oprócz Object Pascala używają również języków wcześniej wymienionych, w których użycie wspomnianych
nawiasów jest obowiązkowe.

Przeciążanie
W Delphi 4 wprowadzono mechanizm przeciążania (overloading) procedur i funkcji, umożliwiający
zdefiniowanie całej rodziny procedur (funkcji), posiadających tę samą nazwę, lecz różniących się postacią listy
parametrów wywołania, na przykład:
function Divide(X, Y: Real): Real; overload;
begin
Result := X/Y;
end;

function Divide(X, Y: Integer): Integer; overload;


begin
Result := X div Y;
end;

Informacją o przeciążeniu procedury funkcji jest klauzula overload (jak w powyższym przykładzie).
Kompilator, analizując postać wywołania przeciążonej procedury (funkcji), automatycznie wybierze właściwy
jej egzemplarz (zwany często jej aspektem), aby jednak zadanie to było wykonalne, poszczególne aspekty
faktycznie muszą różnić się od siebie. Nie jest tak chociażby w poniższym przykładzie:
procedure Cap(S: string); overload;
...

procedure Cap(var Str: string); overload;


...

Wywołanie
Cap(S);

pasuje bowiem do obydwu aspektów — kompilator uzna drugi z nich za próbę redefinicji pierwszego i
zasygnalizuje błąd.
Przeciążanie procedur i funkcji jest chyba najbardziej oczekiwaną nowością Object Pascala od czasu Delphi 1 i,
jakkolwiek bardzo pożądane i użyteczne, stanowi jedno z odstępstw od rygorystycznej kontroli typów danych
(tak charakterystycznej dla początków Pascala). Mamy wszak do czynienia z różnymi procedurami (funkcjami),
kryjącymi się pod tą samą nazwą — przypomina to identycznie nazwane procedury (funkcje) rezydujące w
różnych modułach. Należy więc korzystać z przeciążania rozsądnie, a w żadnym wypadku nie należy go
nadużywać.
Zagadnieniem podobnym do przeciążania procedur i funkcji jest przeciążanie metod, którym zajmiemy się w
dalszej części niniejszego rozdziału.

Domyślne parametry
To kolejna nowość wprowadzona w Delphi 4, umożliwiająca uproszczenie wywołań procedur i funkcji poprzez
pominięcie jednego lub więcej końcowych parametrów listy. W treści procedury (funkcji) parametry te posiadać
będą wartości domyślne, ustalone w jej definicji. Oto przykład deklaracji procedury z jednym parametrem
domyślnym:
procedure HasDefVal( S: String; I: Integer = 0);

Ponieważ drugiemu z parametrów definicja przyporządkowuje domyślną wartość 0, więc wywołanie


HasDefVal('Hello');

równoważne jest wywołaniu


HasDefVal('Hello', 0);

Parametry z wartościami domyślnymi muszą wystąpić w końcowej części listy — nie mogą być „przemieszane”
z pozostałymi parametrami, tak więc poniższa deklaracja
Procedure NoProperDefs( X: Integer = 1; Y : Real);

jest błędna, poprawna jest natomiast deklaracja


Procedure ProperDefs( X: Integer = 1; Y : Real = 0.0);

Parametry z deklarowaną wartością domyślną nie mogą być przekazywane przez referencję (var), lecz jedynie
przez wartość lub przez stałą (const); wiąże się to z oczywistym wymogiem, aby parametr aktualny wywołania
był wyrażeniem o dającej się ustalić wartości. Wymóg ten narzuca również ograniczenie na typ parametru,
któremu przypisuje się wartość domyślną — nie mogą w tej roli wystąpić rekordy, zmienne wariantowe, pliki,
tablice i obiekty. Ograniczeniu podlega także sama wartość domyślna przypisywana parametrowi — może ona
być wyrażeniem typu porządkowego, wskaźnikowego lub zbiorowego.
Pewnego komentarza wymaga użycie parametrów domyślnych w połączeniu z przeciążaniem procedur i funkcji.
Należy uważać, aby nie uniemożliwić kompilatorowi jednoznacznego zidentyfikowania właściwego aspektu
procedury (funkcji) przeciążanej — rozróżnienie takie nie jest możliwe chociażby w poniższym przykładzie:
procedure Confused(I: Integer); overload;
...

procedure Confused(I: Integer; J: Integer = 0); overload;


...

var
X: Integer;
begin
...
Confused(X); // Ta instrukcja spowoduje błąd kompilacji

Mechanizm parametrów domyślnych oddaje nieocenione usługi przy unowocześnianiu istniejącego


oprogramowania. Załóżmy na przykład, iż następującą procedurę
Procedure MyMessage( Msg:String);

wyświetlającą komunikat w środkowej linii okna, chcielibyśmy wzbogacić w możliwość jawnego wskazania
linii (poprzez drugi parametr). Nawet jeżeli przyjmiemy, iż podanie 0 jako numeru linii oznaczać będzie
tradycyjne wyświetlanie w środkowej linii, to i tak nie zwolni nas to z modyfikacji wszystkich wywołań
procedury MyMessage() w kodzie programu. Ściślej — byłoby tak, gdyby nie mechanizm domyślnych
parametrów, bowiem począwszy od Delphi 4 możemy dopuścić wywołanie procedury MyMessage() z jednym
parametrem, przyjmując w takiej sytuacji, iż opuszczony, domyślny drugi parametr ma wartość 0:
Procedure MyMessage ( Msg : String; Line: byte = 0 );

Wówczas wywołanie
MyMessage('Hello', 1)

spowoduje wyświetlenie komunikatu w pierwszej linii okna, natomiast efektem wywołania


MyMessage('Hello')
będzie wyświetlenie komunikatu w linii środkowej. I nie trzeba przy tym niczego zmieniać, poza oczywiście
samą procedurą MyMessage().

Zmienne
W języku C i w Javie możliwe jest deklarowanie zmiennych dopiero w momencie, gdy faktycznie okazują się
potrzebne, na przykład:
void foo(void)
{
int x = 1;
x++;
int y = 2;
float f;
// ... i tak dalej
}

Natomiast w języku Pascal wszystkie deklaracje zmiennych muszą być zlokalizowane przed blokiem kodu danej
procedury, funkcji lub programu głównego:
Procedure Foo;
var
x, y : integer;
f : double;
begin
x := 1;
Inc(x);
y := 2;
(*
itd.
*)
end;

Może się to wydawać nieco krępujące, lecz w rzeczywistości prowadzi do programu bardziej czytelnego i mniej
podatnego na błędy — co jest charakterystyczne dla Pascala, stawiającego raczej na bezpieczeństwo aplikacji niż
hołdowanie określonym konwencjom.

Notatka

Object Pascal i Visual Basic, w przeciwieństwie do Javy i C, niewrażliwe są na wielkość liter w nazwach
elementów składniowych. Wrażliwość taka zwiększa co prawda elastyczność języka, lecz niepomiernie
zwiększa również prawdopodobieństwo popełnienia trudnych do wykrycia błędów. W Pascalu raczej trudno
odczuć brak takiej elastyczności, za to programista ma dość dużą swobodę stylistyczną w pisowni nazw,
na przykład nazwa

prostesortowaniemetodąprzesiewaniazograniczeniami

staje się bardziej czytelna, gdy jest zapisana w tzw. notacji „wielbłądziej”

ProsteSortowanieMetodąPrzesiewaniaZOgraniczeniami

Deklaracje zmiennych tego samego typu mogą być łączone, na przykład deklarację
var
Zmienna1 : integer;
Zmienna2 : integer;

można skrócić do postaci


var
Zmienna1, Zmienna2 : integer;

Jak widać, po liście zmiennych następuje dwukropek i nazwa typu.


Nadawanie zmiennym wartości odbywa się w innym miejscu niż ich deklarowanie, mianowicie w treści
programu. Nowością, która pojawiła się w Delphi 2 jest możliwość inicjowania zmiennych już podczas ich
deklaracji, na przykład:
var
i : integer = 10;
P : Pointer = NIL;
s : String = 'Napis domyślny';
d : Double = 3.1415926 ;

Jest to jednak dopuszczalne wyłącznie dla zmiennych globalnych; kompilator nie zezwoli na inicjowanie w ten
sposób zmiennych lokalnych w procedurach i funkcjach.

Wskazówka

Kompilator dokonuje również automatycznej inicjalizacji wszystkich zmiennych globalnych bez deklarowanej
wartości początkowej, zerując zajmowaną przez nie pamięć; tak więc wszystkie zmienne całkowitoliczbowe
otrzymują wartość 0, zmiennoprzecinkowe — wartość 0.0, łańcuchy stają się łańcuchami pustymi, wskaźniki
otrzymują wartość NIL itp.

Stałe
Stałe (ang. constants) są synonimami konkretnych wartości występujących w programie. Deklaracja stałych
poprzedzona jest słowem kluczowym const i składa się z jednego lub więcej przypisań wartości nazwom
synonimicznym, na przykład:
const
DniWTygodniu = 7;
Stanowisk = 7;
TaboretyNaStanowisku = 4;
TaboretyOgolem = TaboretyNaStanowisku * Stanowisk;
Komunikat = 'Przerwa śniadaniowa';

Zasadniczą różnicą między Object Pascalem a językiem C — w zakresie deklaracji stałych — jest to, że Pascal
nie wymaga deklarowania typów stałych; kompilator sam ustala typ stałej na podstawie przypisywanej wartości.
Ponadto, stałe synonimiczne typów skalarnych nie zajmują dodatkowego miejsca w pamięci programu, gdyż
istnieją jedynie w czasie jego kompilacji. Więc na przykład następujące deklaracje języka C:
const float AdecimalNumber = 3.14
const int i = 10
const char *ErrorString = "Uwaga, niebezpieczeństwo";

posiadają następujące odpowiedniki w Pascalu:

const
AdecimalNumber = 3.14;
i = 10;
ErrorString = 'Uwaga, niebezpieczeństwo';

Kompilator, ustalając typ stałej, wybiera typy o możliwie najmniejszej liczebności. Typ stałej
całkowitoliczbowej ustalany jest w następujący sposób:
Tabela 2.1 Ustalane przez kompilator typy stałych całkowitoliczbowych

Wartość Typ
od –2^63 do –2.147.483.649 Int64

od –2.147.483.648 do –32.769 Longint


od –32.768 do –129 Smallint

od –128 do –1 Shortint

od 0 do 127 0 .. 127

od 128 do 255 Byte

od 256 do 32.767 0 .. 32.767

od 32.768 do 65.535 Word

od 65.536 do 2.147.483.647 0 .. 2.147.483.647

od 2.147.483.648 do Cardinal
4.294.967.295

od 4.294.967.296 do 2^63–1 Int64

Ponadto
• stałe o wartościach rzeczywistych są stałymi typu Extended;
• stałe łańcuchowe, zależnie od ustawienia przełącznika kompilacji $H, są albo łańcuchami typu
ShortString, albo łańcuchami typu AnsiString;

• dla zbiorów (sets) liczbowych i znakowych rozmiar zajętej pamięci wynika bezpośrednio z ich postaci.

Aby uzyskać większą kontrolę nad danymi, programista może jawnie wskazać typ deklarowanej stałej, na
przykład:
const
ADecimalNumber : Double = 3.14;
i : integer = 10;
ErrorString : string = 'Uwaga, niebezpieczeństwo';

Stała ADecimalNumber jest teraz stałą typu Double, bez jawnego wskazania typu byłaby natomiast stałą typu
Extended.
Jawne wskazywanie typów w deklaracjach stałych zasługuje na nieco więcej uwagi. Programista znający Turbo
Pascal rozpozna w tych konstrukcjach zmienne inicjowane, które zostały nazwane stałymi przez
nieporozumienie, gdyż w rzeczywistości zachowują się w programie jak zwykłe zmienne; mógł się o tym
przekonać każdy, kto chciał użyć tak zdefiniowanej „stałej” jako np. indeksu granicznego w deklaracji tablicy.
Tak było we wszystkich wersjach Turbo Pascala i w Delphi 1, natomiast wersja Delphi 2 przyniosła pewną
nowość w tej materii: otóż przy włączonym przełączniku {$J} wszystko jest po staremu, jednak jego
wyłączenie spowoduje, iż kompilator zabroni modyfikowania tak deklarowanych stałych. Zdecydowanie zaleca
się tę drugą ewentualność — stałe pozostają wówczas naprawdę stałymi, zaś zmiennym można nadawać
początkowe wartości za pomocą dyrektywy var.
W wyrażeniach przypisywanych stałym (oraz przy inicjowaniu zmiennych) Object Pascal zezwala na wy-
korzystanie następujących funkcji wbudowanych: Abs(), Chr(), Hi(), High(), Length(), Lo(), Low(),
Odd(), Ord(), Pred(), Round(), SizeOf(), Succ(), Swap() i Trunc() — na przykład w taki sposób:

type
A = array [ 1 .. 2 ] of Integer;
const
W : Word = SizeOf(byte)
var
i : integer = 8;
j : SmallInt = Ord('a');
L : Longint = Trunc(3.14159);
x : ShortInt = Round(2.71828);
B1 : byte = High(A);
B2 : byte = Low(A);
C : Char = Chr(46);

Dopuszczalne jest także rzutowanie typów, na przykład


const
BigOne = Int64(1);

Wskazówka

Ku rozczarowaniu wielu programistów, Object Pascal nie posiada preprocesora podobnego do tego z języka
C. Nie można więc definiować makroinstrukcji i stąd brak mechanizmu odpowiadającego słowu kluczowemu
#define języka C (dyrektywa $define ma znaczenie zupełnie inne — definiuje tzw. symbole warunkowe
kompilacji). Jednak, począwszy od Delphi 6, można wykorzystać dyrektywy $IF i $ELSEIF umożliwiające
użycie definiowanych stałych na równi z symbolami kompilacji warunkowej.

Operatory
Operatory są symbolami języka służącymi — mówiąc najogólniej — do manipulowania danymi. Istnieją
operatory arytmetyczne — dodawania, odejmowania, mnożenia i dzielenia wartości liczbowych, operator
przypisania, wyboru elementu z tablicy itp. W niniejszym podrozdziale rozpatrzymy większość operatorów
Object Pascala i przedstawimy ich odpowiedniki w językach C, Visual Basic i Java.

Operator przypisania
Operator przypisania służy do przypisania zmiennej wartości; jest to bodaj najprostszy, lecz jednocześnie jeden z
najważniejszych operatorów języka. Oto jeden z przykładów jego zastosowania:
Number1 := 5;

Powyższa instrukcja przypisuje zmiennej Number1 wartość 5.

Operatory porównania
Operatory porównania w Delphi i w Visual Basicu są niemalże identyczne. Służą do stwierdzenia równości lub
nierówności dwóch wartości albo ich porównania pod względem relacji mniejszości. W tabeli 2.2 przedstawione
zostały łącznie operatory porównania i operatory logiczne, w tym miejscu chcemy jedynie zwrócić uwagę na
istotną różnicę między operatorem porównania (= w Delphi, == w C i Javie) a operatorem przypisania (:= w
Delphi, = w C i Javie).
Operator badający nierówność dwóch wielkości, w języku C mający sugestywną postać !=, w Pascalu ma postać
<>, na przykład:
if x <> y
Then
Cokolwiek

Operatory logiczne
Operatory logiczne realizują (w ograniczonym zakresie) operacje wynikające z algebry Boole’a (stąd często
nazywane bywają operatorami boolowskimi — ang. Boolean operators). Ich typowym zastosowaniem jest
jednoczesne testowanie kilku warunków, na przykład:
if (warunek1) and (warunek2)
Then
Cokolwiek
While (warunek1) or (warunek2) do
Cokolwiek
Operatory logiczne obecne są w każdym języku programowania, chociaż ich postać jest różnorodna. Tabela 2.2
przedstawia operatory porównania oraz operatory logiczne w Pascalu, C, Javie i Visual Basicu.
Tabela 2.2. Operatory przypisania, porównania i operatory logiczne
Operator Pascal Java i C Visual Basic

Przypisania := = =

Równości = == Is (dla obiektów) = (dla


innych typów)

Nierówności <> != <>

Mniejszości < < <

Większości > > >

Niewiększości <= <= <=

Niemniejszości >= >= >=

Logiczne „i” and && And

Logiczne „lub” or || Or

not ! Not
Zaprzeczenie

Operatory arytmetyczne
Tabela 2.3 prezentuje operatory arytmetyczne Pascala, C, Javy i Visual Basica.

Tabela 2.3. Operatory arytmetyczne


Operator Pascal Java i C Visual Basic
Dodawania + + +
Odejmowania – – –
Mnożenia * * *
Dzielenia rzeczywistego / / /
Dzielenia całkowitego div / \
Reszty z dzielenia (modulo) mod % Mod
Potęgowania brak brak ^

Jak wynika z tabeli, Pascal i Visual Basic rozróżniają dzielenie liczb całkowitych (wynik jest liczbą całkowitą)
od dzielenia liczb rzeczywistych (wynik jest liczbą rzeczywistą); Java i C nie czynią takiego rozróżnienia.

Ostrzeżenie

Wykonując dzielenie, zawsze używaj operatorów stosownych do operandów i oczekiwanego wyniku. Kom-
pilator Object Pascala nie zezwoli na dzielenie całkowite operandów, z których co najmniej jeden nie jest
liczbą całkowitą. Równie powszechnym błędem jest próba przypisania zmiennej całkowitej wyniku dzielenia
rzeczywistego (operator /), co ilustruje poniższy przykład:

Var
i : integer;
r : real;
begin
i := 4/3 // tu wystąpi błąd kompilacji
r := 3.4 div 2.3; // ta linia również jest błędna
i := Trunc(4/3); // ta linia jest poprawna
r := 3.4 / 2.3; // ta linia również jest poprawna
end;

Jako ciekawostkę odnotować należy fakt, iż wiele języków nie wykonuje dzielenia całkowitego i w związku z
tym posiada jeden, uniwersalny operator dzielenia. Dzielenie dwóch liczb całkowitych przebiega więc
następująco: konwersja na typ zmiennoprzecinkowy, wykonanie dzielenia zmiennoprzecinkowego oraz
konwersja wyniku (po zaokrągleniu lub obcięciu — różnie bywa) na typ całkowity. Jest to działanie kosztowne
oraz nieefektywne w sytuacji, gdy procesor posiada instrukcje dzielenia całkowitego (posiadają je wszystkie
procesory 80×86).

Operatory bitowe
Operatory bitowe służą do operowania na poszczególnych bitach wartości binarnej reprezentującej dane
wyrażenie. Operacje te zaliczyć można do jednej z dwóch kategorii: logiczne operacje na bitach oraz
przesuwanie bitów. Tabela 2.4 przedstawia operatory bitowe dla czterech rozpatrywanych tu języków.
Tabela 2.4. Operatory bitowe

Operator Pascal Java i C Visual Basic


Koniunkcja and & And
Zaprzeczenie not – Not
Alternatywa or | Or
Dysjunkcja xor ^ Xor
Przesunięcie w lewo shl << nie istnieje
Przesunięcie w prawo shr >> nie istnieje

Operatory zwiększania i zmniejszania


Realizują one zoptymalizowaną operację zwiększania (increment) lub zmniejszania (decrement) zmiennej typu
porządkowego. Operatory te występują w dwóch odmianach. Pierwsza z nich powoduje zmianę wartości
zmiennej o 1 (w górę lub w dół):
Inc(zmienna);
Dec(zmienna);

i jest przez kompilator przekładana na pojedynczą instrukcję INC lub DEC kodu maszynowego.
Operatory w postaci dwuargumentowej
Inc(zmienna, dystans);
Dec(zmienna, dystans);

powodują zmniejszenie albo zwiększenie zmiennej o wartość zadaną jawnie w postaci drugiego argumentu;
operacja jest realizowana przez kompilator w postaci rozkazu ADD albo SUB.

Notatka

Kompilator Delphi w wersji 2 i następnych jest na tyle „inteligentny”, że sam rozpoznaje operację
zmniejszania/zwiększania zmiennej za pomocą zwykłej operacji dodawania lub odejmowania, tak więc
przekład instrukcji x := x + 1 nie różni się od przekładu instrukcji Inc(x)1, dlatego główną korzyścią
wynikającą z użycia omawianych operatorów jest raczej wygoda programisty.

Zestawienie operatorów zwiększania i zmniejszania dla omawianych języków przedstawia tabela 2.5.
Tabela 2.5. Operatory zwiększania i zmniejszania
Operator Pascal Java i C Visual Basic
Zwiększania Inc() ++ nie istnieje
Zmniejszania Dec() –– nie istnieje

Operatory „wykonaj i przypisz”


Object Pascal, w przeciwieństwie do C i Javy, nie posiada operatorów oznaczających (mówiąc ogólnie)
wykonanie na zmiennej pewnej operacji i stanowiących pewne uogólnienie operatorów inc() i dec() — nowa
wartość zmiennej musi być zapisana explicite po prawej stronie operatora przypisania, tak więc zupełnie
naturalna w C instrukcja
x += 5;

w Pascalu musi być zapisana jako


x := x + 5;

Typy języka Object Pascal


Jedną z najkorzystniejszych cech Object Pascala jest tzw. bezpieczeństwo typów (ang. type safety). Oznacza to,
że kompilator prowadzi rygorystyczną kontrolę typów zmiennych biorących udział w operacjach i będących
parametrami wywołań procedur i funkcji. Jakiekolwiek odstępstwo od ściśle zdefiniowanych reguł powoduje
błąd kompilacji. Jednocześnie użytkownicy Pascala wolni są od tych wspaniałych ostrzeżeń kompilatora o
podejrzanych konstrukcjach z użyciem wskaźników, które to ostrzeżenia są czymś powszednim w języku C; są
to jednak tylko ostrzeżenia, niezdolne powstrzymać prób przysłowiowego zatykania okrągłej dziury
kwadratowym korkiem…
Aby, zbawienna skądinąd, rygorystyczna kontrola typów pascalowskich nie była dla użytkowników zbyt
krępująca, wprowadzono różne możliwości jej „obejścia” — amorficzne wskaźniki (typ Pointer), nakładanie
się zmiennych (dyrektywa absolute), typy wariantowe, amorficzne parametry procedur i funkcji itp. Jak w
przypadku wszelkich mechanizmów tego typu, ich użyteczność uzależniona jest od ich rozsądnego używania.

Porównanie typów

Większość typów Object Pascala posiada swe odpowiedniki w C, Javie i Visual Basicu. Zestawienie widoczne w
tabeli 2.6 może być niezwykle użyteczne w przypadku wykorzystywania w którymś z tych języków bibliotek
DLL stworzonych w innym języku.

Tabela 2.6. Porównanie typów Pascala, Javy, C i Visual Basica


Typ zmiennej Pascal Java C/C++ Visual Basic
całkowity 8-bitowy ShortInt byte char nie istnieje
ze znakiem

1
Z wyjątkiem sytuacji, gdy x jest właściwością klasy (przyp. tłum.)
całkowity 8-bitowy Byte nie istnieje BYTE, unsigned Byte
bez znaku short
całkowity 16- SmallInt short short Short
bitowy ze znakiem
całkowity 16- Word nie istnieje unsigned nie istnieje
bitowy bez znaku short
całkowity 32- Integer, int int, long Integer,
bitowy ze znakiem LongInt Long
całkowity 32- Cardinal, nie istnieje unsigned long nie istnieje
bitowy bez znaku LongWord
całkowity 64- Int64 long __int64 nie istnieje
bitowy ze znakiem
zmiennoprzecinko Single float float Single
wy 4-bajtowy
zmiennoprzecinko Real48 nie istnieje nie istnieje nie istnieje
wy 6-bajtowy
zmiennoprzecinko Double double double Double
wy 8-bajtowy
zmiennoprzecinko Extended nie istnieje long double nie istnieje
wy 10-bajtowy
stałoprzecinkowy Currency nie istnieje nie istnieje Currency
64-bitowy
data/czas 8-bajtowy TDateTime nie istnieje nie istnieje Date
wariantowy 16- Variant, VARIANT** Variant
nie istnieje
bajtowy Olevariant, Variant†, (domyślny)
TVarData Olevariant†
znak 1-bajtowy Char nie istnieje char nie istnieje
znak 2-bajtowy WideChar char WCHAR Char

łańcuch znaków ShortString nie istnieje nie istnieje nie istnieje


o ustalonej
maksymalnej
długości
dynamiczny AnsiString nie istnieje AnsiString† String
łańcuch znaków 1-
bajtowych
łańcuch znaków PChar nie istnieje char * nie istnieje
jednobajtowych z
zerowym
ogranicznikiem
łańcuch znaków PWideChar nie istnieje LPCWSTR nie istnieje
dwubajtowych z
zerowym
ogranicznikiem
dynamiczny WideString String** WideString† nie istnieje
łańcuch znaków
dwubajtowych
boolowski 1- Boolean, boolean (dowolny 1- nie istnieje
bajtowy ByteBool bajtowy)
boolowski 2- WordBool nie istnieje (dowolny 2- Boolean
bajtowy bajtowy)
boolowski 4- BOOL, nie istnieje BOOL nie istnieje
bajtowy LongBool

† — oznacza tu klasę C++ Buildera emulującą odnośną klasę Object Pascala

** — oznacza powszechnie używaną klasę lub typ, nie zaś rodzimy typ języka

Wskazówka

Podczas przenoszenia aplikacji z Delphi 1 bądź świadom tego, że typy Integer i Cardinal, 16-bitowe w
Delphi 1, są już 32-bitowe w Delphi 2 oraz w następnych wersjach. W Delphi 4 zmieniło się też znaczenie typu
Cardinal: w Delphi 2 i 3 jego zakres tożsamy był z nieujemną połową typu integer (bit znaku był po prostu
ignorowany), natomiast począwszy od Delphi 4 jest on pełnoprawną 4-bajtową liczbą całkowitą bez znaku o
zakresie 0 ÷ 4294967296.

Ostrzeżenie

W Delphi 4 zmieniło się również znaczenie identyfikatora Real. W Delphi 1, 2 i 3 oznaczał on — specyficzny
dla Turbo Pascala — 6-bajtowy format liczby zmiennoprzecinkowej, obsługiwany całkowicie w sposób
programowy i nie mający odpowiednika w formatach danych (ko)procesorów. Począwszy od Delphi 4, iden-
tyfikator Real jest synonimem typu Double; wspomnianemu formatowi 6-bajtowemu odpowiada natomiast
identyfikator Real48. Możliwe jest jednak przywrócenie dawnego znaczenia identyfikatora Real — poprzez
użycie dyrektywy kompilatora {$REALCOMPATIBILITY ON}

Znaki
W Delphi istnieją trzy typy reprezentujące pojedynczy znak:
• AnsiChar — to powszechny w większości dotychczasowych języków 1-bajtowy znak ANSI,

• WideChar — dwubajtowy znak ze zbioru znaków Unicode,

• Char — w obecnej wersji Delphi jest to typ identyczny z AnsiChar, lecz Borland zastrzegł sobie prawo
ewentualnego utożsamienia go z typem WideChar w przyszłych wersjach.
Fakt, że znak niekoniecznie jest teraz jednobajtowy, stanowi przesłankę nieco ostrożniejszego kodowania, a
przy ustalaniu rozmiaru struktur zawierających znaki, wskazane jest korzystanie z funkcji SizeOf().

Wskazówka

Funkcja SizeOf() zwraca rozmiar (w bajtach) zmiennej lub typu.

Mnogość łańcuchów…
Łańcuchem (string) nazywamy ciąg znaków i oczywiście reprezentujący go obiekt języka programowania. To
interesujące, że różnorakość implementacji łańcuchów w różnych językach programowania jest znacznie
większa niż w jakimkolwiek innym aspekcie języka.
W Object Pascalu obsługa łańcuchów znaków zrealizowana została w postaci następujących typów:
• AnsiString — podstawowy typ łańcuchów danych w Object Pascalu. Jest ciągiem (o potencjalnie
nieograniczonej długości) znaków AnsiChar. Zgodny jest z typem reprezentującym ciąg jednobajtowych
znaków zakończony bajtem zerowym.
• ShortString — to stary pascalowy znajomy — łańcuch znaków AnsiChar o ustalonej z góry
maksymalnej długości, nie przekraczającej 255 znaków. Podstawowy typ łańcuchowy w Delphi 1.
• WideString — stanowi ciąg znaków WideChar i, podobnie jak AnsiString, nie ma limitowanej
długości i zakończony jest znakiem zerowym.
• PChar — to wskaźnik do ciągu znaków typu Char zakończonego bajtem zerowym, na wzór typów char*
lub lpstr w języku C.
• PAnsiChar — to wskaźnik do zakończonego znakiem zerowym ciągu znaków typu AnsiChar.

• PWideChar — jest to wskaźnik do zakończonego znakiem zerowym ciągu znaków typu WideChar.

Zmienna deklarowana jako String jest zmienną typu AnsiString lub ShortString, zależnie od ustawienia
przełącznika kompilacji $H:
var
{$H-}
S1 : String // zmienna S1 jest typu ShortString
{$H+}
S2 : String // zmienna S2 jest typu AnsiString

Zmienna deklarowana jako String z wyspecyfikowaną maksymalną długością (nie większą niż 255) jest jednak
zawsze typu ShortString:
var
{$H-}
S1[63] : String // zmienna S1 jest typu ShortString
{$H+}
S2[63] : String // zmienna S2 jest typu ShortString

Typ AnsiString
Typ AnsiString, zwany również potocznie long string lub po polsku „długi łańcuch” pojawił się po raz
pierwszy w Delphi 2. Uosabia on tę samą łatwość obsługi, jaką miały „klasyczne” łańcuchy pascalowe i jest
jednocześnie wolny od bardzo dotkliwego ograniczenia długości do 255 znaków — długość łańcucha typu
AnsiString jest praktycznie nieograniczona.
Obsługa długich łańcuchów wiąże się z dość wyrafinowaną, niewidoczną dla użytkownika gospodarką pamięcią
operacyjną, polegającą na jej przydziale stosownie do potrzeb i odzyskiwaniu (ang. garbage collection) wtedy,
gdy nie jest już potrzebna (za chwilę zajmiemy się tym interesującym zagadnieniem w szerszym kontekście).
Uwalnia to programistę od „ręcznej” obsługi pamięci, tak uciążliwej w C++ i wersji 7.0 Borland Pascala (typ
PChar). Strukturę łańcucha AnsiString w pamięci operacyjnej przedstawia rysunek 2.1.

Na rysunku ma być AnsiString, nie AntiString

Rysunek 2.1. Łańcuch AnsiString reprezentujący napis „DDG”

Ostrzeżenie
Wewnętrzny format długich łańcuchów Delphi nie został udokumentowany — firma Borland pozostawiła sobie
w ten sposób możliwość jego modyfikacji w przyszłości. Aplikacje bazujące na tym formacie niosą więc ze
sobą ryzyko ewentualnej niezgodności z przyszłymi wersjami Delphi. Z podobnym problemem zetknęli się
swego czasu użytkownicy Delphi 1 zakładający, iż bieżąca długość łańcucha reprezentowana jest przez jego
początkowy bajt.

Jeżeli więc mimo wszystko prezentujemy tutaj wewnętrzną strukturę długiego łańcucha, to czynimy to
wyłącznie w celu poglądowego wytłumaczenia zasad jego funkcjonowania.

Jak widać na rysunku 2.1, długi łańcuch reprezentowany jest w zmiennej jako wskaźnik do ciągu znaków.
Systemowe procedury zarządzające obsługą łańcuchów gwarantują ponadto, że jest on zakończony bajtem
zerowym. Powoduje to, że długie łańcuchy mogą być używane jako parametry wywołania procedur i funkcji
Win32 API, wymagających łańcuchów z zerowym ogranicznikiem. Najbardziej nieoczywistym elementem
rysunku jest natomiast z pewnością licznik odwołań. Otóż, w celu zminimalizowania zużycia pamięci, Object
Pascal stara się zapamiętywać w pojedynczym egzemplarzu zawartość dwóch (lub więcej) zmiennych
łańcuchowych o (aktualnie) identycznej zawartości, kontrolując jedynie odwołania do tego egzemplarza za
pomocą wspomnianego licznika. Tak więc przypisanie zmiennej łańcuchowej zawartości innej zmiennej nie
spowoduje fizycznego kopiowania, lecz tylko powielenie wskaźnika (fizyczną reprezentacją zmiennych
łańcuchowych są bowiem wskaźniki) i zwiększenie licznika odwołań. Modyfikacja którejś z tych zmiennych
spowoduje jednak zerwanie jej dotychczasowego związku ze wspomnianym egzemplarzem, zmniejszenie
licznika odwołań z nim związanego i utworzenie nowego, niezależnego egzemplarza o zmienionej zawartości.
Poniższy przykład z pewnością dostatecznie wyjaśnia tę koncepcję:
var
S1, S2 : AnsiString;
begin
S1 := 'Taki sobie napis ... '
{
licznik odwołań długiego łańcucha wskazywanego
przez S1 jest równy 1
}

S2 := S1;

{
S1 i S2 wskazują na ten sam długi łańcuch, którego
licznik odwołań jest teraz równy 2
}

S2 := S2 + ' i jeszcze coś ... ';

{
Nastąpiło utworzenie niezależnego egzemplarza dla
zmiennej S2,
S1 i S2 wskazują teraz na dwa różne obszary pamięci.
Licznik odwołań łańcucha wskazywanego przez S1 znowu
jest równy 1
}

<ramka>

Zmienne typu AnsiString jako przykład zmiennych o kontrolowanym czasie życia

Pojęcie „czasu życia” wynika z pojęcia zakresu widoczności deklaracji zmiennej. W Pascalu zmienne globalne
„żyją” więc przez cały czas realizacji programu, zmienne lokalne procedur i funkcji — jedynie w czasie
realizacji tychże procedur i funkcji. Ten oczywisty poniekąd fakt nie powodował żadnych szczególnych
implikacji — aż do pojawienia się Delphi 2 i łańcuchów AnsiString, na potrzeby których, „w tle”, realizowany
jest skomplikowany scenariusz gospodarowania pamięcią.

W Turbo Pascalu, po zakończeniu realizacji programu, zwalniana była cała przydzielona mu pamięć — i tym
samym unicestwiane były wszystkie zmienne globalne. Podobne „unicestwienie” zmiennych lokalnych
procedur i funkcji sprowadzało się po prostu do zdjęcia ich ze stosu. Rozważmy jednak poniższy przykład:
procedure MyProc;

var

X: Pointer;

begin

GetMem(X,10000);

...

// tutaj procedura wykorzystuje do czegokolwiek

// obszar wskazywany przez X

...

FreeMem(X,10000);

end;

Obszar wskazywany przez wskaźnik X istnieje tylko w czasie realizacji procedury MyProc i nie ma poza nią
żadnego znaczenia z tego prostego względu, iż jest na zewnątrz niej niedostępny. Załóżmy teraz, iż
programista zapomniał o końcowej instrukcji FreeMem. Konsekwencją tego faktu byłaby po prostu strata
10000 bajtów pamięci przy każdym wywołaniu procedury. Stąd ważny wniosek, iż do zniwelowania skutków
przydziału pamięci nie wystarczy proste zdjęcie zmiennej X ze stosu.

Ten prosty przykład wyjaśnia istotę problemu, który pojawił się w momencie wprowadzenia do Pascala
zmiennych, na potrzeby których gospodarka pamięcią nie posiada — mówiąc najprościej — odzwierciedlenia
w kodzie źródłowym programu, lecz wykonywana jest „w tle”. Konkretnie — w przypadku zmiennych typu
AnsiString, w związku z zakończeniem procedury nie wystarczy już zwykłe zdjęcie ze stosu zmiennej,
zawierającej li tylko wskaźnik do danych zasadniczych; z drugiej strony wykluczone są jakiekolwiek jawne
instrukcje zwalniające, gdyż gospodarka pamięcią odbywa się tu bez związku z kodem źródłowym programu.
Stąd wniosek, iż integralną częścią procesu niejawnego gospodarowania pamięcią powinno być jej
automatyczne zwalnianie w przypadku zakończenia czasu życia odnośnej zmiennej. Proces taki rzeczywiście
ma miejsce, a zmienne, których on dotyczy, noszą nazwę zmiennych z kontrolowanym czasem życia (lifetime
memory-managed). Opisywane w niniejszym punkcie zmienne typu AnsiString i WideString są właśnie
takimi zmiennymi — inne przykłady zmiennych tej kategorii przedstawimy w dalszej części rozdziału i w
rozdziałach następnych.

Dokładniej — proces obsługi zmiennych o kontrolowanym czasie życia daje się pokrótce opisać następująco:
zmienne globalne inicjowane są automatycznie podczas wykonywania sekcji initialization modułu, w
którym zostały zdefiniowane (lub podczas rozpoczynania programu, o ile moduł takiej sekcji nie posiada).
Wszystkie czynności związane z zakończeniem „czasu życia” zmiennych odbywają się — również
automatycznie — w czasie kończenia sekcji finalization tego modułu (lub podczas kończenia programu,
jeśli moduł sekcji finalization nie posiada). W stosunku do zmiennych lokalnych procedur i funkcji
odpowiednie działania następują podczas wejścia do procedury i bezpośrednio przed wyjściem z niej. Skutek
jest taki, jak gdyby treść procedury „zanurzona” została w dodatkowym bloku try…finally (oczywiście —
tylko pojęciowo), co ilustruje następujący przykład:

// rzeczywista postać procedury

procedure Foo;

var

S: AnsiString;

begin

...

// ciało procedury wykorzystujące S

...

end;

Koncepcyjnie wygląda to natomiast tak:

procedure Foo;

var

S: AnsiString;

begin

S := ''; // inicjalizacja

try

...

// ciało procedury wykorzystujące S

...
finally

// zwolnij zasoby przydzielone do S

end;

end;

Nie jest to zresztą nic nadzwyczajnego. Wróćmy na chwilę do procedury MyProc — jeżeli programista
chciałby uniknąć zagubienia owych 10000 bajtów pamięci na skutek wystąpienia wyjątku, powinien swą
procedurę sformułować mniej więcej tak:

procedure MyProc;

var

X: Pointer;

begin

X := NIL;

try

GetMem(X,10000);

...

// tutaj procedura wykorzystuje do czegokolwiek

// obszar wskazywany przez X

...

finally

if X <>NIL

Then

FreeMem(X,10000);
end;

end;

Analogia widoczna jest aż nadto dobrze.

<ramka>

Operacje na łańcuchach
Do połączenia (konkatenacji) dwóch łańcuchów służy operator + lub funkcja Concat()— ta ostatnia
zachowana została ze względów kompatybilności i należy jej raczej unikać. Oto przykłady łączenia łańcuchów
Object Pascala:

Var
S, S2 : AnsiString;
begin
S := 'Szafa';
S2 := ' grająca';
S := S + S2; { Szafa grająca }
end.

Var
S, S2 : AnsiString;
begin
S := 'Szafa';
S2 := ' grająca';
S := Concat(S,S2); { Szafa grająca }
end.

Wskazówka

Ogranicznikiem literałów łańcuchowych jest w Pascalu pojedynczy apostrof.

Notatka

Funkcja Concat() stanowi przykład „magicznych” funkcji kompilatora — owa „magia” polega na tym, iż
funkcji tej nie da się napisać w Pascalu. Takich „funkcji” i „procedur” jest w Pascalu więcej — że wspomnimy
tylko o najpopularniejszych Readln(), Writeln(), New(), SizeOf(). Ich lista jest zamknięta i ściśle
określona — przy ich przekładzie na kod wynikowy kompilator posługuje się specjalnie zdefiniowanymi na tę
okazję modułami (ang. helper functions) zawartymi w bibliotece RTL i w module System.

Niezależnie od „magicznych” funkcji oraz procedur istnieje dość pokaźny zestaw „pascalowych”
podprogramów operujących na łańcuchach; są one w większości zlokalizowane w module SysUtils, a ich
wykaz można odnaleźć w systemie pomocy pod hasłem „String-handling routines (Pascal-
style)”. Dodatkowo, kilka użytecznych podprogramów operujących na łańcuchach możesz znaleźć w
module StrUtils, znajdującym się na dołączonym do książki krążku CD-ROM w katalogu \Source\Utils.
Długość łańcucha i alokacja pamięci
Bezpośrednio po zadeklarowaniu, zmienna łańcuchowa nie posiada przydzielonej pamięci i nie reprezentuje
żadnego napisu. Przypisanie jej jakiegoś napisu spowoduje przydział pamięci lub ustalenie wskazania na obszar
już przydzielony. Ilustruje to poniższy przykład:
Var
S1, S2 : AnsiString;
begin
S1 := 'Pierwszy';
{
zmiennej S1 przydzielono obszar pamięci o wielkości co
najmniej 9 bajtów, zawierający obecnie napis 'Pierwszy'
zakończony bajtem zerowym
}
S2 := 'Drugi';
{
zmiennej S2 przydzielono obszar pamięci o wielkości co
najmniej 6 bajtów, zawierający obecnie napis 'Drugi'
zakończony bajtem zerowym
}
S1 := S2;
{
zmienne S1 i S2 wskazują teraz na ten sam łańcuch. Obszar
poprzednio wskazywany przez zmienną S2 nie jest już do
niczego potrzebny i może zostać ponownie wykorzystany
}

Mechanizm zarządzania pamięcią na potrzeby długich łańcuchów jest bardzo wyrafinowany, w jednym wszakże
przypadku nie zapewnia on dostatecznej długości łańcucha: wtedy, gdy odwołujesz się do niego na wzór
tablicowy, używając indeksu explicite:
Var
S : AnsiString;
begin
S[1] := 'a';

W powyższym przykładzie zmiennej S nie została jeszcze przydzielona pamięć, jej zawartością jest pusty
wskaźnik, a więc odwołanie do S[1] gwarantuje wystąpienie błędu wykonania. Jeżeli jednak odwołanie
powyższe poprzedzone zostanie przypisaniem łańcuchowi S napisu co najmniej jednoznakowego, błąd nie
wystąpi:
Var
S, T : AnsiString;
begin
S := 'b';
S[1] := 'a'
T := 'Ala ma kota';
T[1] := 'U';

Obecnie treścią zmiennej S jest jednoznakowy napis 'a', natomiast treścią zmiennej T jest zdanie 'Ula ma
kota'.
Istnieje jednak prostszy sposób na wymuszenie przydziału pamięci dla długiego łańcucha — służy do tego
funkcja SetLength():
Var
S : AnsiString;
begin
Setlength(S,1);
S[1] := 'a'

Wywołanie SetLength(S, 1) spowoduje przydzielenie zmiennej S obszaru pamięci o wielkości


wystarczającej przynajmniej na przechowanie napisu jednoznakowego.

Długie łańcuchy a funkcje Win32 API


Procedury interfejsu Win32 API, jak i wielu innych interfejsów programisty, wymagają jako parametrów
różnorodnych tekstów, np. nazw katalogów, plików. Wymaganą ich postacią jest zakończony bajtem zerowym
ciąg znaków, a ich przekazanie do procedury następuje za pomocą wskaźnika; do rzadkości należą środowiska
wymagające innej postaci parametrów tekstowych.
Z opisanych własności długich łańcuchów Object Pascala wynika ich znakomita przydatność do tego celu.
Potrzebny jest jedynie drobny zabieg kosmetyczny — kompilator nie zaakceptuje zmiennej typu String jako
parametru aktualnego w miejscu, gdzie wymagany jest wskaźnik do ciągu znaków (PChar), dlatego konieczne
jest w tym przypadku rzutowanie typu (zjawiskiem tym zajmiemy się w dalszej części rozdziału). Z natury
długiego łańcucha wynika, że takie rzutowanie jest operacją sensowną, poza tym jest ono dozwolone przez
reguły syntaktyczne kompilatora.
Oto prosty przykład funkcji pobierającej nazwę bieżącego katalogu — maksymalna długość nazwy katalogu w
systemach Windows 95 i Windows NT wynosi 260 znaków:
{$H+}
var
S : String;
begin
SetLength(S,260);
GetWindowsDirectory(PChar(S), 260);

To jednak jeszcze nie koniec: jak wynika z rysunku 2.1, długiemu łańcuchowi towarzyszą dodatkowe informacje
organizacyjne. Procedury Win32 API, traktując parametry jako ciągi znaków zakończone bajtem zerowym (i nic
ponadto), nie są owych „dodatków” świadome. Z tego też względu, po wykonaniu procedury
GetWindowsDirectory() w powyższym przykładzie, zmienna S nie jest jeszcze „rasowym” długim
łańcuchem — programista musi sam uaktualnić informacje dodatkowe. Da się to łatwo wykonać za pomocą
wspomnianej funkcji SetLength() lub nieco wygodniejszej funkcji RealizeLength() znajdującej się w
module StrUtils:
procedure RealizeLength ( var S : String );
begin
SetLength( S, StrLen(PChar(S)) );
end;

Oto kompletna postać procedury dostarczającej nazwę bieżącego katalogu:


Procedure GetCurrentDir ( var S : String );
begin
SetLength(S,260); // przydział pamięci
GetWindowsDirectory(PChar(S), 260); // pobranie treści
RealizeLength(S); // uaktualnienie informacji organizacyjnych
end;

Ostrzeżenie

Ponieważ długi łańcuch podlega procesowi odzyskiwania pamięci (garbage collection), należy zachować
ostrożność podczas jego rzutowania na typ PChar — należy mianowicie upewnić się, iż zmienna stanowiąca
argument rzutowania nie zakończyła jeszcze swego życia. W kontekście przedstawionych wcześniej
informacji na temat zmiennych o kontrolowanym czasie życia, wymaganie takie wydaje się zupełnie
oczywiste.

Długie łańcuchy a przenoszenie aplikacji


Możesz w ogóle zrezygnować z długich łańcuchów, konsekwentnie używając przełącznika $H– — i wtedy typ
String z Delphi 1 zachowuje swoje znaczenie w następnych wersjach Delphi. Jeżeli jednak chcesz
wykorzystać zalety długich łańcuchów, musisz nieco zmodyfikować kod aplikacji. W Delphi 1 typ String
reprezentował klasyczny łańcuch znaków poprzedzony bajtem oznaczającym jego długość, w wersjach
następnych, przy ustawionym przełączniku $H+, typ String reprezentuje długi łańcuch, mający diametralnie
różną strukturę. W związku z tym:
• Wszystkie odwołania do typu PString należy zamienić na String (reprezentacją zmiennych tego
ostatniego są bowiem wskazania na łańcuchy).
• Należy pamiętać, że długość łańcucha nie jest już reprezentowana przez jego zerowy element! Tak więc
próba odczytania (zapisania) S[0] jako długości łańcucha S jest bezsensowna — zamiast tego należy użyć
funkcji Length() (SetLength()).
• Funkcje StrPas() i StrPCopy(), stanowiące w Delphi 1 „pomost” między klasycznymi łańcuchami oraz
łańcuchami z zerowym ogranicznikiem, nie są już do niczego potrzebne. Przepisania ciągu znaków typu
PChar do typu String dokonuje się obecnie za pomocą zwykłego operatora przypisania

StrVar := PCharVar;

zamiast niegdysiejszego
StrVar := StrPas(PCharVar);
Typ ShortString
Klasyczne pascalowe łańcuchy dostępne są nadal pod postacią typu ShortString, funkcjonującego niezależnie
od ustawienia przełącznika $H. Dla uproszczenia będziemy ów typ nazywać „krótkim łańcuchem”.
Pod względem strukturalnym krótki łańcuch jest tablicą jednobajtowych znaków, indeksowaną począwszy od 1 i
poprzedzoną bajtem zawierającym bieżącą długość (bajt ten uważany jest także za zerowy element wspomnianej
tablicy). Ilustruje to rysunek 2.2.

Rysunek 2.2. Łańcuch ShortString reprezentujący napis „DDG”

Zarządzanie krótkimi łańcuchami nie wiąże się z dynamicznym gospodarowaniem pamięcią — pamięć dla
zmiennej przydzielana jest od razu zgodnie z jej zadeklarowaną długością, zmienne typu ShortString nie są
więc zmiennymi o kontrolowanym czasie życia. Operacje na krótkich łańcuchach są więc bardzo efektywne,
jednak dotkliwe jest ograniczenie ich maksymalnej długości do 255 znaków.
Oto przykład przypisania wartości krótkiemu łańcuchowi:
var
S : ShortString;
begin
S := 'Krótki napis';

Dla oszczędności pamięci, możemy ograniczyć maksymalną długość krótkiego łańcucha, wskazując ją (w
deklaracji) w nawiasach kwadratowych, na przykład:

var
Nazwisko: string[20];

Zgodnie z powyższą deklaracją zmienna Nazwisko zajmuje w pamięci 21 bajtów. Gdybyśmy zadeklarowali ją
jako
var
Nazwisko: ShortString;

zajmowałaby 256 bajtów.

Przypisywanie krótkiemu łańcuchowi napisu zbyt długiego w stosunku do zadeklarowanej długości jest
bezpieczne dla aplikacji — z zastrzeżeniem, iż końcówka napisu zostaje utracona. W wyniku wykonania
poniższej sekwencji

var
Napis: String[8];

Napis := 'Zbyt długi napis';

zmienna Napis zawierać będzie napis Zbyt dłu.

Nie jest natomiast bezpieczne odwoływanie się do poszczególnych pozycji łańcucha poza zadeklarowaną
długością — poniższa sekwencja z dużym prawdopodobieństwem spowoduje błąd wykonania, w każdym razie
jej skutki są nieokreślone (chyba że ustawiono przełącznik $R+, wtedy wykryte zostanie przekroczenie
dopuszczalnego zakresu):

var
Napis: String[8];
i: integer;


i := 10;
Napis[i] := '*';
Notabene gdy napiszemy to bardziej bezpośrednio
var
Napis: String[8];


Napis[10] := '*';

błąd zostanie wykryty już na etapie kompilacji — na takie prymitywne sztuczki kompilator jest bowiem za
mądry.

Wskazówka

Mimo iż ustawienie przełącznika $R+ może przyczynić się do wykrycia wielu błędów związanych m.in. z
przekroczeniem zadeklarowanego zakresu tablicy lub łańcucha, związane z tym testy generalnie spowalniają
wykonanie aplikacji, więc po (wystarczającym) przetestowaniu aplikacji, należy ów przełącznik wyłączyć.

W przeciwieństwie do długich łańcuchów, krótkie łańcuchy zupełnie nie nadają się na parametry wywołań
większości funkcji Win32 API, nie posiadają bowiem zerowego ogranicznika. Ich przekształcenie do postaci z
zerowym ogranicznikiem nie sprowadza się do zwykłego rzutowania typów, lecz wymaga pewnych
dodatkowych operacji. Zajmuje się tym poniższa funkcja z modułu StrUtils:

Function ShortStringAsPChar ( var S : ShortString ) : PChar;


{
Bieżąca zawartość zmiennej S musi być krótsza niż
maksymalna zadeklarowana długość, inaczej ostatni znak
zostanie zignorowany.
}
begin
if Length(S) = High(S)
Then
Dec (S[0]);
S[Ord(Length(S)) + 1] := #0;
Result := @S[1];
end;

Oto jeszcze jeden przykład przekształcenia długiego łańcucha w łańcuch z zerowym ogranicznikiem:
Function ShortToASCIIZ ( var S : ShortString ) : PChar;
// © A. Grażyński
var
k: byte;
begin
k := Length(S);
if k > High(S) // to na wypadek, gdyby S[0] zawierało zbyt dużą wartość
then
k := High(S);
Move(S[1], S[0], k);
S[k] := #0;
ShortToASCIIZ := @S[0];
end;

Procedura ShortToASCIIZ nie wymaga ograniczenia długości łańcucha wejściowego, powoduje jednak
zniszczenie jego zawartości. Możemy jednak tę zawartość odzyskać, dokonując prostego wywołania
S := StrPas(X);

gdzie X jest wskaźnikiem o wartości zwróconej przez funkcję ShortToASCIIZ (wskaźnik ten straci oczywiście
swą ważność):

var
S: ShortString;
X: PChar;
begin
...
S := 'Jakiś napis';
X := ShortToASCIIZ(S)

...
JakasFunkcjaAPI(X);

...

S := StrPas(X);

// teraz wskaźnik X nie może już zostać użyty

Typ WideString
Łańcuchy typu WideString są odpowiednikami łańcuchów AnsiString w świecie dwubajtowych znaków
WideChar. Są one również dynamicznie alokowane, a ich zmienne są zmiennymi o kontrolowanym czasie
życia. Ponadto typy AnsiString oraz WideString są ze sobą zgodne w sensie przypisania, istnieją jednak trzy
podstawowe różnice pomiędzy nimi:
• Łańcuchy WideString składają się z dwubajtowych znaków WideChar, co czyni je odpowiednimi do
przechowywania słów utworzonych nad alfabetem kompatybilnym z kodem Unicode.
• Pamięć dla łańcuchów WideString alokowana jest za pomocą funkcji API SysAllocStrLen(), co czyni
je kompatybilnymi z charakterystycznymi dla OLE łańcuchami typu BSTR.
• W odniesieniu do łańcuchów WideString nie jest prowadzona „oszczędnościowa polityka” współdzielenia
egzemplarzy na podstawie licznika odwołań. Oznacza to, że łańcuchy WideString, identyczne pod
względem zawartości, lecz związane z różnymi zmiennymi zawsze istnieją w postaci oddzielnych
egzemplarzy. Oznacza to również, iż każda operacja przypisania pomiędzy dwiema zmiennymi typu
WideString realizowana jest przez rzeczywiste kopiowanie zawartości łańcucha. Czyni to oczywiście
łańcuchy WideString mniej efektywnymi od łańcuchów AnsiString pod względem szybkości
operowania oraz wykorzystania pamięci.
Jak wspomniano wcześniej, typy AnsiString oraz WideString są ze sobą zgodne w sensie przypisania —
wszystkie niezbędne konwersje wykonywane są automatycznie. Poniższy fragment programu jest więc
poprawny w Object Pascalu:
var
W: WideString;
S: AnsiString;
begin
W := 'Margaritaville';
S := W;
S := 'Come Monday';
W := S;
end;

Łańcuchy WideString mogą także występować jako parametry standardowych funkcji operujących na
łańcuchach — Concat(), Copy(), Insert(), Pos(), SetLength(), Length() itp. — mogą być również
argumentami operatorów +, = i <>, na przykład:
var
W1, W2 : WideString;
P: Integer;
begin
W1 := 'Enfield';
W2 := 'field';
if W1 <> W2
Then
P := Pos(W1, W2);
end;

Dopuszczalne jest również odwoływanie się do poszczególnych znaków łańcucha WideChar, na przykład:
var
W: WideString;
C: WideChar;
begin
W := 'Ebony and Ivory living in perfect harmony';
C := W[Length(W)]; // C zawiera ostatni znak łańcucha W
end;
Łańcuchy z zerowym ogranicznikiem
Łańcuchy takie, zwane w oryginale null terminated strings, stanowią ciąg znaków z wyróżnionym (np. przez
wskaźnik) pierwszym znakiem. Wszystkie następne znaki aż do znaku o kodzie zero stanowią zawartość
łańcucha, znak zerowy nie jest już jego częścią i pełni rolę ogranicznika. Stąd prosty wniosek, że repertuar
znaków reprezentowalnych w łańcuchach omawianego typu jest zubożony o znak o kodzie zero.
W poprzednich wersjach Pascala, do Delphi 1 włącznie, znak reprezentowany był przez jeden bajt pamięci, stąd
też istniał jedynie jeden typ łańcuchów z zerowym ogranicznikiem — typ PChar. Typ ten istnieje nadal w
następnych wersjach Delphi, ze względów kompatybilności oraz na potrzeby interfejsu Win32 API. Ze względu
jednakże na trzy typy znaków (Char, WideChar i AnsiChar) w wprowadzono w Delphi 2 dodatkowe typy
omawianych łańcuchów: PWideChar i PAnsiChar.
Jak łatwo wywnioskować z powyższego opisu, łańcuch z zerowym ogranicznikiem reprezentowany jest przez
wskaźnik do pierwszego znaku (rysunek 2.3), a wymienione trzy typy PChar, PWideChar i PAnsiChar są
według terminologii języka Pascal typami wskaźnikowymi (pointers).

Rysunek 2.3. Napis „DDG” reprezentowany w postaci łańcucha z zerowym ogranicznikiem


W odróżnieniu od łańcuchów typu AnsiString, opisywanym tu łańcuchom nie towarzyszy żaden mechanizm
wspomagający zarządzanie pamięcią operacyjną — jej przydzielanie i zwalnianie odbywa się w sposób jawny.
Podstawową funkcją dokonującą przydziału pamięci dla łańcuchów z zerowym ogranicznikiem jest funkcja
StrAlloc(), możliwe jest jednak wykorzystanie w tym celu także podstawowych funkcji Object Pascala, w
rodzaju AllocMem(), GetMem(), StrNew() , a nawet VirtualAlloc(). Należy jednak zaznaczyć, że sposób
zwalniania przydzielonej pamięci musi być zgodny ze sposobem jej przydzielania; wzajemną odpowiedniość
niektórych funkcji w tym względzie przedstawia tabela 2.7.

Tabela 2.7. Funkcje przydziału i zwalniania pamięci operacyjnej na potrzeby łańcuchów z zerowym
ogranicznikiem
Funkcja przydzielająca Funkcja zwalniająca
AllocMem() FreeMem()
GlobalAlloc() GlobalFree()
GetMem() FreeMem()
New() Dispose()
StrAlloc() StrDispose()
StrNew() StrDispose()
VirtualAlloc() VirtualFree()

Choć naruszenie reguł powyższej tabeli nie zawsze jest błędem (doświadczony programista wie przecież, że np.
StrAlloc() oraz StrNew() korzystają z procedury GetMem()), to jednak ich przestrzeganie zmniejsza ryzyko
popełnienia błędu.
Poniższy przykład ilustruje wykorzystanie łańcuchów z zerowym ogranicznikiem.
Var
P1, P2 : PChar;
S1, S2 : AnsiString;
begin
P1 := StrAlloc(64 * Sizeof(Char));
{ P1 wskazuje na 63 znakowy łańcuch }

StrPCopy (P1, 'Delphi 6 ');


{ Do łańcucha P1 zostaje wpisana konkretna zawartość }

S1 := 'vademecum profesjonalisty';
{ Do łańcucha S1 zostaje wpisana konkretna zawartość }

P2 := StrNew(PChar(S1));
{ P2 wskazuje na kopię S1 }

StrCat(P1, P2);
{ konkatenacja P1 i P2 }

S2 := P1
{ S2 zawiera napis 'Delphi 6 vademecum profesjonalisty' }

StrDispose(P1);
StrDispose(P2);
{ zwolnienie przydzielonej pamięci}
end.

Zwróć uwagę, że rozmiar przydzielanej pamięci zostaje obliczony za pomocą konstrukcji SizeOf(Char) —
jest to prostą konsekwencją faktu, że znak typu Char być może nie będzie już jednobajtowy w następnych
wersjach Delphi.
Funkcja StrCat() wykonuje konkatenację dwóch łańcuchów typu PChar — nie można w tym celu
wykorzystać operatora +, jak to miało miejsce w przypadku łańcuchów AnsiString, WideString i
ShortString.

Funkcja StrNew() tworzy kopię łańcucha podanego jako parametr. Ponieważ funkcje operujące na łańcuchach
z zerowym ogranicznikiem nie posiadają żadnej informacji o wielkości pamięci przydzielanej na ich potrzeby,
odpowiedzialność za przydzielenie wystarczająco dużego obszaru spoczywa całkowicie na programiście.
Najczęstszym błędem jest przydzielanie zbyt małego obszaru — w poniższym przykładzie funkcja StrCat()
usiłuje przypisać 13-znakowy napis „Witaj świecie” do łańcucha zdolnego pomieścić napis co najwyżej 6-
znakowy:
var
P1, P2 : PChar;
begin
P1 := StrNew('Witaj ');
P2 := StrNew('świecie!');
StrCat (P1, P2 ); // tu następuje wyjście poza przydzieloną pamięć
......

Wskazówka

Opis funkcji oraz procedur operujących na łańcuchach z zerowym ogranicznikiem znaleźć można w systemie
pomocy pod hasłem „String-handling routines (null-terminated)”. Wiele użytecznych funkcji zawiera również
moduł STRUTILS w katalogu \SOURCE\UTIL na załączonym krążku CD-ROM.

Typy wariantowe
Typ Variant jest w Pascalu zupełną nowością; konsekwencją wspomnianego wcześniej bezpieczeństwa typów
jest absolutne ustalenie typu każdej zmiennej już na etapie kompilacji. Takie podejście nie da się jednak
pogodzić ze standardami programowania w Windows, szczególnie w aspekcie mechanizmu OLE, o czym będzie
mowa w dalszej części książki. Wprowadzono więc możliwość dynamicznego „przepoczwarzania” się zmiennej,
czyli zmiany jej typu stosownie do kontekstu wykonywanego programu2. Najważniejszą przesłanką powstania
typu wariantowego była niewątpliwie konieczność przekazywania w jednolity sposób danych różnych typów w
ramach mechanizmu automatyzacji OLE. Nieprzypadkowo więc „delphicka” implementacja typu Variant jest

2
Zjawisko dynamicznej zmiany typu znane było już np. w popularnym języku Clipper — w wersji 87 nie istniała nawet
możliwość deklarowania typu zmiennej, wszystkie zmienne miały charakter wariantowy. W Visual Basicu domyślnym
typem (nie zadeklarowanej) zmiennej jest właśnie typ Variant. W Turbo Pascalu namiastkę zmiennych wariantowych
stanowiły parametry amorficzne (ang. untyped parameters) oraz rzutowanie typów (ang. typecasting) — konstrukcje
zaczerpnięte z języka Ada. O mechanizmie zmiennych wariantowych myśleli już w latach sześćdziesiątych (!) twórcy jednego
z pierwszych języków wysokiego poziomu — języka Algol 60 — lecz szybko uznali oni, że moc obliczeniowa współczesnych
komputerów nie byłaby w stanie zapewnić wystarczającej efektywności językowi implementującemu zmienne wariantowe
(przyp. tłum.).
niemal identyczna ze stosowaną w OLE, choć jej użyteczność wykracza daleko poza ów kontekst — oferując
wyjątkowo silne narzędzie programistyczne. W chwili obecnej Object Pascal jest jedynym całkowicie
kompilowalnym językiem implementującym zmienne wariantowe zarówno w aspekcie dynamicznej zmiany
typu w czasie wykonywania programu, jak i pod kątem pełnoprawnego typu danych w składniowej
i semantycznej konwencji kompilatora.
W Delphi 3 wprowadzono dodatkowo inny typ wariantowy — OLEvariant. Od swego pierwowzoru
(Variant) różni się zakresem reprezentowalnych typów, ograniczonym do typów wykorzystywanych przez
automatyzację OLE. W niniejszym rozdziale skoncentrujemy się głównie na typie Variant, a typ OLEvariant
będziemy przywoływać tylko w kontekście porównań ze swym pierwowzorem.

Dynamiczna zmiana typu


Podstawową cechą zmiennych typu Variant jest możliwość dynamicznej zmiany ich typu w czasie wykonania
programu — i wynikająca stąd niemożność określenia tego typu na etapie kompilacji. Oto fragment programu,
pod każdym względem poprawnego w Object Pascalu:
var
V: Variant;
begin
// Zmienna V zawiera łańcuch znaków
V := 'Delphi 6 jest wspaniałe';

// Zmienna V zawiera liczbę całkowitą


V := 1;

// Zmienna V zawiera liczbę zmiennoprzecinkową


V := 123.34;

// Zmienna V zawiera wartość boolowską


V := TRUE;

// Zmienna V zawiera wskazanie na obiekt OLE


V := CreateOLEobject('Word.Basic');

Zmienna typu Variant może podczas wykonywania programu przechowywać wartości całkowite,
zmiennoprzecinkowe, łańcuchy znaków, wartości boolowskie, znaczniki daty/czasu, kwoty pieniężne
(Currency) i obiekty automatyzacji OLE. Może ona również reprezentować tablicę heterogeniczną, tj. taką,
której rozmiary i typy elementów ulegają dynamicznej zmianie, w szczególności — tablicę, której elementy są
wskaźnikami do innych tablic wariantowych.

Wewnętrzna struktura zmiennej wariantowej


Wewnętrzna implementacja zmiennej typu Variant oparta jest na następującej strukturze3:
Type
TVarType = Word;
PVarData = ^TVarData;
{$EXTERNALSYM PVarData}
TVarData = packed record
VType: TVarType;
case Integer of
0: (Reserved1: Word;
case Integer of
0: (Reserved2, Reserved3: Word;
case Integer of
varSmallInt: (VSmallInt: SmallInt);
varInteger: (VInteger: Integer);
varSingle: (VSingle: Single);
varDouble: (VDouble: Double);
varCurrency: (VCurrency: Currency);
varDate: (VDate: TDateTime);
varOleStr: (VOleStr: PWideChar);
varDispatch: (VDispatch: Pointer);
varError: (VError: LongWord);
varBoolean: (VBoolean: WordBool);

3
Nie należy mylić ze sobą dwóch bytów o zbliżonym nazewnictwie, lecz różnych znaczeniach: rekordu z częścią wariantową
(jakim jest rekord TVarData) oraz zmiennych wariantowych. Część wariantowa rekordu jest pascalowym odpowiednikiem
unii języka C, w ramach której kilka różnych danych zajmuje ten sam obszar pamięci. Jedynym związkiem wspomnianego
rekordu i zmiennej wariantowej jest prezentowana struktura.
varUnknown: (VUnknown: Pointer);
varShortInt: (VShortInt: ShortInt);
varByte: (VByte: Byte);
varWord: (VWord: Word);
varLongWord: (VLongWord: LongWord);
varInt64: (VInt64: Int64);
varString: (VString: Pointer);
varAny: (VAny: Pointer);
varArray: (VArray: PVarArray);
varByRef: (VPointer: Pointer);
);
1: (VLongs: array[0..2] of LongInt);
);
2: (VWords: array [0..6] of Word);
3: (VBytes: array [0..13] of Byte);
end;

Jak łatwo policzyć, zmienna wariantowa zajmuje 16 bajtów pamięci. Pierwsze dwa bajty (VType) określają
aktualny typ zawartości zmiennej:
varEmpty = $0000; { vt_empty }
varNull = $0001; { vt_null }
varSmallint = $0002; { vt_i2 }
varInteger = $0003; { vt_i4 }
varSingle = $0004; { vt_r4 }
varDouble = $0005; { vt_r8 }
varCurrency = $0006; { vt_cy }
varDate = $0007; { vt_date }
varOleStr = $0008; { vt_bstr }
varDispatch = $0009; { vt_dispatch }
varError = $000A; { vt_error }
varBoolean = $000B; { vt_bool }
varVariant = $000C; { vt_variant }
varUnknown = $000D; { vt_unknown }
//varDecimal = $000E; { vt_decimal } {nie obsługiwane}
{ undefined $0f } {nie obsługiwane}
varShortInt = $0010; { vt_i1 }
varByte = $0011; { vt_ui1 }
varWord = $0012; { vt_ui2 }
varLongWord = $0013; { vt_ui4 }
varInt64 = $0014; { vt_i8 }
//varWord64 = $0015; { vt_ui8 } {nie obsługiwane}

{ rozszerzając interpretację typu Variant, należy zmodyfikować


zmienną varLast oraz tablice BaseTypeMap i OpTypeMap w module Variants}
varStrArg = $0048; { vt_clsid }
varString = $0100; { łańcuch pascalowy, niezgodny z OLE }
varAny = $0101; { typ "any" CORBA }
varTypeMask = $0FFF;
varArray = $2000;
varByRef = $4000;

Jak łatwo zauważyć, nie istnieje możliwość reprezentowania w zmiennej wariantowej wskaźników ani obiektów.

Wskazówka

Delphi 6 umożliwia użytkownikowi rozszerzenie powyższej interpretacji i wykorzystywanie zmiennych


wariantowych do reprezentowania wartości samodzielnie zdefiniowanych typów. Wymaga to jednak ingerencji
w kod źródłowy biblioteki RTL, konkretnie — w moduł Variants. Należy zmienić trzy elementy: zmienną
globalną varLast zawierającą numer ostatniego zdefiniowanego wariantu (obecnie varInt64), tablicę
BaseTypeMap określające listę zdefiniowanych wariantów oraz tablicę OpTypeMap określającą konwersję
pomiędzy poszczególnymi wariantami (przyp. tłum.).

Kompilator dopuszcza samodzielne „mapowanie” zmiennej wariantowej przez strukturę TVarData, co


umożliwia bezpośrednie odwoływanie się do pól tej ostatniej, na przykład:
Var
V: Variant;
begin
TVarData(V).VType := varInteger;
TVarData(V).VInteger := 2;
end;
Powyższa konstrukcja jest równoważna prostszej konstrukcji
V := 2;

Nie to jest jednak najważniejsze; jak zobaczymy za chwilę, bezpośrednie operowanie polami struktury
TVarData niesie ze sobą niebezpieczeństwo dezorganizacji zarządzania pamięcią.

Zmienne wariantowe a kontrolowany czas życia


Zmienna wariantowa może reprezentować łańcuch AnsiString — pole VType ma wówczas wartość
varString, natomiast pole VString zawiera wskaźnik do tego łańcucha. Kompilator uwzględnia oczywiście
fakt, iż ten łańcuch jest zmienną o kontrolowanym czasie życia; wobec możliwości reprezentowania przez
zmienną wariantową innych wielkości tej kategorii, generalnie same zmienne wariantowe są zmiennymi o
kontrolowanym czasie życia. Spójrzmy na poniższy fragment:

procedure ShowVariant(S: String);


var
V: Variant;
begin
V := S;
ShowMessage(V);
end;

Wszystko odbywa się tu automatycznie — programista nie musi się martwić o zarządzanie pamięcią na potrzeby
łańcucha reprezentowanego przez zmienną V. Delphi, realizując powyższą sekwencję, nadaje początkowo
zmiennej wariantowej wartość nieokreśloną (Unassigned). Następnie przypisuje polu VType identyfikator
varString, natomiast do pola VString kopiuje wskaźnik do łańcucha reprezentowanego przez S, jednocześnie
zwiększając jego licznik odwołań. Kiedy zmienna V zakończy swój czas życia (to znaczy — gdy zakończy się
wykonywanie procedury ShowVariant), łańcuch ten jest traktowany tak, jak gdyby był reprezentowany przez
„zwykłą” zmienną łańcuchową — jego licznik odwołań jest zmniejszany o 1, a jeżeli osiągnie przez to wartość
zero, zwalniana jest cała pamięć przydzielona łańcuchowi. Można to przedstawić poglądowo jako zanurzenie
całej procedury ShowVariant() w wyimaginowanym bloku try…finally:

procedure ShowVariant(S: String);


var
V: Variant;
begin
V := Unassigned;
try
V := S;
ShowMessage(V);
finally
// zwolnij zasoby przydzielone do zmiennej wariantowej
end;
end;

Z podobnym, choć trochę bardziej złożonym przypadkiem automatycznego zwalniania zasobów, mamy do
czynienia w sytuacji zmiany aktualnego typu zmiennej wariantowej — z łańcuchowego na inny, na przykład:

Procedure ChangeVariant(S:String; I:Integer);


var
V: Variant;
begin
V := S;
ShowMessage(V);
V := I;
end;

To, co dzieje się podczas realizacji powyższej sekwencji, można by zapisać następująco (instrukcje wyróżnione
kursywą niekoniecznie są poprawnymi konstrukcjami Object Pascala):
Procedure ChangeVariant(S:String; I:Integer);
var
V: Variant;
begin
V := Unassigned;
try
// skojarz zmienną V z łańcuchem S
V.Vtype := varString;
V.VString := S;
// zwiększ licznik odwołań łańcucha S
Inc(S.RefCount);

ShowMessage(V);

// zmniejsz licznik odwołań łańcucha S


Dec(S.RefCount)
jeśli S.RefCount = 0 to zwolnij pamięć przydzieloną dla łańcucha S

V.VType := varInteger;
V.VInteger := I;
finally
zwolnij zasoby przydzielone dla zmiennej V
end;
end;

Powyższy schemat mógłby stanowić inspirację do wykonania całego scenariusza za pomocą bezpośredniego
operowania na polach struktury TVarData:
Procedure ChangeVariant(S:String; I:Integer);
var
V: Variant;
begin
V := S;
ShowMessage(V);
TVarData(V).VType := varInteger;
TVarData(V).VInteger := I;
end;

I tu właśnie kryje się pułapka: w powyższym kodzie nie istnieje bowiem miejsce, w którym kompilator mógłby
stwierdzić, iż zerwany zostaje związek zmiennej V z łańcuchem S (struktura TVarData nie jest traktowana w
żaden szczególny sposób). W efekcie licznik odwołań łańcucha S nie zostanie zmniejszony i zarządzanie
pamięcią na jego potrzeby ulegnie pewnemu zachwianiu.
Wniosek — należy unikać operowania wprost na strukturze TVarData.

Zmienne wariantowe a rzutowanie typów


Każda zmienna o typie reprezentowalnym przez typ Variant może być w sposób jawny obsadzona w jego roli,
na przykład:
var
X : Integer;
...
ShowMessage(Variant(X));

Również wartość każdego ze wspomnianych typów może być w sposób jawny przypisana zmiennej
wariantowej, przykładowo
V := 2;
V := 1.6;
V := 'Hello';
V := TRUE;

I vice versa — zmienna wariantowa może być obsadzana w roli dopuszczalnych typów, na przykład:
V := 1.6;
S := String(V); // S zawiera wartość '1.6'
I := Integer(V); // I zawiera wartość 2 jako zaokrąglenie 1,6
// do najbliższej liczby całkowitej
B := Boolean(V); // B zawiera wartość TRUE
D := Double(V); // D zawiera wartość 1.6

lecz i to nie jest konieczne, ponieważ powyższe konstrukcje można by równie dobrze zapisać jako:
V := 1.6;
S := V; // S zawiera wartość '1.6'
I := V; // I zawiera wartość 2 jako zaokrąglenie 1,6
// do najbliższej liczby całkowitej
B := V; // B zawiera wartość TRUE
D := V; // D zawiera wartość 1.6
Zmienne wariantowe w wyrażeniach
Zmienne zadeklarowane jako Variant mogą być argumentami następujących operatorów: +, –, =, *, /, div,
mod, shl, shr, and, or, xor, not, :=, <>, <, >, <= i >=. Znaczenie danego operatora może być uzależnione
od bieżącej zawartości zmiennych wariantowych stanowiących jego argumenty — np. operator + może
oznaczać dodanie dwóch liczb albo konkatenację łańcuchów. Jeżeli argumenty operacji różnią się pod
względem typu, Delphi przeprowadza konwersję na wspólny typ, którym jest typ „silniejszy” — ranking
„siły” poszczególnych typów przedstawia się następująco:
• double

• integer

• string
Może to niekiedy prowadzić do zaskakujących rezultatów (zaskakujących, jeśli nie zna się powyższej reguły).
Spójrzmy na poniższy przykład:

var
V1, V2, V3 : Variant;
begin
V1 := '100'; // łańcuch
V2 := '50'; // łańcuch
V3 := 200; // liczba całkowita
V1 := V1 + V2 + V3;
end;

Po wykonaniu powyższej sekwencji wartością zmiennej V1 jest nie 350 (jak mogłoby się niektórym wydawać),
lecz 10250.0. Istotnie: pierwsza operacja — V1 + V2 — jest konkatenacją łańcuchów, a jej wynikiem jest
'10050'. Kolejna operacja jest dodawaniem łańcucha '10050' do liczby 200 — zgodnie z przedstawionym
rankingiem łańcuch '10050' konwertowany jest na liczbę całkowitą 10050, ta zaś dodawana jest do liczby
(całkowitej) 200, co daje w wyniku (uwaga!) liczbę rzeczywistą4 (double) 10250.0.
Oczywiście nie każda operacja na zmiennych wariantowych jest wykonalna. W poniższej sekwencji
var
V1, V2: Variant;
begin
V1 := 77;
V2 :='Hello';
V1 := V1 / V2;
end;

Delphi spróbuje skonwertować zawartość zmiennej V2 na postać liczbową (integer lub double), co
oczywiście jest niewykonalne; w efekcie otrzymamy wyjątek EVariantError z komunikatem Invalid variant
type conversion5.
Niekiedy celowe może okazać się jawne konwertowanie zawartości zmiennej wariantowej na wskazany typ.
Operacja ta sprawia, że kod wynikowy jest bardziej zwięzły i poprawia efektywność jego wykonywania —
poniższa sekwencja
V4 := V1 * V2 / V3;

jest mniej efektywna od


V4 := Integer(V1) * Double(V2) / Integer(V3);

Należy także zauważyć, iż w drugim przypadku mamy do czynienia z zaokrągleniami zawartości V1 i V3.

4
Mogłoby się wydawać, iż wspólnym typem powinien być w tym przypadku integer i wynik powinien być liczbą
całkowitą. Nieprzypadkowo jednak wspólnym typem łańcucha i liczby całkowitej jest double, nie integer — w
przeciwnym razie niewykonalne byłyby tak oczywiste obliczenia, jak np. dodanie łańcucha '3.7' do liczby 2 (wynik
takiego dodawania to liczba 5.7) (przyp. tłum.).
5
Sytuacja nie zmieniłaby się, gdyby zamiast operatora / wystąpił operator +; wbrew pozorom Delphi nie zinterpretowałoby
prezentowanej operacji jako konkatenacji łańcuchów, bowiem wspólnym typem łańcucha i liczby całkowitej jest double,
nie string (przyp. tłum.).
Jest rzeczą oczywistą, że zmienne wariantowe są wyraźnym odstępstwem od zasady bezpieczeństwa typów. To
prawda, jednak bez nich wykorzystanie mechanizmu OLE byłoby jedynie iluzją. Zresztą, są one również
użyteczne w zastosowaniach o wiele bardziej banalnych, na przykład:
Var
V1, V2 : Variant;
L : Word;
.............

if L < 0 Then
begin
V1 := 'brakuje ';
V2 := L;
end
Else if L > 0 then
begin
V1 := 'nadmiar ';
V2 := L;
end
else
begin
V1 := 'nie brakuje ';
V2 := 'żadnych';
end;
V1 := V1 + V2 + ' pozycji';

Po wykonaniu powyższego fragmentu, zmienna V1 zawiera łańcuch stanowiący czytelny raport na temat
ewentualnych braków czy nadmiaru w wykazie.

Wartości UNASSIGNED i NULL


Spośród wielu możliwych wartości, jakie przyjmować może zmienna wariantowa, dwie zasługują na
obszerniejsze omówienie. Pierwszą z nich jest UNASSIGNED, oznaczająca, że zmienna wariantowa nie
reprezentuje aktualnie żadnej wartości; jest ona nadawana przez Delphi automatycznie każdej zmiennej
wariantowej rozpoczynającej swój „czas życia”. Odpowiada jej wartość varEmpty pola VType.
Drugą ze wspomnianych wartości jest NULL, oznaczająca wartość pustą i reprezentowana w polu VType przez
wartość varNull.
Rozróżnienie pomiędzy tymi dwiema wartościami jest istotne między innymi w sytuacji, gdy tabela bazy danych
zawiera pole typu Variant — szerzej zajmiemy się tym zagadnieniem w dalszej części niniejszego tomu. Inną
ważną cechą odróżniającą wartości UNASSIGNED i NULL jest rezultat użycia zmiennej wariantowej w wyrażeniu:
próba użycia jako operandu zmiennej wariantowej o wartości UNASSIGNED spowoduje wyjątek, natomiast war-
tość NULL posiada własność „propagacji” — wartość wyrażenia, którego chociaż jeden operand ma wartość
NULL, równa jest NULL6.

Ostrzeżenie

Mimo wielkiej użyteczności zmiennych wariantowych — wykorzystuje je biblioteka VCL, korzystają z nich
kontrolki ActiveX — ich elastyczność stanowi jednocześnie pułapkę dla wygodnego programisty. Wrażenie,
że deklarowanie zmiennych jako Variant wszędzie, gdzie tylko się da, ułatwi mu życie, jest złudne; gdy
przyjdzie do testowania programu, prawdopodobne trudności w znalezieniu przyczyny ewentualnego błędu
stanowić będą zbyt wysoką cenę za wygodnictwo (i, być może, fałszywie pojętą elastyczność kodu). Ponadto,
ze względu na znacznie bardziej skomplikowany sposób obsługi zmiennych wariantowych, wydłuża się kod
programu i spada jego ogólna efektywność. Zalecamy więc rozsądnie używać zmiennych wariantowych.

Tablice wariantowe
Wspominaliśmy przed chwilą, iż jedną z wartości reprezentowanych przez zmienną wariantową może być
wskazanie na (być może heterogeniczną) tablicę. Poniższy fragment programu
var
V: variant;

6
Chyba że drugi operand posiada wartość UNASSIGNED (przyp. tłum.).
I, J : Integer;
begin
J := 1
I := V[J];
...

jest syntaktycznie poprawny i kompiluje się bezbłędnie, ale próba jego wykonania skończy się niepowodzeniem,
ponieważ zmienna V nie reprezentuje aktualnie żadnej tablicy. W celu utworzenia tablicy wariantowej można
skorzystać z jednej z dwu przeznaczonych do tego funkcji Object Pascala: VarArrayCreate() lub
VarArrayOf().

Funkcja VarArrayCreate()
Funkcja VarArrayCreate() deklarowana jest w module System w taki oto sposób:
Function VarArrayCreate( const Bounds: array of Integer; VarType: Integer):
Variant;

Funkcja ta tworzy tablicę wariantową na podstawie zadanych par indeksów granicznych i wskazanego typu
elementów. Pary indeksów granicznych są zawartością tablicy przekazanej jako pierwszy parametr (notabene
parametr ten stanowi przykład tablicy otwartej — tablicami otwartymi zajmiemy się w dalszej części rozdziału),
natomiast drugi parametr identyfikuje typ elementów tablicy (w konwencji wartości wpisywanych w pole
VType struktury TVarData).

Oto prosty przykład utworzenia jednowymiarowej tablicy o czterech elementach typu Integer:
Var
V: Variant;
begin
V := VarArrayCreate( [1 , 4], varInteger );
...
V[1] := 1;
V[2] := 3;
V[3] := 5;
V[4] := 7;
...

Z kolei poniższy przykład ilustruje tworzenie macierzy jednostkowej o wymiarze 10×10, zawierającej elementy
typu Double; na przekątnej macierzy wpisywane są jedynki, poza przekątną — zera:
// © A.Grażyński
Const
VDim = 10;
Var
V: Variant;
i, j : Integer
begin
V := VarArrayCreate( [1 , VDim, 1, VDim], varDouble );

For i := 1 to VDim do
begin
For j := 1 to VDim do
begin
V[i, j] := (I div J) * (J div I); // 1, gdy i=j, 0 gdy i<>j
end;
end;
end;

Oczywiście nic nie stoi na przeszkodzie, aby elementy tablicy same były zmiennymi wariantowymi, w
szczególności — zawierały wskazanie na tablice wariantowe! Umożliwia to tworzenie tablic wyższego rzędu,
posiadających ciekawą cechę nieortogonalności. Tablicę nazywamy ortogonalną, jeśli da się ona przedstawić jako
wektor zmiennych jednakowego typu — i tak, np. tablica array [ 1 .. 10 ] of real jest 10-elementowym
wektorem zmiennych typu real, macierz array [ 1 .. 5, 2 .. 20 ] of integer może być
rozpatrywana bądź jako 5-elementowy wektor tablic array [ 2 .. 20 ] of integer, bądź 19-elementowy
wektor tablic array [ 1 .. 5 ] or integer. Ogólnie rzecz biorąc, każda „zwykła” tablica pascalowa jest
tablicą ortogonalną, lecz tablice wariantowe wcale nie muszą posiadać tej cechy. Oto prosty przykład — poniższa
sekwencja tworzy trójkątną tablicę stanowiącą „górny” trójkąt macierzy 10×10 elementów typu Double:
// © A.Grażyński
var
V : Variant;
i : integer;
const
VDim = 10;

begin
V := VarArrayCreate ( [1 , VDim], varVariant);
for i := 1 to VDim do
begin
V[i] := VarArrayCreate ( [1 , VDim-i+1], varDouble);
end;

Funkcja VarArrayOf()
Funkcja VarArrayOf() deklarowana jest w module System następująco:
Function VarArrayOf(const Values: array of Variant): Variant;

i — jak łatwo się domyślić — służy do zgrupowania w jednowymiarową tablicę wariantową wartości
stanowiących kolejne elementy wektora podanego jako parametr. Po wykonaniu poniższej instrukcji
V := VarArrayOf( [ 1, 'Delphi', 2.2] );

V[1] zawiera liczbę całkowitą 1, V[2] zawiera łańcuch 'Delphi', natomiast V[3] jest liczbą rzeczywistą o
wartości 2.2. Tablica utworzona w ten sposób może więc być tablicą heterogeniczną, tj. posiadającą elementy
różnych typów.

Procedury i funkcje wspomagające zarządzanie tablicami wariantowymi


Oprócz opisanych funkcji VarArrayCreate() i VarArrayOf() Object Pascal oferuje kilka równie
użytecznych podprogramów związanych z tablicami wariantowymi. Zdefiniowane są one w module System —
oto ich nagłówki, następnie krótki opis:
function VarIsArray (const A: Variant): Boolean;

function VarArrayDimCount (const A: Variant): Integer;

function VarArrayLowBound (const A: Variant; Dim: Integer): Integer;

function VarArrayHighBound (const A: Variant; Dim: Integer): Integer;

procedure VarArrayRedim (var A : Variant; HighBound: Integer);

function VarArrayRef (const A: Variant): Variant;

function VarArrayLock (const A: Variant): Pointer;

procedure VarArrayUnlock (const A: Variant);

Funkcja VarIsArray() dokonuje prostego sprawdzenia, czy przekazany parametr jest tablicą wariantową:
function VarIsArray(const A: Variant): Boolean;
begin
Result := TVarData(A).VType and varArray <> 0;
end;

Funkcja VarArrayDimCount() zwraca liczbę wymiarów tablicy, natomiast dolną oraz górną granicę każdego
wymiaru poznać można dzięki funkcjom VarArrayLowBound() i VarArrayHighBound().
Procedura VarArrayRedim() umożliwia zmianę górnej granicy najwyższego w hierarchii wymiaru tablicy —
to ten wymiar, który identyfikowany jest przez ostatni (skrajny, prawy) indeks. Istniejące elementy tablicy
zostają zachowane, ewentualne nowe elementy otrzymują wartości o reprezentacji zerowej.
Funkcja VarArrayRef() otrzymując tablicę wariantową, tworzy zmienną wariantową zawierającą wskazanie
na tę tablicę. Ta dziwna na pozór czynność podyktowana została potrzebami wynikającymi z użytkowania
serwerów automatyzacji OLE, które wymagają tablicy wariantowej w takiej właśnie postaci.
function VarArrayRef(const A: Variant): Variant;
begin
if TVarData(A).VType and varArray = 0
then
Error(reVarNotArray);
_VarClear(Result);
TVarData(Result).VType := TVarData(A).VType or varByRef;
if TVarData(A).VType and varByRef <> 0
then
TVarData(Result).VPointer := TVarData(A).VPointer
else
TVarData(Result).VPointer := @TVarData(A).VArray;
end;

Jeżeli więc, na przykład, VA oznacza tablicę wariantową, to wywołanie dowolnej funkcji API związanej z
serwerem powinno mieć postać
Server.PassvariantArray(VarArrayRef(VA));

Istnienie funkcji VarArrayLock() i VarArrayUnlock() podyktowane jest pewnym subtelnym aspektem


efektywnościowym, który postaramy się zilustrować na prostym przykładzie. Załóżmy, iż chcemy skopiować
zawartość wektora bajtów składającego się z 10000 elementów do nowo utworzonej tablicy wariantowej, na
przykład w tak oczywisty sposób:
var
V: Variant;
A: Array [ 1 .. 10000] of byte;
....
V := VarArrayCreate([1, 10000], VarByte);
for i := 1 to 10000 do
V[i] := A[i];

Następuje tutaj wykonanie kolejno 10000 przypisań, z których każde jest dość kosztowne, wymaga bowiem
wykonania dosyć skomplikowanych kontroli i obliczeń, wynikających m.in. ze złożonej struktury samej tablicy
V. Okazuje się, iż możliwa byłaby znaczna redukcja tych operacji, gdyby założyć, iż w trakcie owych 10000
przypisań tablica nie zmienia swej struktury ani położenia w pamięci. Taką właśnie rolę pełni funkcja
VarArrayLock() — „zablokowuje” tablicę w tym sensie, iż do czasu jej „odblokowania” za pomocą funkcji
VarArrayUnlock() niedopuszczalne jest wywołanie w stosunku do niej funkcji VarArrayRedim().
Operowanie na zablokowanej tablicy wariantowej redukuje znacznie liczbę wykonywanych weryfikacji i tym sa-
mym zwiększa ogólną efektywność programu. Dodatkową użyteczną informację niesie wynik funkcji
VarArrayLock(): jest on wskaźnikiem do fizycznego wektora elementów w pamięci operacyjnej, dzięki czemu
możliwe jest wykonywanie pewnych operacji „na skróty” — w tym konkretnym przypadku kopiowanie
elementów może zostać wykonane przez jedno wywołanie procedury Move():
var
V: Variant;
A: Array [ 1 .. 10000] of byte;
P: Pointer;
....

V := VarArrayCreate([1, 10000], VarByte);


P := VarArrayLock(V);
try
Move(A, P^, 10000);
finally
VarArrayUnlock(V);
end;

Wykorzystując funkcję VarArrayLock() w stosunku do wielowymiarowej tablicy wariantowej, musimy


pamiętać, iż fizyczna tablica wskazywana przez wynik tej funkcji posiada strukturę wymiarów odwróconą w
stosunku do tablicy oryginalnej — innymi słowy, w poniższym przykładzie
V := VarArrayCreate([1, 100, 2, 50, 6, 30], VarByte);
P := VarArrayLock(V);

wskaźnik P powinien być traktowany tak, jak gdyby wskazywał na tablicę postaci
array [ 6 .. 30, 2 .. 50, 1 .. 100 ] of byte

Inne podprogramy związane ze zmiennymi wariantowymi


Procedura VarClear() dokonuje „wyczyszczenia” zmiennej wariantowej poprzez wpisanie w jej pole VType
wartości varEmpty.
Procedura VarCopy() kopiuje zawartość zmiennej wariantowej:
procedure VarCopy(var Dest: Variant; const Source: Variant);

Procedura VarCast() dokonuje konwersji zawartości zmiennej wariantowej na wskazany typ, zapisując wynik
w innej zmiennej wariantowej:
procedure VarCast(var Dest: Variant; const Source: Variant; VarType: Integer);

Funkcja VarType() zwraca typ zmiennej wariantowej, a dokładniej — zawartość pola VType struktury
TVarData „nałożonej” na tę zmienną:
function VarType(const V: Variant): Integer;
asm
MOVZX EAX,[EAX].TVarData.VType
end;

Funkcja VarAsType() jest bliźniaczą siostrą procedury VarCast(), zwraca bowiem zmienną wariantową
stanowiącą rezultat konwersji argumentu na zadany typ:
function VarAsType(const V: Variant; VarType: Integer): Variant;
begin
_VarCast(Result, V, VarType);
end;

Funkcja VarIsEmpty() dokonuje sprawdzenia, czy zmienna wariantowa jest zainicjowana:


function VarIsEmpty(const V: Variant): Boolean;
begin
with TVarData(V) do
Result := (VType = varEmpty) or ((VType = varDispatch) or
(VType = varUnknown)) and (VDispatch = nil);
end;

Podobne zadanie spełnia funkcja VarIsNull(), sprawdzająca, czy zmienna wariantowa reprezentuje wartość
NULL:
function VarIsNull(const V: Variant): Boolean;
begin
Result := TVarData(V).VType = varNull;
end;

Funkcja VarToStr() tworzy znakową reprezentację zmiennej wariantowej, przy czym wartości varNull
odpowiada łańcuch pusty:
function VarToStr(const V: Variant): string;
begin
if TVarData(V).VType <> varNull
then
Result := V
else
Result := '';
end;

Zwróć uwagę na ciekawy fakt, iż zasadnicza konwersja dokonywana jest tu automatycznie przez podprogramy
biblioteki RTL, uruchamiane w wyniku pojedynczej instrukcji przypisania
Result := V;

Wreszcie, funkcje VarFromDateTime() i VarToDateTime() dokonują konwersji pomiędzy zmiennymi


wariantowymi a wskazaniami daty/czasu:
function VarFromDateTime(DateTime: TDateTime): Variant;
begin
_VarClear(Result);
TVarData(Result).VType := varDate;
TVarData(Result).VDate := DateTime;
end;

function VarToDateTime(const V: Variant): TDateTime;


var
Temp: TVarData;
begin
Temp.VType := varEmpty;
_VarCast(Variant(Temp), V, varDate);
Result := Temp.VDate;
end;

Typ OLEvariant
Typ ten jest niemal identyczny z typem Variant — różnica sprowadza się do niemożności reprezentowania
przez jego zmienne typów niekompatybilnych z mechanizmem automatyzacji OLE. Wyjątkiem jest typ
AnsiString, reprezentowany przez wartość varString w polu VType — przypisanie go do zmiennej typu
OleVariant spowoduje uprzednią jego konwersję na typ BSTR, w wyniku czego pole VType posiadać będzie
wartość varOleStr, zaś pole VOleStr wskazywać będzie na łańcuch typu BSTR (czyli łańcuch znaków
WideChar zakończony zerowym ogranicznikiem).

Typ Currency
Ten typ pojawił się po raz pierwszy w Delphi 2 i z założenia przeznaczony jest do przechowywania liczb
rzeczywistych reprezentujących wielkości, co do których wymagana jest bezwzględna dokładność — głównie
kwot pieniężnych. Wewnętrzną jego reprezentacją jest 64-bitowa liczba całkowita ze znakiem, zawierająca
wartość 10000 razy większą niż wartość faktycznie reprezentowana — na przykład dla liczby 4,67 wartość ta
równa jest 46700. Jest to więc typ rzeczywisty stałoprzecinkowy — notabene jedyny tego rodzaju typ w Object
Pascalu — zapewniający dokładność czterech cyfr dziesiętnych i maksymalną wartość bezwzględną (263 –
1)/ 10000 = 922337203685477.5807.
Przy przenoszeniu aplikacji z Delphi 1 wskazane jest przeanalizowanie danych i „przeprogramowanie” na postać
typu Currency danych finansowych reprezentowanych dotychczas przez typy zmiennoprzecinkowe Real,
Single, Double i Extended.

Typy definiowane przez użytkownika


Liczby całkowite, zmiennoprzecinkowe, łańcuchy itp. nie czynią jeszcze z języka narzędzia do rozwiązywania
rzeczywistych problemów programistycznych. Repertuar ten musi zostać poszerzony o typy zdefiniowane przez
użytkownika. W języku Object Pascal typy definiowane przez użytkownika mają postać tablic (arrays),
rekordów (records) i obiektów (objects), a ich definicje rozpoczynają się od słowa kluczowego Type.

Tablice
Tablice stanowią uporządkowany ciąg (a właściwie — wektor) zmiennych tego samego typu. Typem elementu
tablicy może być dowolny typ, również zdefiniowany przez użytkownika. Poniższa deklaracja definiuje tablicę
ośmiu liczb całkowitych:
Type
Int8Arr = array [ 0 .. 7 ] of integer;

Od tej chwili typ Int8Arr staje się pełnoprawnym typem danych, a więc jest możliwe definiowanie zmiennych
tego typu:
Var
A : Int8Arr;

Powyższa deklaracja równoważna jest następującej:


Var
A : array [ 0 .. 7 ] of integer;

i odpowiada takiej oto deklaracji języka C:


int A[8]

oraz poniższej deklaracji Visual Basica


Dim A[8] of integer

W przedstawionej deklaracji poszczególne elementy tablicy identyfikowane są kolejnymi liczbami, począwszy


od zera — A[0], A[1] itd. — lecz minimalna wartość indeksu tablicy może mieć w Pascalu dowolną wartość.
Konieczność indeksowania tablicy począwszy właśnie od zera pokutuje jeszcze w C++; Visual Basic pozbył się
tego brzemienia w wersji 4.0. Przypuśćmy na przykład, że chcemy poznać liczbę piątków przypadających
trzynastego dnia miesiąca w każdym roku dwudziestego stulecia i przechować tę informację w tablicy —
poniższa deklaracja wydaje się wówczas najodpowiedniejsza:
Type
TFeralne = array [ 1901 .. 2000 ] of byte;

Indeksy tablicy odpowiadają tutaj wprost bezwzględnym numerom kolejnych lat.


Dolną i górną wartość graniczną indeksu tablicy wymiarowej zwracają funkcje Low() i High() — poniższa
sekwencja wypełnia zerami tablicę typu Double:

for i := Low(X) to High(X) do


X[i] := 0.0;

Wskazówka

Na szczególną uwagę — gdy chodzi o indeksowanie — zasługują tablice znakowe (array […] of Char);
deklarowane z zerową wartością dolnego indeksu, stają się kompatybilne z typem PChar (było tak już w wersji
7.0 Turbo Pascala). Wskazane jest zatem ich deklarowanie z zerową graniczną wartością indeksu, jeżeli nie
sprzeciwiają się temu inne względy projektowe.
Repertuar tablic w Object Pascalu nie ogranicza się do tablic jednowymiarowych. Możliwe jest deklarowanie
tablic o większej liczbie wymiarów; deklaracje poszczególnych par indeksów granicznych oddzielone są od
siebie przecinkami, na przykład:
var
G: array [ 1 .. 3, 4 .. 656, –10 .. 10 ] of byte;

Równie naturalne są odwołania do poszczególnych elementów takiej tablicy wielowymiarowej, przykładowo


K := G [2, 5, 4] + F/ G[ 1, 1, -10] * (5 + G [ 1, 1, 1]);

Tablice dynamiczne
Tablice dynamiczne pojawiły się po raz pierwszy w Delphi 4. Deklaracja tablicy dynamicznej definiuje liczbę jej
wymiarów i typ elementów, jednak nie definiuje a priori indeksów granicznych — „rozpiętości” poszczególnych
wymiarów określone zostaną dopiero w trakcie wykonywania programu. Zajmijmy się na początek jednowymia-
rowymi tablicami dynamicznymi, za chwilę natomiast uogólnimy rozważania na tablice wielowymiarowe.
Oto przykładowa deklaracja jednowymiarowej tablicy dynamicznej:
var
A: array of string;

Deklaracja ta definiuje zmienną A jako wektor łańcuchów tekstowych, nie określając jednakże rozmiaru tego
wektora. Określenie tego rozmiaru, i jednocześnie przydzielenie odpowiedniej ilości pamięci, wykonywane jest
przez funkcję SetLength():
Readln(N);
...
SetLength(A, N);

Ostatnia z powyższych instrukcji ustala rozmiar tablicy A na N elementów (biorąc oczywiście pod uwagę bieżącą
wartość zmiennej N).
Dolną wartością graniczną indeksu tablicy dynamicznej jest zawsze zero, toteż indeksami granicznymi wektora
A będą wartości 0 oraz N-1; innymi słowy, jeśli w czasie wykonania instrukcji SetLength(…) wartością N
było (powiedzmy) 5, to od tej pory tablicę A wykorzystywać można na równi ze „statyczną” tablicą
zadeklarowaną jako
array [ 0 .. 4 ] of string;

na przykład w taki sposób:


A[1] := 'Jestem już pełnoprawną tablicą';
....
Writeln(A[1], A[2]);
....
Delete(A[3], 1, Length(A[4]))

Opóźniona deklaracja wielkości wymiaru (wymiarów) tablicy dynamicznej nie jest jednakże jedyną istotną
cechą odróżniającą ją od tablic „statycznych”. Jej specyfika wiąże się również z dynamicznym przydziałem
pamięci, dokonującym się dopiero w momencie wywołania procedury SetLength(); fizyczną reprezentacją
zmiennej określającej tablicę dynamiczną (w tym wypadku — zmiennej A) jest wskaźnik.
Tablice dynamiczne należą ponadto do zmiennych o kontrolowanym czasie życia. Oznacza to, że po zakończeniu
czasu życia tablicy dynamicznej (która jest np. zmienną lokalną funkcji/procedury) przydzielona do niej pamięć
jest automatycznie zwalniana (ang. garbage-collected). Możemy też wymusić wcześniejsze wykonanie tej
czynności, podstawiając pod zmienną tablicową wartość NIL:
A := NIL; // zwolnienie pamięci przydzielonej dla tablicy dynamicznej A

Jest to zalecane szczególnie w odniesieniu do dużych tablic dynamicznych, których zawartość przestała już być
potrzebna.
Innym mechanizmem charakterystycznym dla tablic dynamicznych jest oszczędność gospodarowania pamięcią
na podstawie licznika odwołań (podobnie jak w przypadku łańcuchów AnsiString). Dwie tablice dynamiczne
o identycznej zawartości mają w rzeczywistości wspólną reprezentację pamięciową, a fakt jej współdzielenia jest
odzwierciedlany przez wartość licznika odwołań równą 2. Z tego faktu wynika pewna niespodzianka.
Przyjrzyjmy się poniższemu fragmentowi
var
A1, A2 : array of Integer;
begin
SetLength(A1, 4);
A2 := A1;
A1[0] := 1;
A2[0] := 26;
...
i zgadnijmy, co kryje się pod elementem A1[0]?
Poprawna odpowiedź brzmi: 26. Otóż przypisanie A2 := A1 jest de facto utożsamieniem tablic A1 i A2, a
wspomniana instrukcja dokonuje tylko przepisania wskaźnika oraz zwiększenia licznika odwołań. Każda zmiana
w obrębie tablicy A1 skutkować będzie identyczną zmianą w obrębie tablicy A2 i vice versa — ergo: przypisanie
A2[0] := 26 ustala wartość elementu A1[0] na 26.
Możliwe jest jednakże faktyczne powielenie tablicy dynamicznej — do tego celu służy funkcja standardowa
Copy(). Po wykonaniu poniższej sekwencji
var
A1, A2 : array of Integer;
begin
SetLength(A1, 4);
A2 := Copy(A1);
A1[0] := 1;
A2[0] := 26;

wartość A1[0] będzie równa 17.


Możliwe jest powielenie jedynie wybranego fragmentu tablicy źródłowej. Instrukcja
A2 := Copy (A1, 2,2);

wycina z tablicy A1 elementy A1[2] i A1[3], tworząc z nich zawartość tablicy A2 — identycznie do poniższej
sekwencji:
SetLength(A2,2);
A2[0] := A1[2];
A2[1] := A1[3];

Wielowymiarowe tablice dynamiczne deklaruje się poprzez zagnieżdżanie klauzuli array of; oto przykład
tablicy dwuwymiarowej:
var
B: array of array of Integer;

Wywołanie procedury SetLength() musi oczywiście uwzględniać liczbę wymiarów, na przykład:


SetLength(B, 5, 7)

nadaje tablicy dynamicznej B strukturę


array [ 0 .. 4, 0 .. 6 ] of integer;

Należy w tym miejscu zaznaczyć, iż możliwe jest tworzenie nieortogonalnych tablic dynamicznych (pojęcie
ortogonalności tablicy wyjaśnione zostało przy opisie tablic wariantowych). Jest taka możliwość, gdyż macierz
może być rozpatrywana jako wektor wektorów, które w przypadku tablicy dynamicznej (i tablic wariantowych)
nie muszą być identyczne. Poniższy przykład przedstawia tworzenie trójkątnej macierzy łańcuchów:
var
A : array of array of string;
I, J : Integer;
begin
SetLength(A, 10);
for I := Low(A) to High(A) do
begin
SetLength(A[I], I);
for J := Low(A[I]) to High(A[I]) do
A[I,J] := IntToStr(I) + ',' + IntToStr(J) + ' ';
end;
end;

Rekordy
Rekord — w przeciwieństwie do tablicy — nie ma charakteru struktury jednorodnej, lecz stanowi agregat
potencjalnie różnych typów. Odpowiednikiem pascalowego rekordu są: struktura języka C definiowana za
pomocą słowa kluczowego struct oraz typ definiowany (user-defined type) Visual Basica. Oto przykład
rekordu w języku Object Pascal oraz jego odpowiedniki w C i Visual Basicu:

7
Podobny efekt dał o sobie znać w momencie, gdy autorzy Delphi 1 zadecydowali, iż wszelkie obiekty reprezentowane będą
w programie przez wskaźniki do fizycznej reprezentacji. Od tej pory zwykła instrukcja przypisania pomiędzy zmiennymi
obiektowymi powoduje jedynie powielenie wskaźnika do istniejącego obiektu, zaś fizyczne powielenie reprezentacji
wykonywane jest w sposób jawny przez metodę Assign() (przyp. tłum.).
{ Pascal }
Type
MyRec = Record
i : integer;
d : double
end;

/* C */
typedef struct {
int i;
double d;
} MyRec;

' Visual Basic


Type MyRec
i As Integer
d As Double
End Type

Składowe rekordu nazywane są jego polami (fields), a odwołania do nich mają postać odwołań kwalifikowanych
— po nazwie zmiennej następuje kropka rozdzielająca i nazwa pola:
var
N : MyRec;
begin
N.i := 23;
N.d := 3.4;
end;

Aby uniknąć żmudnego powtarzania nazwy zmiennej (w odwołaniach kwalifikowanych), można użyć tzw.
instrukcji wiążącej with, powodującej, że odwołania do pól rekordu dotyczą konkretnej zmiennej. Oto
poprzedni przykład po zastosowaniu instrukcji wiążącej:
var
N : MyRec;
begin
with N do
begin
i := 23;
d := 3.4;
end;
end;

Rekord pascalowy może posiadać tzw. część zmienną, zwaną również częścią wariantową (uwaga: nie mylić ze
zmiennymi typu Variant!). Interpretacja części zmiennej rekordu może odbywać się na jeden ze
zdefiniowanych z góry sposobów. Znawcy języka C natychmiast rozpoznają w tym odpowiednik unii (union).
Oto przykład rekordu z częścią zmienną oraz jego odpowiednik w C++:
Type
TVariantRecord = record
NullStrField : PChar;
IntField : Integer;
Case Integer of
0 : (D: Double);
1 : (I: Integer);
2 : (C: Char);
End;

struct TUnionStruct
{
char * StrField;
int IntField;
union
{
double D;
int I;
char C;
};
};

Zgodnie z powyższą definicją, pola D, I oraz C zajmują ten sam obszar pamięci.
Część zmienna rekordu musi wystąpić na jego końcu. Nie ma przeciwwskazań, by w części zmiennej pojawiło
się pole będące rekordem zawierającym także część zmienną.

Wskazówka
Reguły Object Pascala zabraniają definiowania w zmiennej części rekordu pól będących zmiennymi o
kontrolowanym czasie życia.

Zbiory
Zbiory (sets) są konstrukcją unikatową, właściwą jedynie Pascalowi (chociaż C++Builder implementuje klasę-
szablon Set emulującą zbiory pascalowe). Zbiory oferują wyjątkowo efektywny mechanizm reprezentowania
kolekcji złożonych z elementów typów porządkowych, znakowych lub wyliczeniowych. Zbiory deklaruje się za
pomocą klauzuli set of, na przykład
type
TCharSet = set of Char;

definiuje zbiór znaków typu Char.


Oto inny przykład zbioru, zawierającego dni tygodnia:
Type
TWeekDays = (Ni, Pn, Wt, Sr, Cz, Pt, So);
// typ wyliczeniowy
WeekDaysSet = set of TWeekDays;
// zbiór oparty na typie wyliczeniowym

I jeszcze jeden rodzaj deklaracji — deklaracje zbiorów opartych na typie okrojonym:


DigitsSet = set of 0 .. 9;
Litery = 'A' .. 'z';

Liczba elementów zbioru nie może przekraczać 256, natomiast numery porządkowe jego elementów (Ord())
nie mogą wykraczać poza przedział 0 ÷ 255. Z tego względu poniższe deklaracje są błędne:
TShortIntSet = Set of ShortInt;
// Ord(Low(ShortInt)) < 0

TIntSet = set of Word;


// więcej niż 255 elementów

TStringSet = set of String;


// "String" nie jest typem porządkowym

Każdy kandydat na element zbioru reprezentowany jest przez pojedynczy bit: jedynka oznacza przynależność do
zbioru, zero — brak elementu w zbiorze. Rozmiar zmiennej zbiorowej zależny jest więc od liczności (mocy)
typu, na bazie którego zbiór zdefiniowano — nie przekracza on więc nigdy 32 bajtów. W szczególności, zbiory
oparte na typach o mocy nie przekraczającej 32 elementów cechują się szczególną efektywnością — ich zmienne
nie przekraczają rozmiaru czterech bajtów, mogą więc być w całości ładowane do rejestrów procesora.
Stałe oznaczające zbiory zapisuje się w nawiasach prostokątnych jako ogranicznikach, na przykład:
Var
Robocze, Parzyste, Wolne, Happy: WeekDaysSet;

...

Robocze := [ Pn .. Pt ];
Parzyste := [ Wt, Cz, So ];
Wolne := [ So, Ni ];
Happy := [];

Konstrukcja [] oznacza zbiór pusty.

Operatory zbiorowe
Ideą typu zbiorowego jest odwzorowanie algebry zbiorów, co znajduje odzwierciedlenie w zestawie właściwych
temu typowi operatorów.

Relacje przynależności do zbioru i zawierania zbiorów


Obecność danego elementu w zbiorze testowana jest za pomocą operatora in. Oto proste przykłady:
var
C: Char;
I: Integer;
const
Digits = [ '0' .. '9' ];
....
if not (C in Digits) // czy C nie jest cyfrą?
Then
SignalError();

if i in [ 1 .. 127, 255 ] Then


begin
....
end;

Do testowania relacji zawierania zbiorów służy operator <=. Zbiór A zawiera się w zbiorze B (co oznaczamy A
<= B), jeżeli każdy element zbioru A jest jednocześnie elementem zbioru B (niekoniecznie na odwrót).
var
X1, X2 : WeekDaysSet;
...

if X1 <= X2
Then
.....

Suma i różnica zbiorów


Sumą zbiorów A i B — oznaczaną A + B — jest zbiór tych elementów, które należą do przynajmniej jednego z
nich. Różnicę zbiorów A i B, oznaczaną A – B, tworzą wszystkie te elementy, które należą do zbioru A i
jednocześnie nie należą do zbioru B. Oto przykłady:
Var
A, B, C: set of Char
....
A := B + ['0'];
...
C := A - B + [' ', '+', '–'];

Szczególnym przykładem tworzenia sumy zbiorów może być dołączanie elementu do zbioru:
var
C : Char;
ObtainedChars : Set of Char;
....

ObtainedChars := [];
......

ObtainedChars := ObtainedChars + C;

i analogicznie — tworząc różnicę możemy wyłączyć ze zbioru pojedynczy element:


var
C: Char;
ReservedChars : Set of Char;
ReservedChars := [#0 .. #255];
....

ReservedChars := ReservedChars - C;

Począwszy od wersji 7.0 Turbo Pascala dostępne są procedury Include() i Exclude() dokonujące dołączania
elementu do zbioru i wykluczania z niego elementu:
Include(ObtainedChars, C);
// to samo co:
// ObtainedChars := ObtainedChars + C;

oraz

Exclude(ReservedChars, C);
// to samo co:
// ReservedChars := ReservedChars - C;

Wskazówka

Należy używać procedur Include() i Exclude() wszędzie tam, gdzie jest to możliwe. Są one bowiem
realizowane w sposób niezwykle efektywny — za pomocą pojedynczej (!) instrukcji procesora, natomiast
realizacja sumy (różnicy) zbiorów wymaga 13 + 6n instrukcji (n oznacza tu liczbę bitów zajmowanych przez
zbiór).
Iloczyn zbiorów
Iloczyn zbiorów A i B — oznaczany A * B — tworzą te elementy, które należą jednocześnie do obydwu
zbiorów. Oto przykładowy test, czy dwa zbiory posiadają wspólne elementy:
var
A, B : set of integer;
...

if A * B <> []
Then
.....

Obiekty
Obiekty stanowią zasadniczy trzon Delphi oraz języka Object Pascal — w szczególności obiektami są wszystkie
komponenty wizualne. Obiekty podobne są do rekordów, mogą jednak dodatkowo zawierać — jako składowe
— procedury i funkcje. Ta uproszczona definicja nie oddaje w pełni sensu obiektu pascalowego (obiektom
poświęcony jest obszerny fragment w dalszej części tego rozdziału), jednak w tym miejscu interesują nas jedynie
podstawy składniowe obiektu jako jednego z elementów języka Object Pascal.
Ogólna postać definicji obiektu jest następująca:
Type
TPochodny = class(TMacierzysty)
JakiesPole : integer;
Procedure JakasProcedura;
End;

Choć obiekty C++ różnią się od obiektów Delphi, to ich deklaracja jest trochę podobna:
class TPochodny : public TMacierzysty {
int JakiesPole;
void jakasProcedura()
}

Procedury oraz funkcje stanowiące składowe obiektu nazywane są jego metodami (methods). Wewnątrz
deklaracji typu obiektowego znajduje się jedynie nagłówek metody, który musi być rozwinięty w tekście
programu; kompletna definicja metody zawiera — oprócz jej nazwy — nazwę typu obiektowego, którego
składową stanowi:
Procedure TPochodny.JakasProcedura;
begin
{ treść metody }
end;

Kropka stanowiąca separator odwołania kwalifikowanego podobna jest do operatora :: języka C oraz operatora
. (kropki) Visual Basica. Mimo iż wszystkie trzy języki posiadają obsługę klas, jedynie Object Pascal i C++
umożliwiają definiowanie nowych klas w sposób całkowicie zgodny z kanonami programowania obiektowego
(powrócimy do tej kwestii w dalszej części rozdziału).

Notatka

Obiekty języka C++ zostały zrealizowane w zupełnie inny sposób niż obiekty języka Object Pascal; ich
łączenie w ramach jednej aplikacji możliwe jest tylko w wyniku zastosowania specjalnych zabiegów (więcej
szczegółów znaleźć można w rozdziale 13. „Zaawansowane techniki programistyczne” książki „Delphi 4.
Vademecum profesjonalisty”). Wyjątkiem od tej zasady są obiekty C++Buildera deklarowane z użyciem dyrek-
tywy __declspec(delphiclass). Są one jednak niekompatybilne z „regularnymi” obiektami C++.

Wskaźniki
Wskaźniki (pointers) stanowią wskazanie na zmienną, znajdującą się w pamięci. Przykładem typu
wskaźnikowego jest poznany już typ PChar, stanowiący wskazanie na ciąg znaków (bądź na pierwszy znak
ciągu, zależnie od kontekstu). Każdy typ posiada swój odpowiednik wskaźnikowy (regułę tę należy stosować
rekurencyjnie — istnieją wskaźniki do wskaźników). W Pascalu występuje również tzw. wskaźnik amorficzny
(untyped pointer) stanowiący wskazanie na obszar pamięci operacyjnej bez związku z konkretnym typem i
deklarowany jako Pointer. Ponieważ stanowi on jednak odstępstwo od zasady bezpieczeństwa typów, nie
powinien być nadużywany.

Wskaźniki są bardzo silnym narzędziem programistycznym. Niezwykle użyteczne w ręku doświadczonego


programisty, mogą równocześnie okazać się skrajnie niebezpieczne (dla aplikacji), gdy są niewłaściwie
używane.

Wskaźniki „typowane” (typed pointers) definiowane są za pomocą operatora ^ w połączeniu z typem


podstawowym, na który wskazują. Nie dotyczy to oczywiście wskaźników typu Pointer, gdyż nie wskazują
one na żaden typ.
Oto kilka przykładów definicji typów wskaźnikowych:
Type
PInt = ^Integer
{ typ PInt jest typem wskazującym na typ Integer }

Foo = record
Nazwisko : string;
Wiek : byte;
end;

PFoo = ^Foo;
{ Typ PFoo wskazuje na typ Foo }

var
P: Pointer {wskaźnik amorficzny}
P2: PFoo; {wskaźnik rekordu typu Foo}

Notatka

Znaczenie operatora ^ w Object Pascalu podobne jest do znaczenia operatora * w C. Odpowiednikiem


wskaźnika amorficznego (pointer) jest w C typ void *.

Należy zaznaczyć, że wartością wskaźnika jest adres pamięci zajmowanej przez odnośną zmienną. Ewentualna
alokacja wskazywanej przez wskaźnik pamięci leży całkowicie w gestii programisty. Możliwe jest uwolnienie
wskaźnika od jakiegokolwiek wskazania; wartością „nie wskazującą na nic” jest w Pascalu wartość NIL (w
Delphi 2 i następnych dodatkowo NULL). Reprezentacją takiego „pustego” wskaźnika nie są binarne zera.
Odwołanie się do wskazywanej zmiennej następuje przez użycie operatora ^ (zwanego operatorem dereferencji).
Najlepiej wyjaśnić to na przykładach:
Program PtrTest;
Type
MyRec = record
I : Integer;
S : String;
R : Real;
End;
PMyRec = ^MyRec;

Var
Rec : PMyRec;

begin
New(Rec); {tworzy dynamiczny rekord typu MyRec,
zmienna Rec zawiera wskazanie na niego}

Rec^.I := 10;
Rec^.S := 'Coś dla odmiany ... '
Rec^.R := 6. 384;

......

Dispose(Rec) ; {zwolnienie zajętej pamięci}


end;

<ramka>
Jak alokować i zwalniać pamięć?

Jeżeli przydzielasz pamięć dla zmiennej ściśle określonego typu, zawsze używaj procedury New().
Gwarantuje to przydzielenie pamięci w ilości odpowiedniej do typu struktury. Nie zapomnij o zwolnieniu tak
przydzielonej pamięci za pomocą procedury Dispose().

Zamiast procedur New() i Dispose() można oczywiście wykorzystywać procedury GetMem() i


FreeMem()8, lecz jest to mniej bezpieczne. Niekiedy jednak nie da się zastosować procedury New() i użycie
GetMem() jest konieczne — typowym przykładem jest alokacja pamięci dla ciągu znaków PChar
wykonywana przez funkcję StrNew(). Nieco bezpieczniejszą od GetMem() jest funkcja AllocMem() —
wykonuje ona dodatkowo zerowanie przydzielonego obszaru pamięci.

Odwołanie się do pamięci poza przydzielonym obszarem jest błędem i najczęściej kończy się wyjątkiem
(Access Violation), jednak na szczęście zasada bezpieczeństwa typów redukuje znacznie
prawdopodobieństwo takiego zjawiska; jego niebezpieczeństwo wzrasta jednak, gdy używamy wskaźników
amorficznych.

<ramka>
Z typami wskaźnikowymi wiąże się bardzo ciekawa osobliwość Turbo Pascala dotycząca zgodności typów
(mogąca przyprawić o ból głowy programistów przyzwyczajonych do języka C): otóż identyczna deklaracja
dwóch typów nie jest jeszcze gwarancją ich zgodności (w sensie reguł kompilatora). Oto typowy przykład:
var
a : ^integer;
b : ^integer;

begin
.....
a := b; //tu kompilator zgłosi błąd

Dla programistów „wychowanych” na języku C stanowi to nie lada zaskoczenie — wszak zgodnie z deklaracją
int *a;
int *b;

zmienne a i b są tego samego typu.


Przyczyną owej niezgodności są zaostrzone reguły zgodności typów w Pascalu — kompilator nie wchodzi w
szczegóły definicji typów i ewentualna identyczność dwóch różnych deklaracji nie ma dla niego znaczenia.
Rozwiązaniem tego problemu jest jawne zdefiniowanie typu wskazującego na typ integer:
Type
PInteger = ^Integer;
var
a : PInteger;
b : PInteger;

begin
.....
a := b; {tu wszystko jest w porządku}

Aliasy typów
Object Pascal umożliwia definiowanie typów równoważnych typom już zdefiniowanym — służy do tego prosta
dyrektywa zrównania typów, np.
Type
Numerki = Integer;

Od tej chwili typ Numerki nie będzie się dla kompilatora różnił od typu Integer.

8
Procedura New(P) jest równoważna GetMem(P, SizeOf(P^)), natomiast Dispose(P) odpowiada
FreeMem(P,SizeOf(P^). Nie dotyczy to jednak w pełni typów obiektowych, chociaż stosowanie do nich procedur
New() i Dispose() z jednym parametrem również nie jest typowe (przyp. tłum.).
Nowością Delphi, niedostępną w Turbo Pascalu są tzw. aliasy typów (strongly typed aliases) oznaczające
zgodność, lecz nie identyczność typów.
Deklaracja aliasu ma postać:
Type
nowy_typ = type typ_bazowy;

Jej konsekwencją jest wzajemna zgodność obydwu typów w sensie przypisania. Na przykład w wyniku poniższej
deklaracji
Type
Licznik = type Integer;
Var
L : Licznik;
K : Integer;

obydwa przypisania
L := K;
...
K := L;

są poprawne.
Typy Licznik i Integer nie są jednak w rozumieniu składni Object Pascala typami identycznymi, co czyni je
niezgodnymi w kontekście przekazywania do procedur i funkcji parametrów opatrzonych klauzulami var i out.
Poniższe fragmenty zostaną więc przez kompilator odrzucone:
procedure Dolicz(var X: Licznik);
begin
...
end;

Procedure Verify(var Y: Integer);


begin
...
end;

var
L: Integer;
N: Licznik;
.......

Dolicz(L); // błąd

Verify(N); // błąd

Poprawne są jednak następujące konstrukcje:


procedure Zalicz(X: Licznik);
begin
...
end;

Procedure Granica (Y: Integer);


begin
...
end;

var
L: Integer;
N: Licznik;
.......

Zalicz(L);
Granica(N);

Rozróżnialność pomiędzy typem bazowym a jego aliasem ma szczególne znaczenie w przypadku właściwości
klas, pozwala bowiem tworzyć odrębne edytory właściwości dla dwóch różnych typów o identycznej strukturze.
Zajmiemy się tą kwestią obszerniej w rozdziale 12.
Rzutowanie i konwersja typów
Rzutowanie typów (typecasting) stanowi jeden ze sposobów osłabienia rygorystycznej kontroli typów
wykonywanej przez kompilator. Nie każde naruszenie zasad bezpieczeństwa typów jest błędem — wszystko
zależy od tego, czy programista wie, co robi. Przeanalizujmy poniższy przykład:
var
c : Char;
b : Byte;

begin
...
c := 's';
b := c; { tu kompilator zaprotestuje}

Ostatnie przypisanie zostanie przez kompilator zakwestionowane, gdyż zmienne b i c są zupełnie różnych
typów. Intencją programisty było prawdopodobnie potraktowanie obszaru pamięci na dwa sposoby: raz jako
znaku, raz jako bajtu (przypomina to nieco zmienną część rekordu). Możemy to osiągnąć, nakazując
kompilatorowi, by potraktował zmienną c jak bajt9 — czyli rzutując tę zmienną na typ byte:
var
c : Char;
b : Byte;

begin
...
c := 's';
b := byte(c); { to kompilator zaakceptuje}
{ b zawiera kod znaku 's' }

Rzutowanie typów jest narzędziem bardzo silnym (a więc dla aplikacji potencjalnie niebezpiecznym), chociaż
stosowane właściwie, daje wyraźne korzyści. Jest ono w zasadzie tylko inną interpretacją bitowego wzorca
zmiennej, w szczególności — nie jest związane z żadnymi konwersjami. Niezrozumienie tego faktu prowadzi do
tworzenia bezsensownych konstrukcji, jak w poniższym przykładzie:

Var
k : integer;
s : single;
begin
s := 1.0;
k := integer(s);

W Delphi 6 ostatnia instrukcja jest niepoprawna; była ona poprawna jeszcze w Delphi 5, mimo to, nawet
wówczas naiwnością byłoby oczekiwać, że wartością zmiennej k po wykonaniu ostatniego przypisania jest 1 (w
rzeczywistości zmienna ta miałaby wartość 1065353216 — przyp. tłum.). Zmienne typu integer i single
przechowywane są w pamięci w zupełnie różny sposób i wartość ta stanowi wynik interpretacji wzorca zmiennej
typu single na modłę typu integer.
Intencją programisty było tu zapewne przekształcenie liczby zmiennoprzecinkowej na odpowiadającą jej liczbę
całkowitą. Takie przekształcenie — zwane konwersją typów — wykonywane jest w Object Pascalu w sposób
jawny, za pomocą funkcji standardowych, bądź też w sposób niejawny przez kompilator, na przykład:
Var
s : single;
k : integer;

begin
.....

s := 4.7;
k := Trunc(s); // przekształcenie jawne, k = 4
k := Round(s); // przekształcenie jawne, k = 5
.....
k := 3;
s := k; // przekształcenie ukryte, s = 3.0

Zakres i reguły przekształceń domyślnych są ściśle określone — wrócimy do tego w dalszej części książki.

9
O takiej samej reprezentacji bitowej (przyp. tłum.).
Rzutowanie typów obiektowych wymaga oddzielnego omówienia — zajmiemy się tym w dalszej części
rozdziału.

Notatka

Warunkiem koniecznym (lecz nie wystarczającym) wykonalności rzutowania typów jest identyczny rozmiar
zmiennej podlegającej rzutowaniu i typu docelowego.

Zasoby łańcuchowe
W Object Pascalu stałe tekstowe (łańcuchy) przechowywane są standardowo w kodzie aplikacji. Koncepcja ta
narodziła się jeszcze w Turbo Pascalu, a jej konsekwencją było z jednej strony odciążenie ograniczonego do 64
kB segmentu danych, z drugiej zaś — po pojawieniu się DPMI i sprzętowych mechanizmów ochrony — do-
datkowe zabezpieczenie stałych tekstowych przed modyfikacją, zamierzoną lub przypadkową. W środowisku
Win32, wobec rezygnacji z segmentowanego modelu pamięci, aktualna jest jedynie druga z wymienionych
przesłanek.
Wraz z Delphi 3 pojawił się alternatywny sposób przechowywania stałych tekstowych, mianowicie — w
zasobach łańcuchowych (string resources), umieszczanych przez kompilator w generowanym pliku zasobowym
*.RES. Nowością są oczywiście nie same zasoby łańcuchowe, lecz sposób ich integracji z kodem projektu. Otóż
łańcuchy przeznaczone do przechowania w takich szczególnych zasobach deklarowane są tak, jak zwykłe stałe
tekstowe, z tą różnicą, iż zamiast dyrektywy const występuje dyrektywa resourcestring, jak w poniższym
przykładzie:
resourcestring
HelloMsg = 'Hello';
WorldMsg = 'world';
var
String1: String;
begin
String1 := HelloMsg + ' ' + WorldMsg + '!';
Writeln(String1);
...
end;

Program wypisuje historyczne już dla Pascala powitanie Hello world!. Stałe tekstowe HelloMsg oraz WorldMsg
zdefiniowane zostały przy użyciu dyrektywy resourcestring; skutkuje to z jednej strony umieszczeniem ich
przez kompilator w generowanych zasobach i automatycznym dołączaniem tych zasobów do aplikacji, z drugiej
natomiast — automatycznym ich ładowaniem w czasie wykonania programu, za pomocą niejawnych wywołań
funkcji LoadString(). I to wszystko bez zaprzątania uwagi programisty.
Najważniejszą jednak korzyścią, płynącą z oddzielenia stałych tekstowych od kodu aplikacji, jest niewątpliwie
ułatwiona zmiana jej wersji językowej, która manifestuje się przede wszystkim w postaci wypisywanych
komunikatów (choć nie tylko). Łańcuchy deklarowane z użyciem dyrektywy resourcestring dołączane są do
pliku .EXE (lub .DLL) jako zasoby, zatem kwestia zmiany wersji językowej aplikacji sprowadza się wówczas do
wymiany tychże zasobów, bez konieczności ponownej kompilacji kodu źródłowego.

Instrukcje warunkowe
W kolejnych punktach przedstawimy dwie instrukcje warunkowe: instrukcję if oraz instrukcję wyboru case.
Zakładamy, że nie są one dla Czytelnika nowością, ograniczymy się do ich porównania z odpowiednikami w C i
Visual Basicu.
Instrukcja If
Instrukcja if umożliwia uzależnienie wykonania pewnej instrukcji od spełnienia określonego warunku. Oto
przykłady:
{ Pascal }
if x = 4
then
y := x;

/* C */
if ( x == 4 ) y = x;

' Visual Basic


if x = 4 Then y = x

Ostrzeżenie

Pamiętaj, że w Pascalu operatory boolowskie mają wyższy priorytet niż operatory porównania. Testując więc
koniunkcję czy alternatywę kilku warunków, nie zapomnij o ujęciu każdego z nich w nawiasy, na przykład:

if (x = 7) and (y = 8) Then ....

Opuszczenie nawiasów spowoduje w tym wypadku błąd kompilacji.

Instrukcja uzależniona od spełnienia warunku, zwana instrukcją uwarunkowaną, może być instrukcją złożoną;
innymi słowy, warunek może wiązać — między „nawiasami pionowymi” begin i end — kilka instrukcji, jak w
poniższym przykładzie:
if x = 6 Then
begin
Cokolwiek;
ATerazCosInnego;
IJeszczeCos;
end;

W języku C „nawiasami pionowymi” są nawiasy { i }.


Możliwe jest też testowanie całej kaskady warunków:
if x = 100
Then
FunkcjaDlaSetki
Else if x = 200
Then
SpecjalnieDla200
Else if x = 300
Then
IteracjaDla300
Else
begin
SytuacjaAwaryjna;
UstawIndeks;
end;

Instrukcja wyboru
Instrukcja ta pozwala na wykonanie jednej instrukcji z podanego zestawu, na podstawie wartości wyrażenia
testowego, zwanego selektorem. Instrukcja wyboru może być zastąpiona „kaskadową” instrukcją if, lecz jest od
niej zdecydowanie zgrabniejsza i wygodniejsza:
case x of
100:
FunkcjaDlaSetki;
200:
SpecjalnieDla200;
300:
IteracjaDla300;
Else
begin
SytuacjaAwaryjna;
UstawIndeks;
end;
end;

Notatka

Selektor w instrukcji wyboru musi być wyrażeniem typu porządkowego (ordinal type), natomiast wartości
określające poszczególne warianty muszą być stałymi. Oznacza to, że instrukcja wyboru nie nadaje się np. do
testowania wariantów łańcuchowych.

Oto odpowiednik prezentowanego przykładu w języku C:

switch (x)
{
case 100:
FunkcjaDlaSetki; break;
case 200:
SpecjalnieDla200; break;
case 300:
IteracjaDla300; break;
default:
{
SytuacjaAwaryjna;
UstawIndeks;
}
}

Pętle
Pętle służą do kontrolowanego powtarzania bloków instrukcji. Przedstawimy trzy różne rodzaje pętli w Object
Pascalu — w języku C istnieją podobne konstrukcje, w Visual Basicu są one nieco bardziej rozbudowane.

Pętla For
Typowym zastosowaniem pętli For jest wykonanie danego bloku instrukcji pewną — z góry określoną — liczbę
razy. Oto przykład obliczający sumę kwadratów pierwszych stu liczb nieparzystych:
Var
i, k, sum : integer;
begin
sum := 0;
for i := 1 to 100 do
begin
k := i + i - 1; // kolejna liczba nieparzysta
sum := sum + k * k;
end;
.....
end;

Oto równoważna konstrukcja w C:


void main(void)
{
int i, k, sum;
sum = 0;

for (i=1; i<=100; i++)

{
k = i + i - 1;
sum += k * k
}
...
}
i w Visual Basicu:
sum := 0;
For i = 1 to 100
k = i + i - 1;
sum = sum + k * k;
Next i

Notatka

Modyfikacja zmiennej sterującej pętli For, dozwolona (choć mocno problematyczna) w Delphi 1 oraz po-
przednich wersjach Pascala, począwszy od Delphi 2 jest wyraźnie zabroniona; bez tego ograniczenia
możliwości optymalizacji pętli przez kompilator byłyby mocno uszczuplone.

Pętla While...Do
Instrukcja pętli While…Do powoduje cykliczne powtarzanie instrukcji uwarunkowanej tak długo, jak długo
spełniony jest określony warunek. Warunek sprawdzany jest przed wykonaniem instrukcji uwarunkowanej, więc
możliwe jest, że instrukcja ta nie zostanie wykonana ani razu. Jednym z typowych zastosowań pętli While…Do
jest przetwarzanie kolejnych linii pliku tekstowego, aż do jego wyczerpania (warunek końca pliku Eof() jest
prawdziwy, jeżeli w pliku nie ma już żadnej linii do pobrania10).
Program CzytajTekst;

{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);

while not EOF(F) do


begin
readln(F,S);
writeln(S);
end;

closeFile(F);
end.

Pascalowa pętla While…Do funkcjonuje w sposób analogiczny do pętli While w języku C i pętli Do While w
Visual Basicu.

Pętla Repeat...Until
W przeciwieństwie do instrukcji While...Do, warunek uzależniający wykonywanie bloku instrukcji
sprawdzany jest po jego wykonaniu, więc pętla zostaje wykonana przynajmniej raz. Poniższy przykład sumuje
kwadraty kolejnych liczb nieparzystych do momentu, kiedy wynik przekroczy wartość 10000:
Program Sumowanie;
{$APPTYPE CONSOLE}
var
liczba, sum: integer;
begin
sum := 0;
liczba := -1;
repeat
Inc(liczba, 2);
Inc(sum, liczba * liczba);
until sum > 10000;
writeln('Ostatnią uwzględnioną liczbą jest ', liczba);
end.

10
a nie po nieudanej próbie odczytania linii, dlatego musi być sprawdzany przed odczytem (przyp. tłum.)
Procedura Break()
Wywołanie procedury Break wewnątrz pętli While, Repeat lub For powoduje jej natychmiastowe
zakończenie. Jeżeli pętle są zagnieżdżone, następuje zakończenie najbardziej wewnętrznej pętli zawierającej
wywołanie procedury Break.
Oto fragment programu czytający i wypisujący zawartość pliku tekstowego aż do napotkania pustej linii:
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
Repeat
Readln(F,S);
Writeln(S);
Until S = '';
CloseFile(F);
End.

Program ten jednak załamie się, jeżeli plik nie będzie zawierał pustej linii — próba wykonania instrukcji
Readln po wyczerpaniu jego zawartości spowoduje wygenerowanie wyjątku. Należy więc sprawdzać warunek
Eof(F) przed wykonaniem tej instrukcji i gdy jest prawdziwy, przerywać pętlę repeat:
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
Repeat
if Eof(f)
then
Break;
Readln(F,S);
Writeln(S);
Until S = '';
CloseFile(F);
End.

Procedura Continue ()
Wywołanie procedury Continue wewnątrz pętli While, Repeat lub For powoduje porzucenie bieżącego
„cyklu” pętli i natychmiastowe przejście do sprawdzania jej warunku. Oto zmodyfikowany poprzedni przykład
— jeżeli odczytana linia zaczyna się od średnika, nie jest w ogóle wypisywana:

Program CzytajTekst;
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);

Repeat
if EOF(F)
Then
Break;
readln(F,S);
if Copy(S,1,1) = ';'
Then
continue; // to samo co "skok" do linii Until
writeln(S);
Until S = '';

CloseFile(F);
End.

Procedury i funkcje
Procedury i funkcje stanowią wydzielone części aplikacji, wykonujące ściśle określony, zamknięty blok
instrukcji. Dodatkowo, funkcja zwraca pod swą nazwą wartość określonego typu. W języku C istnieją wyłącznie
funkcje; odpowiednikiem procedury jest funkcja, która zwraca wynik nieznaczący (void). Przykłady użycia
funkcji i procedur przedstawia wydruk 2.1.

Wydruk 2.1. Przykładowe funkcje i procedury


Program FuncProc;

{$APPTYPE CONSOLE}

Procedure Ponad10 (i : integer);


// wypisuje komunikat, jeżeli parametr jest większy od 10
begin
if i > 10
Then
Writeln ('Ponad 10.');
end;

Function Nieujemna( i : integer) : Boolean;


begin
Result := ( i >= 0 );
end;

var
Num : Integer;
begin
Num := 23;
Ponad10(Num);
if Nieujemna(Num)
Then
Writeln ('Nieujemna.')
Else
Writeln ('Ujemna.');
end.

Na specjalny komentarz zasługuje zmienna Result wykorzystana w funkcji Nieujemna. Symbolizuje ona
wartość zwracaną przez funkcję. Gdy występuje po lewej stronie operatora przypisania — jest równoważna
nazwie funkcji. Nie można jej jednak zastąpić nazwą funkcji, gdy występuje po prawej stronie operatora
przypisania: wystąpienie w tym miejscu nazwy funkcji oznacza jej wywołanie (w tym wypadku rekursywne), zaś
wystąpienie zmiennej Result — jej wartościowanie. Poniższa funkcja jest całkowicie poprawna
function SignPower(X:Real; N:Integer):Real;
var
i: integer;
Y : real;
begin
Result := 1.0;
for i := 1 to Abs(N) do
begin
Result := Result * X;
end;
if N < 0
then
Result := 1.0/Result;
end;

Jeżeli jednak zamienimy zmienną Result na nazwę funkcji, otrzymamy kod błędny syntaktycznie, bo funkcja
SignPower wywoływana jest bez parametrów:
function SignPower(X:Real; N:Integer):Real;
var
i: integer;
Y : real;
begin
SignPower := 1.0;
for i := 1 to Abs(N) do
begin
SignPower := SignPower * X; // ta instrukcja jest błędna syntaktycznie
end;
if N < 0
then
SignPower := 1.0/ SignPower; // ta instrukcja jest błędna syntaktycznie
end;

Gdybyśmy wykonali podobny zabieg z funkcją bezparametrową, program wpadłby w nieskończoną rekurencję.

Ostrzeżenie

Nie należy mylić przypisania do zmiennej Result z instrukcją return języka C — ta ostatnia, wskazując
zwracaną wartość, powoduje jednocześnie zakończenie działania funkcji (podobnie jak pascalowa instrukcja
Exit); przypisanie wartości zmiennej Result jest natomiast tylko przypisaniem i może odbywać się w ciele
funkcji wielokrotnie.

Użycie zmiennej Result dopuszczalne jest tylko wtedy, gdy ustawiony jest przełącznik $X+ (lub zaznaczona
jest opcja Extended Syntax na karcie Compiler opcji projektu).

Przekazywanie parametrów do procedur i funkcji


Od początku istnienia języka Pascal, parametry procedur i funkcji poddane były niezwykle rygorystycznym
regułom, zwiększającym co prawda bezpieczeństwo programowania, lecz jednocześnie ograniczającym w
znacznym stopniu swobodę i naturalność wyrażania koncepcji programistycznych — wiedzą o tym aż nadto
dobrze ci, którym przyszło zmagać się z różnymi wersjami Pascala poprzedzającymi Turbo Pascal. Rygoryzm
ten ulegał stopniowemu łagodzeniu w kolejnych wersjach Turbo Pascala — że przywołamy tylko tablice otwarte
i parametry amorficzne — lecz prawdziwą rewolucję przyniosły dopiero Delphi 1, oferując hybrydowe tablice
array of const, stanowiące tak naprawdę pierwszy znaczący krok w kierunku elastycznych nagłówków
procedur i funkcji, oraz Delphi 2 ze swoim typem Variant. Pisaliśmy już wcześniej o udogodnieniach
wprowadzonych w Delphi 4 — przeciążaniu i parametrach domyślnych — obecnie zajmiemy się parametrami
procedur i funkcji w sposób bardziej systematyczny.

Przekazywanie parametrów przez wartość


Przekazanie parametru przez wartość wiąże się z automatycznym utworzeniem jego lokalnej kopii, dostępnej
przez oryginalną nazwę parametru formalnego — innymi słowy, wszystkie operacje wykonywane w treści
procedury na parametrze, wykonywane są na jego kopii lokalnej, oryginalny parametr aktualny zostaje zatem
nienaruszony. Eliminuje to możliwość przypadkowego zmienienia go przez procedurę, lecz — uwaga — wiąże
się niekiedy z wykorzystaniem dość znacznego obszaru stosu (tam zostaje utworzona wspomniana kopia
lokalna). W aplikacjach 16-bitowych stanowiło to często nie lada problem, w 32-bitowych wersjach Delphi
sprawa jest może mniej dotkliwa, niemniej jednak należy mieć świadomość opisanego zjawiska. Deklaracja
parametru przekazywanego przez wartość nie zawiera żadnego dodatkowego słowa kluczowego, a jedynie
nazwę parametru i jego typ:
Procedure TakaSobie ( s : string );

Przekazywanie parametrów przez referencję


Przekazywanie parametru przez referencję, zwane również przekazaniem przez adres lub przez zmienną,
powoduje, że w treści procedury pod nazwą parametru formalnego kryje się rzeczywisty parametr aktualny, a
wszystkie operacje wykonywane są bezpośrednio na nim. Jest to więc właściwy sposób na przekazanie przez
procedurę wartości zwrotnej do programu wywołującego — po zakończeniu wykonywania funkcji procedury
wartość parametru odzwierciedla wszystkie wykonane na nim operacje.
Parametr przekazywany przez referencję musi więc posiadać zdolność do przypisywania mu wartości, więc
odpowiadającym mu parametrem aktualnym musi być L-wyrażenie, czyli np. zmienna, element tablicy lub
dereferencja wyrażenia wskaźnikowego.
Deklaracja parametru przekazywanego przez referencję polega na poprzedzeniu jego nazwy słowem kluczowym
var:
Procedure TakaSobie ( var m : integer );
begin
m := m + (m div 2);
end;
.....

var
k: integer;
begin
k := 3;
TakaSobie(k);
{ wartość k wynosi teraz 4 }
....

Zasada bezpieczeństwa typów wymaga, by parametr aktualny przekazywany przez referencję był dokładnie tego
samego typu, co odpowiadający mu parametr formalny; w przeciwnym wypadku wystąpi błąd kompilacji.
Przekazywanie parametru przez zmienną nie powoduje obciążenia stosu, na którym „odkładany” jest jedynie
wskaźnik do parametru aktualnego — gdyż nie jest sporządzana kopia tego parametru.
W języku C++ odpowiednikiem pascalowego przekazywania parametru przez referencję jest przekazywanie
referencji parametru aktualnego (z użyciem operatora &).

Przekazywanie parametrów przez stałą


Ten sposób przekazywania parametrów pojawił się w wersji 7.0 Turbo Pascala i łączy w sobie zalety dwóch
poprzednich sposobów. Z jednej strony, pod względem składniowym, parametr taki podlega tym samym
regułom, co parametr przekazywany przez wartość; jednak z drugiej strony, na stosie odkładany może być
adres11parametru,nie jego kopia — zależnie od tego, co kompilator uzna za bardziej efektywne. Ponieważ
parametr aktualny nie musi już być L-wyrażeniem, należy zatroszczyć się o jego niezmienność. Jest ona
gwarantowana na drodze syntaktycznej: kompilator nie dopuści bowiem żadnej konstrukcji mogącej zmienić
wartość parametru12.

11
Nie ma jednak gwarancji, iż do procedury (funkcji) faktycznie zostanie przekazany adres parametru — kompilator może
zastosować przekazanie przez wartość, jeśli uzna to za bardziej optymalne. Nie należy ponadto zapominać, iż parametrem
aktualnym może być w tym przypadku wyrażenie — obliczona wartość tego wyrażenia odkładana jest w tymczasowej
zmiennej roboczej i to właśnie adres tej zmiennej przekazywany jest do procedury (funkcji); może się tak stać nawet
wówczas, gdy wyrażenie to jest L-wyrażeniem (np. zmienną prostą). Jeżeli więc zdefiniujemy następującą funkcję
function AddrOfCParm(const X:SmallInt):pointer;
begin
result := @X;
end;
nie mamy gwarancji, że wywołanie AddrOfCParm(MyVar) zwróci adres zmiennej MyVar (przyp. tłum.).
12
Można jednak z łatwością oszukać kompilator, przekazując w zagnieżdżonym wywołaniu wskaźnik parametru
przekazanego przez stałą, jak w poniższym przykładzie:
Type
PMyRec = ^TMyRec;
TMyRec = record
a, b: integer
Deklaracja parametru przekazywanego przez stałą polega na poprzedzeniu jego nazwy słowem kluczowym
const:

type
TMyRecord = record
Counters: array[0..1000] of integer;
Labels: array[0..1000] of String[30];
end;

Procedure InitPivotElement ( const X : TMyRecord );
begin
with X do
begin
Counters[0] := 0; // ta instrukcja spowoduje błąd kompilacji
Labels[0] := 'Pivot'; // ta również
end;
end;

Function CountVowels(const S:ShortString):byte;


const
Vowels = ['A','E','I','O','U','Y'];
var
i: byte;
begin
Result := 0;
for i := 1 to Length(S) do
begin
if UpCase(S[i]) in Vowels
then
inc(Result);
end;
end;

var
T: ShortString;
K, L : byte;

K := CountVowels(T); // to wywołanie jest poprawne



L := CountVowels('Exportable'); // to również

Należy przyjąć zasadę, że przekazanie parametru przez stałą jest sposobem najodpowiedniejszym w przypadku,
gdy parametr jest parametrem wejściowym (tzn. nie przekazuje informacji zwrotnej) i nie zamierzamy
wykorzystywać jego kopii lokalnej jako obszaru roboczego.

Mechanizm tablic otwartych


Przekazywanie tablic do procedur i funkcji od początku stwarzało w Pascalu problemy. W oryginalnej,
wzorcowej wersji języka z lat 70., funkcja obliczająca wyznacznik macierzy o rozmiarach 3×3 była już

end;

procedure Inner(X: PMyRec);


begin
X^.a := 1;
end;

procedure Outer(const Y: TMyRec);


begin
Inner(@Y);
end;
nieodpowiednia dla macierzy 5×5, bo z punktu widzenia Pascala były to dwa różne typy danych. Pierwszym
rozwiązaniem tego problemu stał się tzw. schemat tablicy uzgadnianej, gdzie graniczne wartości indeksów
przekazywane były wraz z identyfikatorem tablicy w miejsce pojedynczego parametru formalnego. Schemat
ten nie znalazł jednak zastosowania w Turbo Pascalu i Delphi, nie będziemy się więc nim zajmować.
Innym rozwiązaniem opisanego problemu stał się mechanizm tablic otwartych, wprowadzony do Turbo
Pascala w wersji 6.0. Ogranicza się on jednak tylko do tablic jednowymiarowych — jeżeli chcielibyśmy
przekazać do procedury np. macierz, musimy ją potraktować jak tablicę wektorów.
Deklaracja tablicy otwartej sprowadza się do określenia typu jej elementów, podobnie jak w przypadku tablic
dynamicznych:
procedure KazdaTablica1 ( var X : array of integer );
procedure KazdaTablica2 ( const X : array of integer );
procedure KazdaTablica3 ( X : array of integer );

W miejsce parametru X powyższych procedur można przekazać dowolną tablicę liczb typu integer, można też
wpisać zawartość tablicy explicite, za pomocą tzw. konstruktora tablicowego, wymieniającego kolejne jej
elementy, na przykład konstruktor
[1,2,3,4,5]

definiuje pięcioelementową tablicę, której elementami są początkowe liczby naturalne13. Ponieważ z punktu
widzenia składni konstruktor tablicowy jest stałą, nie można przekazać go przez referencję.

Aby w treści procedury (funkcji) poznać wielkość tablicy przekazanej jako parametr aktualny (w miejsce
parametru formalnego będącego tablicą otwartą), można wykorzystać funkcje Low() i High(). Ale uwaga: z
punktu widzenia procedury (funkcji), jej parametr, będący tablicą otwartą, postrzegany jest jako tablica
indeksowana od zera (zero-based array), tak więc funkcja Low() zwraca dla niej zawsze 0, zaś funkcja High()
— wartość mniejszą o 1 od faktycznej liczby elementów. Poniższy fragment programu wypisuje wartości 0 i 4:


procedure WypiszRozmiary(var X:array of real);
begin
writeln(Low(X),' ',High(X));
end;


var
Vector: array [3 .. 7] of Real;

WypiszRozmiary(Vector);

Oto jeszcze jeden przykład zastosowania tablicy otwartej — poniższa funkcja oblicza średnią arytmetyczną
elementów wektora:

function RealAverage(const aVector: array of Real):Real;


var
i: integer;
begin
Result := 0.0;
for i := Low(aVector) to High(aVector) do
Result := Result + aVector[i];

Result := Result/(High(aVector)-Low(aVector)+1);
end;

13
Zwróć uwagę, iż konstruktor tablicowy może wyglądać tak samo jak zbiór (set), dlatego jedynym dozwolonym jego
zastosowaniem jest rola parametru aktualnego procedury (funkcji) — nie można użyć go w wyrażeniu ani w instrukcji
przypisania (przyp. tłum.).
Prawdziwą rewolucją w zakresie składni Pascala są z pewnością tablice heterogeniczne. Tablica heterogeniczna
to tablica zawierająca elementy różnych typów; nie da się jej zdefiniować jako typ, a jedynie zapisać w postaci
konstruktora tablicowego, na przykład:

['Delphi 6', TRUE, 1, 3.5, @MyFunc, X-Y]

Tablicy heterogenicznej nie można użyć w wyrażeniach; jedynym jej zastosowaniem jest rola parametru
aktualnego procedur i funkcji. Parametr formalny odpowiadający tablicy heterogenicznej deklarowany jest za
pomocą frazy array of const:
procedure CoMyTuMamy(A: array of const);
procedure ZobaczmyJeszczeTo (const A: array of const);

Mimo iż możliwe jest zadeklarowanie procedury (funkcji) z tablicą heterogeniczną przekazywaną przez
referencję
procedure ToTezCiekawe(var A: array of const);

to parametrem aktualnym odpowiadającym takiej deklaracji może być tylko inny parametr array of const,
na przykład:

procedure First(var X: array of const);


begin

end;

procedure Second(Y: array of const);


begin

First(Y);

end;

W treści procedury (funkcji) liczbę elementów tablicy heterogenicznej możemy poznać za pomocą funkcji
High() — wszak jest to odmiana tablicy otwartej. Jeżeli natomiast chodzi o typy poszczególnych elementów, to
tablica heterogeniczna postrzegana jest wewnątrz procedury (funkcji) jako tablica o strukturze
array [ 0 .. … ] of TVarRec

gdzie TVarRec jest następującym rekordem:


TVarRec = record
case Byte of
vtInteger: (VInteger: Integer; VType: Byte);
vtBoolean: (VBoolean: Boolean);
vtChar: (VChar: Char);
vtExtended: (VExtended: PExtended);
vtString: (VString: PShortString);
vtPointer: (VPointer: Pointer);
vtPChar: (VPChar: PChar);
vtObject: (VObject: TObject);
vtClass: (VClass: TClass);
vtWideChar: (VWideChar: WideChar);
vtPWideChar: (VPWideChar: PWideChar);
vtAnsiString: (VAnsiString: Pointer);
vtCurrency: (VCurrency: PCurrency);
vtVariant: (VVariant: PVariant);
vtInterface: (VInterface: Pointer);
vtWideString: (VWideString: Pointer);
vtInt64: (VInt64: PInt64);
end;

Informację o typie elementu — czyli wariancie rekordu TVarRec odpowiadającym elementowi — zawiera pole
VType. Znaczenie poszczególnych jego wartości jest następujące:

vtInteger = 0;
vtBoolean = 1;
vtChar = 2;
vtExtended = 3;
vtString = 4;
vtPointer = 5;
vtPChar = 6;
vtObject = 7;
vtClass = 8;
vtWideChar = 9;
vtPWideChar = 10;
vtAnsiString = 11;
vtCurrency = 12;
vtVariant = 13;
vtInterface = 14;
vtWideString = 15;
vtInt64 = 16;

Poniższa procedura wykorzystuje tę informację, wypisując typ poszczególnych elementów:

procedure ZawartoscTablicy (A: array of const);


var
i: Integer;
TypeStr: string;
begin
for i := Low(A) to High(A) do
begin
case A[i].VType of
vtInteger : TypeStr := 'Integer';
vtBoolean : TypeStr := 'Boolean';
vtChar : TypeStr := 'Char';
vtExtended : TypeStr := 'Extended';
vtString : TypeStr := 'String';
vtPointer : TypeStr := 'Pointer';
vtPChar : TypeStr := 'PChar';
vtObject : TypeStr := 'Object';
vtClass : TypeStr := 'Class';
vtWideChar : TypeStr := 'WideChar';
vtPWideChar : TypeStr := 'PWideChar';
vtAnsiString : TypeStr := 'AnsiString';
vtCurrency : TypeStr := 'Currency';
vtVariant : TypeStr := 'Variant';
vtInterface : TypeStr := 'Interface';
vtWideString : TypeStr := 'WideString';
vtInt64 : TypeStr := 'Int64';
end;
ShowMessage(Format('Element %d jest typu %s', [i, TypeStr]));
end;
end;

Zasięg deklaracji
Zasięg deklaracji (scope) jest pojęciem związanym z obowiązywaniem poszczególnych deklaracji w
poszczególnych fragmentach programu. I tak, zmienne globalne, deklarowane w programie głównym
(„projekcie”) widoczne są w całym programie, natomiast zmienne lokalne deklarowane w procedurze nie są
widoczne na zewnątrz niej. Oto prosty przykład:

Wydruk 2.2. Ilustracja zasięgu deklaracji


Program Zasieg;
{$APPTYPE CONSOLE}
const
StalaGlobalna = 100;

var
ZmiennaGlobalna : Integer;
R : Real;
Procedure Przykladowa ( var R : Real );
var
ZmiennaLokalna : real;
begin
ZmiennaLokalna := 10.0;
R := R - ZmiennaLokalna;
end;

begin { początek programu głównego }


ZmiennaGlobalna := StalaGlobalna;
R := 4.593;
Przykladowa(R)
end.

Na poziomie globalnym definiowane są tutaj trzy elementy: stała StalaGlobalna oraz zmienne
ZmiennaGlobalna i R. Procedura Przykladowa deklaruje w swym wnętrzu zmienną lokalną
ZmiennaLokalna — próba użycia tej zmiennej poza procedurą spowoduje błąd kompilacji. Procedura ta
deklaruje także parametr o nazwie R — ma on taką samą nazwę, jak jedna ze zmiennych globalnych i tym
samym przesłania tę ostatnią w treści procedury. Identyfikator R ma więc różne znaczenie wewnątrz procedury i
na zewnątrz niej.

Moduły
Moduły (units) stanowią podstawowe jednostki programu, grupujące deklaracje oraz procedury i funkcje,
osiągalne zarówno z programu głównego, jak i z poziomu poszczególnych modułów. Każdy moduł składa się —
obowiązkowo — z następujących elementów:
• Dyrektywa UNIT. Stanowi ona pierwszą linię modułu i zawiera jego nazwę poprzedzoną słowem unit.
Nazwa modułu musi być tożsama z nazwą pliku, w którym się on znajduje; moduł o nazwie BANKI musi
znajdować się w pliku BANKI.PAS.
• Część publiczna. Rozpoczyna się od dyrektywy interface i zawiera te części modułu (stałe, zmienne,
nagłówki procedur i funkcji itp.), które mają być widoczne dla innych modułów oraz programu głównego.
Zwracamy uwagę, że deklaracje procedur i funkcji w części publicznej ograniczają się jedynie do
nagłówków.
• Część prywatna. Rozpoczyna się od dyrektywy implementation, oznaczającej zarazem koniec części
publicznej, i zawiera te elementy modułu, które nie mają być widoczne na zewnątrz niego. Zawiera również
pełne teksty procedur oraz funkcji zadeklarowanych w części publicznej (można pominąć parametry, o ile
nie korzysta się z przeciążania i parametrów domyślnych — lecz nie jest to konieczne).
Ponadto, w module mogą opcjonalnie wystąpić następujące elementy:
• Część inicjacyjna. Rozpoczyna się od dyrektywy initialization, a kończy dyrektywą end, kończącą
równocześnie cały moduł, bądź też słowem finalization (patrz następny punkt). Instrukcje znajdujące
się w części inicjacyjnej wykonywane są jednokrotnie, podczas rozpoczynania pracy programu, a kolejność
wykonywania części inicjacyjnych poszczególnych modułów zależy od ich wzajemnego uzależnienia
wynikającego z dyrektyw uses (o tym za chwilę). Programy nie powinny uzależniać swego działania od
kolejności wykonywania części inicjacyjnych poszczególnych modułów, gdy kolejność ta może się zmienić
na skutek drobnej nawet modyfikacji któregoś z modułów.
• Część kończąca. Jeżeli w ogóle występuje, to jest ostatnią częścią modułu. Rozpoczyna się od dyrektywy
finalization a kończy dyrektywą end, kończącą zarazem cały moduł. Pojawiła się w Delphi 2,
zastępując znany z Turbo Pascala mechanizm tzw. funkcji kończących ExitProc (mechanizm ten istniał
jeszcze w Delphi 1, wspierany dodatkowo przez funkcję AddExitProc). Podobnie jak w przypadku części
inicjującej, nie należy zakładać żadnej określonej kolejności wykonywania części kończących
poszczególnych modułów.

Dyrektywa uses
Dyrektywa uses w module lub programie głównym specyfikuje listę modułów, do których występują
odwołania. Nazwy poszczególnych modułów na liście oddzielone są przecinkami:
uses
Scans, Convert, Arith;

Dyrektywa uses może wystąpić w części publicznej i (lub) w części prywatnej. Nazwy znajdujące się na liście
uses w części publicznej, obowiązujące są w całym module; lista uses w części prywatnej modułu nie jest
natomiast widoczna w jego części publicznej. Niedopuszczalne jest wystąpienie tej samej nazwy na obydwu
listach.
Oto schemat prostego modułu:
unit FooBar;
interface
uses
BarFoo;

// tu deklaracje części publicznej

implementation
uses
BarFly;

// tu deklaracje i definicje części prywatnej

initialization

// część inicjująca

finalization

// część kończąca

end.

Cykliczna zależność między modułami


Jeżeli nazwa A występuje na liście uses w części publicznej modułu B, to mówimy, że moduł B jest
bezpośrednio zależny od modułu A. Jeżeli w publicznej części modułu A na liście uses występuje nazwa C, to
moduł B jest zależny pośrednio od modułu C — poprzez moduł A. Ogólnie rzecz biorąc, opisana zależność
pomiędzy modułami może prowadzić przez większą liczbę modułów pośrednich.
Zależność taka ma bardzo wyraźny aspekt praktyczny: jeżeli moduł X zależny jest od modułu Y, to
skompilowanie (przynajmniej) części publicznej modułu Y jest konieczne do tego, by mogła rozpocząć się
kompilacja modułu X. Jeżeli więc zdarzy się tak, iż przynajmniej dwa moduły (nazwijmy je P i Q) będą od siebie
nawzajem zależne, nie będzie możliwe skompilowanie ani modułu P, ani Q; w efekcie nie będzie możliwe
skompilowanie projektu. Takie wzajemne uzależnienie publicznych części modułów nazywamy w Pascalu
odwołaniem cyklicznym (circular unit reference); powoduje ono oczywiście błąd kompilacji.
Należy zaznaczyć, iż listy uses w części prywatnej modułów nie powodują opisanego uzależnienia. Wynika
stąd prosty wniosek, iż pierwszym krokiem w celu pozbycia się odwołania cyklicznego powinna być próba
przeniesienia kolidujących nazw na listach uses z części publicznej do części prywatnej modułów (chodzi
oczywiście o te nazwy, które w części publicznej nie są potrzebne i znalazły się tam np. przez niedopatrzenie).
Jeżeli nie rozwiąże to problemu, należy stworzyć odrębny moduł i przenieść do jego części publicznej te
elementy, które stanowią przyczynę wystąpienia odwołania cyklicznego.

Notatka

Z matematycznego punktu widzenia — relacja zależności między modułami (stanowiąca przechodnie


domknięcie relacji zależności bezpośredniej) powinna być relacją antysymetryczną, w przeciwnym razie
mamy do czynienia z odwołaniem cyklicznym.

Poniższe trzy moduły uwikłane są w odwołanie cykliczne:


UNIT A;
Interface
uses
B;

implementation
....
end.

UNIT B;
interface
uses
C;

implementation
....
end.

UNIT C;
interface
uses
A;

implementation
....
end.

Jeżeli jednak w module B przeniesiemy dyrektywę uses do części prywatnej


UNIT B;
interface

implementation
uses
C;

....
end.

pozbędziemy się odwołania cyklicznego (jest to oczywiście możliwe tylko wtedy, gdy w części publicznej
modułu B nie ma elementów odwołujących się do modułu C).
Pewnego wyjaśnienia wymaga relacja zwana zależnością pseudocykliczną. Występuje wtedy (przy założeniu, że
dwa moduły nazwaliśmy A i B), gdy moduł A odwołuje się do modułu B w części publicznej, zaś moduł B
odwołuje się do modułu A w części prywatnej — jak w poniższym przykładzie:
UNIT A;

Interface
uses
B;

Implementation

End.

UNIT B;

Interface

Implementation
uses
A;

End.

{ program główny }
USES
A,B;
BEGIN
....
END.
Począwszy od wersji 7.0 Turbo Pascala relacja taka nie ma dla kompilatora żadnego szczególnego znaczenia i
nie powoduje nigdy błędu kompilacji.

Pakiety
W Delphi 3 pojawiła się możliwość podziału kodu wynikowego aplikacji na kilka oddzielnych fragmentów, które
mogą być współdzielone przez kilka aplikacji. Takie „fragmenty”, mające postać bibliotek DLL i stanowiące
kolekcje skompilowanych modułów (units), nazywane są w Delphi pakietami (packages); poszczególne pakiety
przyłączane są do aplikacji w czasie jej wykonania, nie w czasie kompilacji i konsolidacji. Przeniesienie części
kodu aplikacji do pakietów powoduje „odchudzenie” głównego modułu .EXE lub .DLL, jednak z istotną
oszczędnością kodu mamy do czynienia w sytuacji, gdy pojedynczy pakiet wykorzystywany jest równocześnie
przez kilka aplikacji.
Istnieją cztery typy pakietów:
• Pakiety wykonywalne (runtime packages) — poprawniej byłoby nazwać je „pakietami etapu wykonania” —
to pakiety wykorzystywane bezpośrednio przez działającą aplikację. Ich obecność jest konieczna do
uruchomienia aplikacji; przykładem pakietów tej kategorii jest „firmowy” pakiet Delphi 6 VCL60.BPL.
• Pakiety środowiskowe (design-time packages) — zwane również „pakietami etapu projektowania” —
zawierają elementy niezbędne do przeprowadzenia procesu projektowania aplikacji: komponenty, edytory
komponentów i właściwości, programy ekspertowe itp.; przykładami pakietów tej kategorii są w Delphi 6
pliki DCL*.BPL. Pakiety środowiskowe wykorzystywane są wyłącznie przez środowisko IDE; instaluje się
je za pomocą opcji Component|Install Package menu głównego. Zajmiemy się nimi dokładniej w
rozdziale 11.
• Pakiety uniwersalne łączą w sobie funkcjonalność pakietów wykonywalnych i środowiskowych. Dzięki
zintegrowaniu całej funkcjonalności w pojedynczym pliku, pakiet tej kategorii jest nieco wygodniejszy w
dystrybucji, jednak podczas wykorzystywania go przez działającą aplikację wynikową spora część kodu —
ta dotycząca środowiska IDE — nie jest wykorzystywana i bezproduktywnie powiększa rozmiar pliku.
• Pakiety nie mieszczące się w żadnej z powyższych kategorii spotykane są bardzo rzadko i pełnią zazwyczaj
rolę pomocniczą w stosunku do innych pakietów, nie są zaś bezpośrednio wykorzystywane ani przez
uruchomione aplikacje, ani przez środowisko IDE.

Wykorzystywanie pakietów
Wszelkie czynności niezbędne do wygenerowania kodu aplikacji z podziałem na pakiety sprowadzają się do
…zaznaczenia pola Build with Runtime Packages na karcie Packages opcji projektu i skompilowania
projektu w trybie Build. Należy przy tym pamiętać, iż wszystkie wygenerowane pakiety stanowią integralną
część aplikacji i niezbędne są do jej uruchomienia.

Składnia pakietu
Każdy pakiet reprezentowany jest przez plik źródłowy z rozszerzeniem .DPK. Taki plik tworzony jest przez
edytor pakietów (Package Editor) uruchamiany za pomocą polecenia File|New|Package menu głównego.
Zawartość pliku .DPK posiada następujący format:

package nazwa_pakietu
requires Pakiet1, Pakiet2, ...;
contains
Unit1 in 'Unit1.pas',
Unit2 in 'Unit2.pas',
...;
end.
Lista requires zawiera nazwy pakietów niezbędnych do funkcjonowania danego pakietu; są to najczęściej
pakiety zawierające moduły (units) wykorzystywane przez moduły wymienione na liście contains. Lista
contains zawiera nazwy modułów włączanych do pakietu; żadna z tych nazw nie może wystąpić na liście
contains któregokolwiek pakietu wymienionego na liście requires; inaczej mówiąc — każdy moduł (unit)
musi być ładowany przez dany pakiet jednokrotnie. Każdy z modułów wykorzystywanych przez moduły
wymienione na liście contains dołączany jest do pakietu automatycznie (chyba że znalazł się już w pakiecie z
tytułu przynależności do listy requires).

Programowanie zorientowane obiektowo


Programowanie zorientowane obiektowo zdobyło w ciągu ostatnich kilkunastu lat rangę niemal kultową. Nic w
tym dziwnego — po językach algorytmicznych, a następnie programowaniu strukturalnym jest to następna idea,
której skutki w rewolucjonizowaniu procesu projektowania oraz programowania są widoczne aż nazbyt dobrze.
Doceniając znaczenie OOP, pozostawimy jednak na boku wszelką egzaltację — niech przemówią konkrety, jak
przystało na podręcznik dla zaawansowanych programistów.

Idea, na której opiera się filozofia obiektów, jest — jak wszystkie genialne pomysły — bardzo klarowna. Zrywa
mianowicie z dotychczasową ideą aplikacji rozumianej jako współpraca dwóch światów — danych i
operującego na nich kodu. W dużych aplikacjach każdy z owych „światów” przejawiał nierzadko tendencje
rozrostu do rozmiarów (niemalże) wszechświata, komplikując coraz bardziej i tak niełatwą już pracę
programistów i projektantów. Cała sprawa znacznie by się uprościła, gdyby współpracę tę zorganizować na
zasadzie istnienia swoistych mikroświatów, z których każdy stanowi cząstkę logicznie powiązanych danych i
kodu. Owe „mikroświaty”, nazwane (może trochę banalnie) obiektami, stanowią podstawę nowego paradygmatu
programowania, zwanego właśnie programowaniem zorientowanym obiektowo (OOP — Object Oriented
Programming). Mimo iż sama idea programowania obiektowego niekoniecznie prowadzi do łatwego
programowania, to zwykle efektem jej zastosowania jest klarowny kod, który łatwo jest utrzymywać i w którym
łatwo jest znajdować ewentualne błędy.

Współczesne języki programowania implementują co najmniej trzy następujące koncepcje OOP:

• Enkapsulacja zwana też hermetyzacją. Polega na ścisłym powiązaniu kodu oraz danych służących temu
samemu celowi, poprzez zamknięcie ich w ramach jednego bytu — typu obiektowego, z jednoczesnym
ukryciem szczegółów implementacyjnych. Umożliwiając izolowanie poszczególnych fragmentów kodu,
przyczynia się do modularyzacji programu.
• Dziedziczenie. W złożonych aplikacjach nie sposób nie zauważyć podobieństw między poszczególnymi
fragmentami danych i kodu. Stąd pomysł, by przy definiowaniu nowych typów obiektowych nie zaczynać
pracy ab initio, lecz wykorzystać cechy obiektów już istniejących. Rysunek 2.4 przedstawia zastosowanie
filozofii dziedziczenia cech wśród różnych rodzajów owoców; im dalej od „korzenia”, tym więcej
konkretnych cech rozważanego obiektu.
• Polimorfizm. Tłumaczony dosłownie oznacza „wielopostaciowość” i określa sytuację, w której jednakowo
identyfikowane cechy mogą manifestować się w różny sposób, w zależności od konkretnego egzemplarza
obiektu. Na gruncie Object Pascala oznacza to różne funkcjonowanie identycznie nazwanych metod w
odniesieniu do różnych klas obiektów.
Rysunek 2.4. Ilustracja koncepcji dziedziczenia
<ramka>

Zwróć uwagę na ważny fakt, iż na rysunku 2.4 dla każdego obiektu istnieje dokładnie jedna ścieżka łącząca
go z korzeniem, a więc każdy obiekt dziedziczy swe cechy od dokładnie jednego obiektu macierzystego. Taki
właśnie charakter ma dziedziczenie cech obiektowych w Object Pascalu. Język C++ jest pod tym względem
znacznie bardziej rozbudowany; oferuje dziedziczenie od wielu obiektów jednocześnie (multiple inheritance)
— co można by uwidocznić na wspomnianym rysunku, gdyby udało nam się wyhodować krzyżówkę np.
renety i arbuza.

Brak wielokrotnego dziedziczenia w Object Pascalu postrzegany jest rozmaicie — przez niektórych jako
dotkliwe ograniczenie, przez innych natomiast jako brak jeszcze jednej okazji do popełniania błędów.
Niezależnie od subiektywnego spojrzenia na brak wielokrotnego dziedziczenia, warto zastanowić się nad
rozwiązaniami stanowiącymi jego odpowiednik — a te są w Object Pascalu dwojakie. Jedno z nich,
wykorzystane m.in. przy budowie biblioteki VCL, polega na zawieraniu się w danym obiekcie obiektów klasy
„macierzystej” — obiekt reprezentujący krzyżówkę renety i arbuza mógłby wywodzić się z klasy „arbuz” i
zawierać w sobie (jako jedno z pól) obiekt klasy „reneta”. Druga koncepcja polega na implementowaniu przez
pojedynczy obiekt elementów zachowań kilku specyficznych klas, zwanych interfejsami — zajmiemy się nimi
w dalszej części rozdziału.

<ramka>
Z pojęciem obiektu, jako typu w języku programowania, wiążą się ponadto trzy następujące terminy:
• Pole (field) — zwane także „zmienną egzemplarza” (instance variable) stanowi odmianę zmiennej
funkcjonującej w kontekście obiektu. W języku C++ pola nazywane są „elementami danych” (data
members).
• Metoda (method) — jest procedurą lub funkcją działającą na rzecz pól obiektu. W C++ metody nazywane są
„funkcjami składowymi” (member functions).
• Właściwość (property) — stanowi połączenie koncepcji pola i metody i realizuje mechanizm dostępu do pól
i metod obiektu. Operowanie właściwościami obiektu (w przeciwieństwie do bezpośredniego operowania
jego polami) uniezależnia sposób korzystania z niego od jego szczegółów implementacyjnych.

Wskazówka

Bezpośrednie operowanie na polach obiektu, choć możliwe, jest zasadniczo sprzeczne z filozofią
programowania obiektowego, koncepcyjnie stanowi bowiem rodzaj ingerencji w szczegóły implementacyjne
obiektu. Powinniśmy go unikać i posługiwać się właściwościami.

Środowisko bazujące na obiektach kontra środowisko


zorientowane obiektowo
Rozróżnienie dwóch wymienionych w tytule środowisk (ang. object-based i object-oriented) bierze się stąd, że
istnieją środowiska oferujące gotowe obiekty i jednocześnie skrywające przed programistą całą filozofię OOP;
sztandarowym przykładem są starsze wersje Visual Basica z kontrolkami VBX i OCX. Trudno mówić o
jakiejkolwiek obiektowej orientacji programowania, skoro programista — jakby na przekór — posiada
możliwość programowania i definiowania własnych typów jedynie w „klasycznym” stylu. Dlatego też można
bez przesady stwierdzić, że środowisko to jedynie bazuje na gotowych obiektach.
Delphi nie stwarza natomiast żadnych ograniczeń w tym względzie, umożliwiając tworzenie nowych obiektów
zarówno „od zera”, jak i drogą dziedziczenia z istniejących obiektów — wizualnych, niewizualnych, czy nawet
kompletnych formularzy.

Wykorzystanie obiektów w Delphi


Obiekty, zwane także w Delphi klasami14, są (jak wspominaliśmy wcześniej) jednostkami zawierającymi dane i
powiązany z nimi kod. Jako środowisko w pełni zorientowane obiektowo, Delphi udostępnia wszelkie korzyści
płynące z trzech zasadniczych filarów OOP — enkapsulacji, dziedziczenia i polimorfizmu.

Deklarowanie obiektów i kreowanie zmiennych


obiektowych
Typ obiektowy w Delphi deklarujemy za pomocą słowa kluczowego class:
Type
TFooObject = class;

Posiadając już zdefiniowany typ obiektowy, możemy zdefiniować jego egzemplarz (instance):
Var
FooObject : TFooObject;

To jednak dopiero początek; w przeciwieństwie do Turbo Pascala w wersji 5.5 – 7.0, nie ma w Delphi
możliwości definiowania statycznych egzemplarzy obiektów, natomiast powyższa deklaracja określa zmienną
przechowującą wskaźnik do dynamicznie tworzonego egzemplarza klasy TFooObject.
Do dynamicznego tworzenia egzemplarzy klas służą wyróżnione metody zwane konstruktorami. W Object
Pascalu każda klasa posiada przynajmniej jeden konstruktor o nazwie Create(). Jego zestaw parametrów (lub
ich brak) zależy od konkretnej klasy; dla uproszczenia w dalszej części rozdziału ograniczymy się do jego wersji
bezparametrowej.
W przeciwieństwie do C++, konstruktory w Object Pascalu muszą być wywoływane w sposób jawny. Instrukcja
powodująca utworzenie egzemplarza obiektu, zgodnie ze zdefiniowanym wcześniej typem, ma następującą
postać:
FooObject := TFooObject.Create;

Zwróćmy przy tym uwagę na sposób wywołania konstruktora: jest on wywoływany na rzecz określonego typu
(klasy), nie zaś konkretnego egzemplarza (obiektu). Jest to zrozumiałe wobec faktu, iż przed wywołaniem
konstruktora nie istnieje jeszcze egzemplarz obiektu.

Inicjalizacja obiektu wykonywana przez konstruktor wiąże się między innymi z wyzerowaniem całego
przydzielonego dla obiektu obszaru pamięci. Powoduje to, że wszystkie liczby (będące rzecz jasna polami
obiektu) stają się równe zero, łańcuchy stają się pustymi napisami (''), a wskaźniki — pustymi wskazaniami
(NIL).

Destrukcja obiektu
Po wykorzystaniu obiektu należy zwolnić zajętą przez niego pamięć. Wcześniej muszą zostać wykonane
charakterystyczne dla danego typu czynności kończące. Zadanie to wykonuje wyróżniona metoda zwana
destruktorem. Każda klasa w Object Pascalu zawiera destruktor zwany Destroy(). Teoretycznie, możliwe jest
jego aktywowanie dla konkretnego egzemplarza obiektu, który mamy zamiar unicestwić:
FooObject.Destroy;

14
Zgodnie jednak z przyjętą konwencją, pod pojęciem klasy kryje się w Delphi konkretny typ danych, natomiast określenie
„obiekt” używane jest w odniesieniu do konkretnego egzemplarza tego typu (przyp. tłum.).
Zamiast tego zaleca się jednak wywołanie metody Free():
FooObject.Free;

Metoda ta sprawdza wpierw, czy zmienna obiektowa (FooObject) nie zawiera pustego wskazania (NIL) —
jeżeli nie, następuje wywołanie metody Destroy() dla wskazywanego obiektu.

Ostrzeżenie

W C++ destruktor obiektu zadeklarowanego statycznie wywoływany jest automatycznie w momencie, gdy
sterowanie opuszcza zasięg deklaracji tego obiektu; obiekty tworzone dynamicznie muszą być jednak
zwalniane w sposób jawny, za pomocą słowa kluczowego delete. W Delphi nie ma obiektów statycznych,
musimy więc jawnie zwalniać każdy egzemplarz obiektu, mając na uwadze dwa (z szeregu innych)
uwarunkowania. Po pierwsze, zwalniany obiekt dokonuje jednoczesnego zwolnienia wszystkich innych
obiektów, dla których jest właścicielem; po drugie — istnieją współdzielone przez kilka aplikacji obiekty,
których wykorzystanie opiera się na tzw. liczniku odwołań (reference counter), i które są zwalniane dopiero
wówczas, gdy licznik ten osiągnie wartość 0 (czyli ostatnia z aplikacji zakończy swe operacje na obiekcie).
Przykładami takich współdzielonych obiektów są obiekty klas TInterfacedObject i TComObject.

Nasuwa się pytanie, skąd bierze się obecność konstruktora Create(), destruktora Destroy() i metody
Free() w każdym typie obiektowym? Odpowiedź na to pytanie wskazuje jeszcze jedną różnicę między Turbo
Pascalem a Delphi. W Delphi każdy obiekt bez wskazanej jawnie klasy bazowej jest traktowany jako typ
pochodny klasy TObject, tak więc deklaracja
Type TFoo = class;

równoważna jest deklaracji


Type TFoo = class (TObject);

Wymienione metody — Create(), Destroy() i Free() są częścią klasy TObject — powrócimy za chwilę
do tej kwestii.

Metody
Metody są tym aspektem typu obiektowego, który pobudza obiekt do życia i decyduje o jego zachowaniu
(trudno to powiedzieć o polach obiektu, które są co najwyżej „pożywką” dla metod). Przykładami metod są
poznane przed chwilą konstruktory i destruktory.
Deklarowanie własnej metody przebiega dwuetapowo. Etap pierwszy to umieszczenie nagłówka metody
wewnątrz deklaracji klasy, na przykład:
Type
TDyskoteka = class;
Taniec : Boolean;
Procedure ZatanczSambe;
End;

Konkretyzacja treści procedury odbywa się w drugim etapie, w części implementacyjnej modułu zawierającego
deklarację klasy:
Procedure TDyskoteka.ZatanczSambe;
begin
Taniec := TRUE;
end;
Zwróć uwagę na to, iż właściwa nazwa procedury poprzedzona jest nazwą klasy, dla której ta procedura jest
metodą. Podobną (kwalifikowaną) postać mają odwołania do metody — nazwa metody poprzedzona jest
określeniem obiektu, na rzecz którego metoda ta jest wywoływana:
Var
Maxim : TDyskoteka;

........

Maxim.ZatanczSambe;

Podobnie jak w przypadku rekordów, odwołania kwalifikowane można zastąpić instrukcją with:
with Maxim do
ZatanczSambe;

W treści metod danej klasy odwołania do pól jej obiektów nie mają postaci kwalifikowanej, gdyż treść metody
jest dla tych pól zakresem ich widoczności (vide pole Taniec w metodzie TDyskoteka.ZatanczSambe).

Typy metod
Metoda klasy w Object Pascalu może być metodą statyczną, wirtualną, dynamiczną i komunikacyjną. Oto
przykład deklaracji metod każdego z wymienionych rodzajów
TFoo = class
Procedure Statyczna;
Procedure Wirtualna;virtual;
Procedure Dynamiczna;dynamic;
Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage;
End;

Metody statyczne
Metoda, której deklaracja nie jest opatrzona żadnymi dodatkowymi klauzulami, jest metodą statyczną.
Funkcjonuje ona podobnie do „zwykłej” procedury lub funkcji, jej adres znany jest już w czasie kompilacji, a jej
wywołanie przebiega bardzo efektywnie. Metody statyczne nie udostępniają jednak żadnych korzyści płynących
z polimorfizmu.

Wskazówka

W przeciwieństwie do C++, klasy Object Pascala nie mogą posiadać statycznych pól. W Object Pascalu pole
jest zawsze częścią egzemplarza klasy (czyli obiektu) — zmiana zawartości pola w jednym obiekcie nie ma
wpływu na jego zawartość w innym; pole statyczne jest natomiast częścią klasy, wspólną dla wszystkich jej
obiektów, ma więc dla nich charakter globalny. Symulacją (do pewnego stopnia) globalnych pól klasy może
być w Object Pascalu wykorzystanie zmiennych globalnych modułu (w jego części prywatnej) — w treści
metod zmienne takie zachowują się tak, jak zachowywałyby się pola statyczne (gdyby istniały)15.

Metody wirtualne
Dziedziczenie wiąże się z możliwością przedefiniowywania (overriding) metod obiektu. Oznacza to, że metoda
o danej nazwie może mieć zupełnie różne działanie dla różnych klas (macierzystej i pochodnej). Innymi słowy,
kompilator, znając nazwę metody, nie potrafi określić jej konkretnego adresu, gdy nie zna konkretnego obiektu
(a właściwie jego typu), na rzecz którego jest ona aktywowana.
Zjawisko różnego zachowania metod o tej samej nazwie w odniesieniu do różnych typów w całym poddrzewie
typów pochodnych danej klasy nosi nazwę polimorfizmu — metoda o danej nazwie ma jak gdyby „wiele
twarzy”. Zachowuje się różnie, w zależności od typu obiektu, na rzecz którego zostanie wywołana. Dla realizacji
polimorfizmu Object Pascal utrzymuje struktury zwane tablicami VMT (Virtual Method Tables), po jednej dla
każdej klasy. Każda tablica VMT zawiera adresy wszystkich metod wirtualnych swej klasy (także tych,
które dziedziczone są z klasy bazowej bez zmian), a więc metody wirtualne przyczyniają się w pewnym

15
Zmienne globalne modułu w połączeniu z mechanizmem właściwości pozwalają na symulację statycznych pól nie tylko w
treści metod — niebawem powrócimy do tej kwestii (przyp. tłum.).
stopniu do obciążenia pamięci. Obciążenie to można do pewnego stopnia zmniejszyć, za cenę nieznacznego
pogorszenia efektywności, używając metod dynamicznych.

Metody dynamiczne
Metody dynamiczne wykorzystywane są dokładnie tak samo, jak metody wirtualne, jednak ich realizacja
ukierunkowana została przede wszystkim na efektywne wykorzystanie pamięci, kosztem efektywności ich
wywoływania. Dla każdej klasy deklarującej chociaż jedną metodę dynamiczną kompilator utrzymuje strukturę
zwaną tablicą DMT (Dynamic Method Table). Tablica DMT zawiera adresy tylko tych metod, które są
przedefiniowane w stosunku do klasy macierzystej; klasy nie deklarujące własnych metod dynamicznych nie
posiadają w ogóle tablicy DMT. Metody dynamiczne nie obciążają więc pamięci brzemieniem dziedziczonym z
klas macierzystych, ich wywoływanie jest jednak mniej efektywne (niż w przypadku metod wirtualnych),
ponieważ bardziej złożony jest algorytm poszukiwania adresu konkretnej metody.

Metody komunikacyjne
Metody tej kategorii stanowią reminiscencję „klasycznego” programowania w Windows i służą do obsługi
wybranych komunikatów — identyfikator komunikatu zawarty jest w klauzuli message w deklaracji metody.
Metody komunikacyjne nie są raczej przeznaczone do bezpośredniego wywoływania — należą do tzw. funkcji
zwrotnych (callback), wywoływanych automatycznie przez system operacyjny. Szczegółami obsługi
komunikatów systemowych zajmiemy się w rozdziale 3.

Przedefiniowywanie metod
Przedefiniowywanie metod jest praktyczną realizacją polimorfizmu. Na gruncie danej klasy i wszystkich jej klas
pochodnych metoda o danej nazwie może wykazywać różne zachowanie w zależności od konkretnej klasy (lub
klasy konkretnego obiektu). Przedefiniowywane mogą być tylko metody wirtualne i dynamiczne; fakt
przedefiniowania metody zaznacza się klauzulą override w jej deklaracji. W poniższym przykładzie klasa
TFooChild przedefiniowuje metody Wirtualna i Dynamiczna, odziedziczone z klasy macierzystej TFoo:
TFooChild = class(TFoo)
Procedure Wirtualna;override;
Procedure Dynamiczna;override;
End;

Użycie klauzuli override powoduje zmianę odpowiedniego wskaźnika w tablicy VMT. Z


przedefiniowywaniem metod w Delphi wiąże się dodatkowo istotna różnica w stosunku do Turbo Pascala:
użycie w miejsce klauzuli override klauzuli virtual albo dynamic nie oznacza przedefiniowania (jak w
Turbo Pascalu), lecz stanowi zapoczątkowanie nowego łańcucha powiązanych metod o (przypadkowo)
identycznej nazwie. W poniższym przykładzie
TFooBastard = class(TFoo)
Procedure Wirtualna;virtual;
Procedure Dynamiczna;dynamic;
End;

metody Wirtualna i Dynamiczna nie mają nic wspólnego z identycznie nazwanymi metodami klasy TFoo.
Gdy kompilator napotka taką sytuację, wygeneruje ostrzeżenie, iż metoda klasy pochodnej zasłania identycznie
nazwaną metodę klasy macierzystej.

Reintrodukcja metody
Opisane przed chwilą „zasłonięcie” metody klasy bazowej i zapoczątkowanie nowego łańcucha metod o
identycznej nazwie może być niekiedy działaniem całkowicie zamierzonym. Dla podkreślenia, iż nie mamy do
czynienia z pomyłką — i jednocześnie dla wyeliminowania ostrzeżeń ze strony kompilatora — możemy w
sposób jawny zasygnalizować ten fakt, opatrując deklarację metody klauzulą reintroduce, jak w poniższym
przykładzie:
TFoo = class
Procedure Statyczna;
Procedure Wirtualna;virtual;
Procedure Dynamiczna;dynamic;
Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage;
End;
TFooOrphan = class(TFoo)
Procedure Wirtualna;reintroduce;
Procedure Dynamiczna;reintroduce;
End;

Klauzula reintroduce nie wyklucza oczywiście wystąpienia innych klauzul (virtual, dynamic i message)
w deklaracji metody.

Przeciążanie metod
Podobnie jak „zwykłe” procedury i funkcje, również metody mogą być przeciążane — umożliwia to opatrzenie
wspólną nazwą wielu aspektów konkretnej metody (w konkretnej klasie) różniących się zestawem parametrów.
Oto przykład:

Type
TSomeClass = class
procedure Amethod(I: Integer);overload;
procedure Amethod(S: String);overload;
procedure Amethod(D: Double);overload;
end;

Poniższy przykład (zaczerpnięty z systemu pomocy Delphi) ilustruje ciekawy przypadek, gdy różne aspekty
danej metody należą do różnych klas:

type
T1 = class(TObject)
procedure Test(I: Integer); overload; virtual;
end;

T2 = class(T1)
procedure Test(S: string); reintroduce; overload;
end;
...
SomeObject := T2.Create;
SomeObject.Test('Hello!'); // wywołuje T2.Test()
SomeObject.Test(7); // wywołuje T1.Test()

Identyfikator Self
Aby w treści metody możliwe były kwalifikowane odwołania do pól, metod i właściwości obiektu, konieczne
jest użycie identyfikatora tegoż obiektu — takim uniwersalnym identyfikatorem jest Self reprezentujący w
treści konkretnej metody obiekt, na rzecz którego wywołana została ta metoda. Jego zawartość, stanowiąca
wskaźnik do wspomnianego obiektu, przekazywana jest niejawnie jako dodatkowy parametr wywołania
wszystkich metod.

Właściwości
Natura właściwości (property) obiektu jest nieco bardziej abstrakcyjna niż natura pola czy metody.
Koncepcyjnie właściwość zbliżona jest do pola, gdyż podobnie jak pole przechowuje (modyfikowalną) wartość
określonego typu; bardziej skomplikowany jest natomiast sposób nadawania i odczytywania tej wartości.
Spójrzmy wpierw na deklarację przykładowej właściwości:

TMyObject = Class
private
SomeValue : Integer;
Procedure SetSomeValue(Avalue: Integer);
public
Property Value: Integer read SomeValue write SetSomeValue;
End;

procedure TMyObject.SetSomeValue(AValue: Integer);


begin
if SomeValue <> AValue
Then
SomeValue := AValue;
end;

Klasa TMyObject definiuje pole SomeValue, metodę SetSomeValue i właściwość Value; ta ostatnia
powiązana jest z pozostałymi elementami za pomocą klauzul read i write. Klauzula read określa sposób
odczytywania właściwości: ponieważ specyfikuje ona nazwę pola, aktualna wartość tego pola przyjmowana jest
jako wartość właściwości. Klauzula write specyfikuje natomiast nazwę metody — a to oznacza, że przypisanie
właściwości nowej wartości zostanie fizycznie zrealizowane jako wywołanie tejże metody z przypisywaną
wartością jako parametrem. Konkretnie: dla obiektu MyObj:TMyObject instrukcja
WhatValue := MyObj.Value;

równoważna jest instrukcji


WhatValue := MyObj.SomeValue;

Z kolei instrukcja
MyObj.Value := NewValue;

oznacza to samo, co
MyObj.SetSomeValue(NewValue);

W każdej z klauzul read i write może wystąpić bądź nazwa pola, bądź nazwa metody; żadna z klauzul read i
write nie jest obowiązkowa — na przykład opuszczenie klauzuli write powoduje, iż właściwości nie można
przypisywać explicite nowej wartości. Metody specyfikowane w ramach klauzul read i write umożliwiają
pełną kontrolę nad odczytywaniem i modyfikacją właściwości; stanowią one jedyny sposób dostępu do
właściwości i z tego względu nazywane są jej metodami dostępowymi (property access methods).
Właściwości komponentów VCL stanowią podstawowy środek ich komunikacji z aplikacjami; ich właściwości
opublikowane (published) dostępne są za pośrednictwem inspektora obiektów.

Wskazówka

A oto zapowiadana symulacja statycznych pól klasy za pomocą właściwości — właściwość StaticValue
zachowuje się (prawie) tak, jak statyczne pole w C++:

var

GlobalField: integer; // zmienna globalna modułu

Type

MyStaticClass = class

private

function GetGlobalField: integer;

procedure SetGlobalField(const Value: integer);


published

property StaticValue: integer read GetGlobalField write SetGlobalField;

private

end;

function MyStaticClass.GetGlobalField: integer;

begin

Result := GlobalField;

end;

procedure MyStaticClass.SetGlobalField(const Value: integer);

begin

GlobalField := Value;

end;

(przyp. tłum.).

Widoczność elementów obiektu


Poszczególne elementy obiektu mogą być w różny sposób udostępniane innym elementom aplikacji. Delphi
definiuje pięć kategorii dostępności, określanych za pomocą kwalifikatorów protected, private, public,
published oraz automated. Oto przykład:
type
TSomeObject = class
private
APrivateVariable: Integer;
AnotherPrivateVariable: Boolean;
protected
Procedure AProtectedProcedure;
Function ProtectMe: Byte;
public
constructor APublicConstructor;
destructor APublicKiller;
published
property Aproperty read APrivateVariable write APrivateVariable;
End;

Znaczenie każdej z podanych kategorii jest następujące:


• private — elementy opatrzone tą klauzulą, zwane elementami prywatnymi, widoczne są jedynie
wewnątrz modułu, w którym zdefiniowano dany obiekt. Umożliwia to ukrycie pewnych szczegółów
implementacji metod obiektu oraz ukrycie tych pól, które pełnią jedynie rolę pomocniczą i nie powinny być
dostępne dla użytkownika.
• protected — ten kwalifikator powoduje udostępnienie wskazanych elementów klasy jedynie metodom i
właściwościom jej klas pochodnych. Uniemożliwia to nieskrępowane wykorzystywanie pewnych
elementów klasy do definiowania klas pochodnych i chroni je jednocześnie przed użyciem do innych celów
— dlatego elementy tej kategorii nazywane są elementami chronionymi.
• public — kwalifikuje elementy klasy jako w pełni dostępne dla pozostałych elementów aplikacji (czyli
publiczne). Konstruktory i destruktory zawsze są metodami publicznymi.
• published — konsekwencją użycia tego kwalifikatora jest opublikowanie wybranych elementów klasy;
oprócz tego, że stają się elementami publicznymi, oznacza to także ich ewidencjonowanie w ramach
mechanizmu RTTI (Runtime Type Information) udostępniającego szczegóły definicji klasy w czasie
wykonywania programu. Z mechanizmu RTTI korzysta także inspektor obiektów, tworząc za jego pomocą
listy właściwości i zdarzeń poszczególnych komponentów.
• automated — kwalifikator ten jest pozostałością po Delphi 2 i zachowany został jedynie ze względów
kompatybilności.

Wskazówka

Ewentualne początkowe elementy deklaracji klasy nie opatrzone żadnym kwalifikatorem widoczności są, w
zależności od ustawienia przełącznika kompilacji $M, opublikowane {$M+} bądź publiczne {$M–}(przyp.
tłum.).

Możliwa jest zmiana kwalifikatora widoczności dziedziczonego elementu w klasie pochodnej, ale tylko w
kierunku „rosnącym” — na przykład element chroniony (protected) może zostać uczyniony elementem
publicznym (public), ale nie prywatnym (private) (przyp. tłum.).

Klasy zaprzyjaźnione
„Zaprzyjaźnienie” klas w C++ oznacza dostęp do prywatnych elementów definicji danej klasy z poziomu
innych klas (zwanych „klasami zaprzyjaźnionymi” — friend classes). Koncepcja ta, mimo iż nie nazwana w
sposób wyraźny, jest jednak faktycznie obecna w Object Pascalu, chociaż w sposób zdecydowanie mniej
selektywny — wszystkie klasy definiowane w tym samym module są dla siebie nawzajem klasami
zaprzyjaźnionymi.

Wewnątrz obiektów
Używanie zmiennych obiektowych w Object Pascalu wiąże się z pewną niekonsekwencją, która dla
niewprawnego użytkownika może być nieco myląca. Otóż zmienne obiektowe, mimo iż są używane w sposób
charakterystyczny dla zmiennych statycznych, są w istocie 32-bitowymi wskaźnikami do obiektów; te ostatnie
alokowane są na stercie i wspomniane wskaźniki stanowią jedyny sposób dostępu do nich — nie ma w Object
Pascalu możliwości ich bezpośredniego reprezentowania. Zgodnie z ogólnymi regułami Pascala odwołanie się
do wskaźnika wymaga użycia operatora dereferencji (^) i wydawałoby się, iż zamiast
Button1.Caption

powinno się pisać


Button1^.Caption
Tę drugą postać kompilator traktuje jednak jako błędną, zapewniając właściwą interpretację pierwszej — użycie
zmiennej obiektowej połączone jest z niejawną dereferencją zawartego w niej wskaźnika.

Notatka

Opisana niekonsekwencja występuje również w odniesieniu do zmiennych rekordowych. Zgodnie z


poniższymi deklaracjami

type

TMyRecord = record

A,B : integer;

end;

var

P: ^TMyRecord;

instrukcja

P.A := 1;

powinna być uznana za błędną z powodu braku operatora dereferencji — i taką też jest w Turbo Pascalu.
Object Pascal jednak, jakby odgadując intencje programisty, traktuje ją jako poprawną, zakładając niejawną
dereferencję. W przeciwieństwie do zmiennych obiektowych, jawne użycie operatora ^

P^.A := 1;

jest dla zmiennych rekordowych w dalszym ciągu poprawne. (przyp. tłum.).

TObject — protoplasta wszystkich klas


Każda klasa w Object Pascalu wywodzi się (pośrednio bądź bezpośrednio) z klasy TObject — nawet wówczas,
gdy nie zaznaczono tego w sposób jawny. Oznacza to, że każdy obiekt w Delphi posiada „na dzień dobry”
całkiem niemały zasób funkcjonalności. Możliwe jest między innymi uzyskanie z egzemplarza obiektu nazwy
jego klasy i jej elementów oraz pewnych informacji związanych z dziedziczeniem. Wszystko to stanie się
zrozumiałe, gdy przyjrzymy się deklaracji klasy TObject:

TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer):

HResult;virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;

Widzimy tu „starych znajomych” — konstruktor Create, destruktor Destroy i metodę Free. Znaczenie każdej
z deklarowanych metod opisane jest w systemie pomocy Delphi. Dociekliwym czytelnikom proponujemy
ponadto, by przyjrzeli się kodowi źródłowemu definicji tych metod w module SYSTEM.PAS.
Pewnego wyjaśnienia wymagają metody opatrzone dyrektywą class. Metody takie, notabene analogiczne do
statycznych metod C++, funkcjonują w kontekście klasy jako całości, bez różnicy dla poszczególnych obiektów
— i na przykład metoda ClassName, aktywowana na rzecz konkretnego obiektu, zwraca nazwę jego klasy,
pozostającą bez żadnego związku z jego zawartością16.

Interfejsy
Interfejsy (interfaces) stanowią specjalną kategorię klas, związaną z wykorzystaniem technologii obiektów —
komponentów (COM — Component Object Model); jako odrębny element syntaktyczny zostały wydzielone
dopiero w Delphi 3 — w Delphi 2 funkcjonowały na równi z innymi klasami (jako klasy pochodne w stosunku
do klasy IUnknown). Generalnie, interfejs jest zbiorem funkcji i procedur umożliwiających interakcję z
obiektem; zbiór ten określa pewien aspekt zachowania się obiektu, lecz jedynie w ujęciu intencjonalnym —
interfejs zawiera bowiem jedynie deklaracje wspomnianych procedur i funkcji; nadanie im treści, czyli
powiązanie ich z konkretnymi działaniami, jest kwestią ich implementacji jako metod w konkretnym obiekcie.
Wewnętrzne szczegóły funkcjonowania interfejsów opierają się na koncepcji tzw. klasy czysto wirtualnej (pure
virtual class) — jest nią klasa pozbawiona pól i nie implementująca swych metod; do reprezentowania takiej
klasy wystarczająca jest sama tablica VMT.

Definiowanie interfejsów
Podobnie jak wszystkie klasy Object Pascala wywodzą się z klasy TObject, tak każdy interfejs jest pochodną
interfejsu IUnknown:
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj):
HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;

Notatka:

16
Ponieważ podmiotem wywołania metody opatrzonej dyrektywą class jest klasa jako całość, inne jest znaczenie
identyfikatora Self w jej treści — zawiera on mianowicie wskaźnik do tzw. punktu zerowego tablicy VMT związanej z
klasą, na rzecz której metoda jest wywoływana; syntaktycznie jest on zmienną metaklasy (patrz następny przypis) (przyp.
tłum.).
W Delphi 6 bazowy interfejs nosi nazwę IInterface, natomiast IUnknown jest synonimem tej nazwy:

type

IInterface = interface

['{00000000-0000-0000-C000-000000000046}']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

IUnknown = IInterface;

(przyp. tłum.).

Jak widać, deklaracja interfejsu podobna jest do deklaracji klasy; jednak tym, co odróżnia interfejs od klasy, jest
unikatowy identyfikator (GUID — Globally Unique Identifier). Identyfikuje on każdy interfejs jednoznacznie —
dwa różne interfejsy, zdefiniowane w tej samej lub w różnych aplikacjach, stworzonych na tym samym
komputerze lub różnych komputerach, w dowolnym czasie, powinny mieć dwa różne identyfikatory GUID.

Wskazówka

W środowisku IDE unikatowy identyfikator GUID otrzymuje się przez naciśnięcie kombinacji klawiszy
Ctrl+Shift+G.

Metody interfejsu IUnknown związane są ściśle z technologią COM, którą zajmiemy się szczegółowo w drugim
tomie niniejszej książki.
Definiowanie interfejsów pochodnych nie różni się zasadniczo od definiowania klas pochodnych — poniższy
interfejs definiuje własną metodę F1; ponieważ nie wskazano interfejsu bazowego, jest nim domyślnie
IUnknown.
Type
IFoo = interface
['{A77A4BE0-0C82-11D6-AA88-444553540001}']
Function F1: Integer;
end;

Poniższy fragment definiuje interfejs IBar jako pochodny w stosunku do IFoo:


Type
IBar = interface(IFoo)
['{A77A4BE1-0C82-11D6-AA88-444553540001}']
Function F2: Integer;
end;

Implementowanie interfejsów
Jak wspomnieliśmy wcześniej, docelową rolą interfejsu jest jego implementacja w postaci metod jakiejś klasy.
Poniższy przykład ilustruje implementację interfejsów IFoo i IBar przez klasę TFooBar:

Type
TFooBar = class(TInterfacedObject, IFoo, IBar)
function F1: Integer;
function F2: Integer;
End;

...
Function TFooBar.F1: Integer;
begin
Result := 0;
end;

Function TFooBar.F2: Integer;


begin
Result := 0;
end;

Zwróć uwagę, iż na liście klas bazowych występuje kilka pozycji; tak naprawdę klasą bazową jest jednak tylko
TInterfacedObject, pozostałe pozycje są nazwami implementowanych interfejsów. Klasa
TInterfacedObject zawiera wszystkie niezbędne mechanizmy implementacji interfejsów, jest więc dla
obiektów implementujących interfejsy klasą bazową:

TInterfacedObject = class(TObject, IInterface)


protected
FRefCount: Integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
class function NewInstance: TObject; override;
property RefCount: Integer read FRefCount;
end;

Jak widać, nazwy metod stanowiących implementację interfejsu tożsame są z nazwami metod tego interfejsu.
Może się jednak zdarzyć, iż dwa różne interfejsy implementowane przez tę samą klasę posiadać będą metody o
tej samej nazwie; jedna z tych metod (lub obydwie) musi być wówczas implementowana pod zmienioną nazwą
(aliasem) — w poniższym przykładzie zmienione zostają nazwy obydwu metod F1:
type
ITool = interface
['{7C9CAAA4-40EB-11D2-A3FB-444553540000}']
Function F1: integer;
End;

ITip = interface
['{7C9CAAA5-40EB-11D2-A3FB-444553540000}']
Function F1: integer;
End;
TPrompt = Class(TInterfacedObject, ITool, IHelp)

Function ITool.F1 = ToolF1;


Function ITip.F1 = TipF1;
Function ToolF1: Integer;
Function TipF1: Integer;
End;

Function TPrompt.ToolF1: Integer;


begin
Result := 0;
end;
Function TPrompt.TipF1: Integer;
begin
Result := 0;
end;

Dyrektywa implements
Implementacja interfejsu może mieć również charakter pośredni — mianowicie wskaźnik do implementowanego
interfejsu może być wartością właściwości, jak w poniższym przykładzie:

Type
TSomeClass = class(TInterfacedObject, ICasual)
...
Function GetCasual: TCasual;
Property Casual: TCasual read GetCasual implements ICasual;
...
End;

Dyrektywa implements stanowi dla kompilatora informację, iż implementacji metod interfejsu poszukiwać
należy w innej klasie (w tym przypadku TCasual) — zjawisko to nazywa się więc popularnie
implementowaniem delegowanym (implementation by delegation). Typ właściwości zawierającej dyrektywę
implements musi być zgodny z typem implementowanego interfejsu lub z typem implementującej go klasy.
Powody wprowadzenia implementacji delegowanej (pojawiła się w Delphi 4) są dwojakie. Po pierwsze,
umożliwia ona klarowną realizację koncepcji agregacji obiektów, stanowiącej integrację (na gruncie technologii
COM) kilku klas w celu realizacji wspólnego celu; zajmiemy się tym szczegółowo w jednym z rozdziałów
drugiego tomu niniejszej książki.
Drugi powód wprowadzenia implementowania delegowanego związany jest z oszczędnością zasobów
systemowych. Jeżeli implementacja jakiegoś interfejsu wiąże się np. z dużym obciążeniem pamięci, a interfejs
ten wykorzystywany jest bardzo rzadko, wskazane byłoby tę implementację odłożyć do momentu, gdy
wspomniany interfejs okaże się faktycznie potrzebny. W niniejszym przykładzie aplikacja chcąca skorzystać z
interfejsu ICasual (dokładniej — z jego implementacji) odwoła się w tym celu do właściwości Casual. Przy
pierwszym odwołaniu tego rodzaju metoda dostępowa GetCasual powinna utworzyć obiekt typu Casual i
zwrócić jako wynik jego adres (przy kolejnych odwołaniach powinna zwracać wskaźnik do istniejącego obiektu
— singletonu). Jeżeli odwołania do właściwości Casual nie będzie, nie będzie też tworzony wspomniany obiekt
i nie wystąpi związane z tym obciążenie zasobów systemu.

Korzystanie z interfejsów
W tym miejscu chcielibyśmy zwrócić uwagę na pewne charakterystyczne cechy zmiennych reprezentujących
interfejsy. Po pierwsze, zmienne wskazujące na interfejsy należą do kategorii zmiennych o kontrolowanym
czasie życia (lifetime memory-managed) oraz są inicjowane przez kompilator wartością NIL. Pod drugie —
dostęp do implementowanych interfejsów kontrolowany jest przez liczniki odwołań (reference counters).
Poniższy przykład wyjaśnia w sposób poglądowy „zakulisowe” działania odzwierciedlające obydwa te
mechanizmy:

var
I: ISomeInterface;
begin
// w tym miejscu zmienna I inicjowana jest automatycznie
// wartością NIL
//------------------------------------------------------

I := (jakaś funkcja zwracająca wskazanie na interfejs


ISomeInterface)
// następuje automatyczne zwiększenie licznika odwołań
// związanego z interfejsem ISomeInterface.
//------------------------------------------------------
...
I.SomeMethod;
// interfejs ciągle jest w użyciu
...

-------------------------------------------------------
// kończy się wykonywanie bloku procedury (funkcji),
// kończy się więc czas życia zmiennej I.
// Następuje automatyczne zmniejszenie licznika odwołań
// związanego z interfejsem ISomeInterface.
// Jeżeli w wyniku tego licznik osiągnął wartość zero,
// to następuje również zwolnienie interfejsu.
end;

Inną ważną cechą każdego interfejsu (jako typu) jest jego zgodność w sensie przypisania z każdą
implementującą go klasą. Oto przykład poprawnej instrukcji przypisania (opieramy się tu na przedstawionych
wcześniej definicjach TFooBar i IFoo):
procedure Test(FB: TFooBar)
var
F: IFoo;
begin
...
F := FB; // poprawne, bowiem FB implementuje F
...

I kolejny automatyzm Object Pascala — operator as użyty w kontekście interfejsu powoduje (automatyczne)
wywołanie metody QueryInterface tegoż interfejsu — oto przykład:
var
FB : TFooBar;
F: IFoo;
B: IBar;
begin
FB := TFooBar.Create;
F := FB;
B := F as IBar;
// powyższa instrukcja równoważna jest wywołaniu
// F.QueryInterface(IBar, B);

Gdyby interfejs IFoo nie oferował udostępniania interfejsu IBar, ostatnia instrukcja spowodowałaby wyjątek
EIntfCastError.

Strukturalna obsługa wyjątków


Strukturalna obsługa wyjątków (SEH — Structured Exception Handling) stanowi mechanizm umożliwiający
aplikacji powrót do stanu normalności po wystąpieniu błędu wykonania. Wyjątki (exceptions) istniały już w
Delphi 1, lecz w Delphi 2 stworzono im nowe oblicze, integrując je z Win32 API. Materialnym wyrazem
wyjątków są obiekty zawierające niezbędną informację, obsługiwane z wykorzystaniem wszelakich zalet OOP
— poza predefiniowanymi klasami-wyjątkami Delphi użytkownik ma możliwość definiowania nowych klas
wyjątków, specyficznych dla swojej aplikacji.
Zacznijmy od przykładu — poniższy wydruk przedstawia prosty program z wbudowaną obsługą wyjątków
związanych z operacjami wejścia/wyjścia.

Wydruk 2.3. Przykładowa obsługa wyjątków wejścia/wyjścia


Program FileIO;

uses
Classes, Dialogs;

{$APPTYPE CONSOLE}

Var
F: TextFile;
S: String;
begin
AssignFile(F, 'FOO.TXT');
try
Reset(F);
try
Readln(F,S);
finally
CloseFile(F);
end;
except
on EInOutError do
ShowMessage('Błąd wejścia/wyjścia!');
end;
end;

W przedstawionej konstrukcji try ... finally ... end wykonywana jest najpierw grupa instrukcji
pomiędzy klauzulami try i finally. Po jej zakończeniu — normalnym lub na skutek wyjątku — wykonywana
jest grupa instrukcji pomiędzy finally i end. Ta grupa instrukcji wykonywana jest niezależnie od tego, jaki
był skutek wykonania instrukcji pierwszej grupy. Jest to bardzo wygodne w przypadku, gdy trzeba na przykład
bezwarunkowo zwolnić przydzielone zasoby, czy też — jak w przedstawionym przykładzie — zamknąć otwarte
pliki.

Notatka

Instrukcje zawarte pomiędzy finally a end wykonywane są niezależnie od ewentualnego wyjątku


zaistniałego w czasie wykonywania ciągu instrukcji pomiędzy try a finally. Ponadto, w czasie
wykonywania bloku instrukcji pomiędzy finally a end, wyjątek nadal istnieje, więc po pierwsze nie można
zakładać jego braku w tym momencie, po drugie, należy pamiętać, iż po wykonaniu tych instrukcji sterowanie
przekazane zostanie do najbardziej zagnieżdżonego bloku except…end obejmującego instrukcję, która
wyjątek spowodowała.

Zewnętrzna konstrukcja try ... except ... end jest właśnie podstawowym narzędziem obsługi wyjątków.
Słowo except oddziela grupę instrukcji zasadniczych od bloku dokonującego obsługi wyjątku. Dokonano więc
rozdziału miejsca, w którym wyjątek występuje od miejsca, w którym jest on obsługiwany.
Istnienie dwóch różnych konstrukcji związanych z wyjątkami — try…finally i try…except —
odzwierciedla dwojakiego rodzaju działania zapewniające aplikacji bezpieczne działanie. Oprócz obsłużenia
błędów zapewnia bezwarunkowe wykonanie pewnych krytycznych instrukcji, na przykład zwalniających
przydzieloną pamięć czy zamykających otwarte pliki.
Sama informacja o fakcie wystąpienia wyjątku jest na ogół niewystarczająca — wobec różnorodności
możliwych wyjątków ich obsługa musi być bardziej selektywna. Spójrzmy na poniższy przykład:

Wydruk 2.4. Blok obsługi wyjątków


Program Obsluga;

{$APPTYPE CONSOLE}

Var
R1, R2 : Double;
begin
While True do
begin
try
Write('Podaj liczbę rzeczywistą:');
Readln(R1);
Write('Podaj inną liczbę rzeczywistą:');
Readln(R2);
Writeln ('Teraz spróbuję podzielić wprowadzone liczby ...');
Writeln ('Iloraz wynosi ', (R1/R2) :5:2 );
except
on EZeroDivide do
Writeln('Próba dzielenia przez zero!');
on EInOutError do
Writeln('Nieprawidłowa postać liczby!');
end;
end;
end;
W powyższym przykładzie mogą być poprawnie obsłużone dwie kategorie wyjątków: dzielenia przez zero oraz
konwersji liczby z postaci znakowej na zmiennoprzecinkową. Pozostałe wyjątki pozostaną nie obsłużone, chyba
że blok obsługi zostanie wzbogacony w tzw. sekcję obsługi domyślnej (default exception handler):
Program Obsluga;

{$APPTYPE CONSOLE}

Var
R1, R2 : Double;
begin
While True do
begin
try

................

except
on EZeroDivide do
Writeln('Próba dzielenia przez zero!');
on EInOutError do
Writeln('Nieprawidłowa postać liczby!');

Else
Writeln('Błąd niesprecyzowany!');
end;
end;
end;

Sekcja obsługi domyślnej rozpoczyna się nie od słowa kluczowego on, lecz od słowa else. Podobny efekt
możemy uzyskać nie specyfikując w bloku obsługi wyjątku żadnej sekcji on ... — cały blok będzie wówczas
stanowił domyślną sekcję:
Program Obsluga;

{$APPTYPE CONSOLE}

Var
R1, R2 : Double;
begin
While True do
begin
try

................

except
Writeln('Błąd przetwarzania – coś jest nie w porządku!');
end;
end;
end;

Ostrzeżenie:

W sekcji domyślnej obsługiwane są wszystkie wyjątki, nawet te najbardziej niespodziewane, wymagające


specjalnej akcji. Wskazane jest więc ponowienie wyjątku w sekcji domyślnej — za chwilę powrócimy do tego
zagadnienia.

Wyjątki jako klasy


Jak już wcześniej wspomniano, wystąpieniu wyjątku towarzyszy utworzenie obiektu zawierającego stosowną
informację. Klasą bazową dla obiektów reprezentujących wyjątki jest klasa Exception zdefiniowana
następująco:
Type
Exception = class(TObject)
private
FMessage: string;
FHelpContext: Integer;
public
constructor Create(const Msg: string);
constructor CreateFmt
(const Msg: string; const Args: array of const);
constructor CreateRes
(Ident: Integer; Dummy: Extended = 0);
constructor CreateResFmt
(Ident: Integer; const Args: array of const);
constructor CreateHelp
(const Msg: string; AHelpContext: Integer);
constructor CreateFmtHelp
(const Msg: string; const Args: array of const;
AHelpContext: Integer);
constructor CreateResHelp
(Ident: Integer; AHelpContext: Integer);
constructor CreateResFmtHelp
(Ident: Integer; const Args: array of const;
AHelpContext: Integer);
property HelpContext: Integer read FHelpContext write
FHelpContext;
property Message: string read FMessage write FMessage;
end;

Powyższa deklaracja znajduje się w module SysUtils.


Najważniejszym elementem klasy Exception jest właściwość Message, zawierająca werbalny opis sytuacji
powodującej wystąpienie wyjątku.

Ostrzeżenie

Własne klasy wyjątków powinny być definiowane na bazie innych, prawidłowo funkcjonujących klas wyjątków,
na przykład klasy Exception. Gwarantuje się w ten sposób użycie niezbędnych, standardowych
mechanizmów obsługi wyjątków.

Ze względu na obiektową naturę wyjątków w Delphi, ich obsługa ma pewien związek ze zjawiskiem
dziedziczenia. Otóż, sekcja zdefiniowana dla określonej klasy wyjątków (po słowie on) jest również sekcją
obsługi wyjątków pochodnych. Na przykład sekcja zdefiniowana dla wyjątku EMathError będzie również
obsługiwać wyjątki EZeroDivide i EOverflow, które są typami pochodnymi w stosunku do EMathError.
Wyjątki nie obsłużone w ramach bloku except podlegają obsłudze w ramach domyślnej (dla aplikacji)
procedury obsługi wyjątków. Standardowo obsługa ta polega na wypisaniu komunikatu — pisaliśmy o tym
szczegółowo w 4. rozdziale „Delphi 4. Vademecum profesjonalisty”, w punkcie „Zmiana domyślnej procedury
obsługi wyjątków”.
Podczas obsługi wyjątku konieczne jest niekiedy uzyskanie dostępu do obiektu reprezentującego ten wyjątek —
na przykład w celu odczytania treści komunikatu ukrywającego się pod właściwością Message. Obiekt ten
dostępny jest za pośrednictwem funkcji ExceptObject (jeżeli wyjątek aktualnie nie występuje, funkcja ta
zwraca NIL), możemy się jednak do niego dostać znacznie prościej, specyfikując w sekcji on reprezentujący go
identyfikator, oddzielony dwukropkiem od identyfikatora klasy, na przykład

try

except
on E:ESomeException do
ShowMessage(E.Message);
end;

W powyższym przykładzie identyfikator E reprezentuje bieżący obiekt wyjątku klasy ESomeException. Ten
sam efekt można uzyskać w następujący sposób:
try

except
on E:ESomeException do
ShowMessage(ESomeException(ExceptObject).Message);
end;
Ponieważ we frazie else bloku except nie występuje identyfikator klasy wyjątku, nie jest także możliwa
opisana „konstrukcja z dwukropkiem”; jedynym środkiem dostępu do obiektu wyjątku pozostaje wówczas
funkcja ExceptObject.

Poza obsługiwaniem wyjątków, możliwe jest także ich generowanie w aplikacjach. Robi się to za pomocą słowa
kluczowego raise, po którym występuje wyrażenie reprezentujące obiekt wyjątku, na przykład:
raise EBadStuff.Create('Coś jest nie w porządku');

„Samotne” słowo raise, bez podania obiektu wyjątku, powoduje ponowienie wyjątku aktualnie istniejącego.

Wyjątki a przepływ sterowania w programie

W przypadku wystąpienia wyjątku sterowanie przekazane zostaje do najbliższego — czyli najbardziej


zagnieżdżonego w stosunku do instrukcji powodującej wyjątek — bloku obsługi, i być może do kolejno
zewnętrznych bloków, aż do obsłużenia wyjątku i (automatycznego) zwolnienia reprezentującego go obiektu.
Wystąpienie wyjątku związane jest ściśle z aktualnym stanem stosu wywołań podprogramów (call stack), jest
więc sytuacją globalną dla całego programu, nie zaś dla poszczególnych podprogramów.
Spójrzmy na wydruk 2.5 przedstawiający kod modułu związanego z formularzem zawierającym pojedynczy
przycisk. Kliknięcie przycisku wywołuje procedurę zdarzeniową Button1Click(). Procedura ta wywołuje
procedurę Proc1(), która wywołuje procedurę Proc2() — ta z kolei wywołuje procedurę Proc3(). W
procedurze Proc3() generowany jest wyjątek; uruchamiając aplikację i śledząc wyświetlane komunikaty,
możemy zaobserwować przepływ sterowania aż do momentu obsłużenia wyjątku.
Wydruk 2.5. Ilustracja przepływu sterowania podczas wystąpienia wyjątku
unit Main;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}
type
EBadStuff = class(Exception);

Procedure Proc3;
begin
try
raise EBadStuff.Create('Dzieje się coś niedobrego!');
finally
ShowMessage('Wystąpił wyjątek, który procedura Proc3
zauważyła');
end;
end;

Procedure Proc2;
begin
try
Proc3;
finally
ShowMessage('Procedura Proc2 również jest świadoma
wyjątku');
end;
end;
Procedure Proc1;
begin
try
Proc2;
finally
ShowMessage('Procedura Proc1 również jest świadoma
wyjątku');
end;
end;

procedure TForm1.Button1Click(Sender: TObject);


const
ExceptMsg = 'Wystąpił wyjątek określony jako "%s"';
begin
ShowMessage('Występuje łańcuch wywołań Proc1->Proc2->
Proc3');
try
Proc1;
except
on E:EBadStuff do
ShowMessage(Format(ExceptMsg, [E.Message]));
end;
end;

end.

Wykonanie generującej wyjątek instrukcji raise (w procedurze Proc3()) powoduje przekazanie sterowania do
bloku finally; wyjątek pozostaje nieobsłużony, sterowanie wraca do procedury Proc2().
Z punktu widzenia procedury Proc2() instrukcja wywołująca procedurę Proc3() jest instrukcją powodującą
wyjątek; sterowanie wędruje więc do bloku finally (oczywiście w procedurze Proc2()); wyjątek pozostaje
nieobsłużony, sterowanie wraca do Proc1().
Tu sytuacja się powtarza — instrukcja wywołująca Proc2() uważana jest za instrukcję powodującą wyjątek;
sterowanie trafia do bloku finally, a następnie do procedury Button1Click(). Wyjątek pozostaje
nieobsłużony.
Z punktu widzenia procedury Button1Click() za wyjątek odpowiedzialna jest instrukcja wywołująca
procedurę Proc1(). Instrukcja ta znajduje się w obszarze konstrukcji try…except…end, sterowanie wędruje
więc do bloku except i wyjątek zostaje wreszcie obsłużony.

Wskazówka

Opisaną sytuację możesz prześledzić samodzielnie, ustawiając punkt przerwania (breakpoint) na instrukcji
wywołującej procedurę Proc1() i kontynuując wykonanie w sposób krokowy. Musisz jedynie zadbać o to, by
nie przeszkadzały Ci w tym procedury obsługi wyjątków w zintegrowanym debuggerze — wyłącz je poprzez
usunięcie zaznaczenia opcji Stop on Delphi Exceptions na karcie Language Exceptions opcji
debuggera (Tools|Debugger Options).

Ponowienie wyjątku
Gdy wskutek wystąpienia wyjątku sterowanie trafia do odpowiedniej sekcji on… w bloku except (lub w ogóle
do bloku except w przypadku braku podziału na sekcje), po wyjściu sterowania z tego bloku wyjątek jest
obsłużony i reprezentujący go uprzednio obiekt już nie istnieje. Nie dotyczy to jednak wyjątków zaistniałych w
trakcie realizacji bloku except — wędrują one do bloku except na wyższym poziomie zagnieżdżenia.
Gdy w poniższej funkcji RPower wystąpi wyjątek, jedynym jego sygnałem będzie zwrócenie zerowego wyniku
— gdyż do tego sprowadza się obsługa wszelkich wyjątków w bloku except.
Function RPower(X, Y: Real):Real;
begin
try
Result := exp(Y*ln(X));
except
Result := 0.0;
end;
end;

Rozsądniej będzie jednak powierzyć obsługę ewentualnego wyjątku zewnętrznym blokom except, regenerując
(ponawiając) go za pomocą instrukcji raise:

Function RPower(X, Y: Real):Real;


begin
try
Result := exp(Y*ln(X));
except
Result := 0.0;
raise;
end;
end;

RTTI
Pod tytułowym skrótem kryje się mechanizm udostępniający uruchomionej aplikacji informacje o jej obiektach
— ang. Runtime Type Information. Informacja ta wykorzystywana jest także przez środowisko IDE, warunkując
jego prawidłową współpracę z komponentami na etapie projektowania aplikacji.
Elementy zapewniające łączność obiektu ze strukturami danych RTTI wbudowane są już w klasę TObject,
posiada je zatem każdy obiekt Delphi. Najważniejsze z metod udostępniających informację RTTI opisane są w
tabeli 2.8.

Tabela 2.8. Ważniejsze metody klasy TObject udostępniające informację z kategorii RTTI
Metoda Typ wyniku Znaczenie
ClassName() String Nazwa klasy
ClassType() TClass Klasa jako typ17
InheritsFrom() Boolean Informuje o istnieniu lub nieistnieniu relacji
dziedziczenia między klasami
ClassParent() TClass Typ macierzysty w stosunku do danego
ClassInfo() Pointer Wskaźnik do bloku RTTI w pamięci

Do kategorii RTTI należą także dwuargumentowe operatory is i as. Pierwszy z nich zwraca wartość boolowską
informującą o tym, czy obiekt stanowiący lewy operand zalicza się do klasy identyfikowanej18 przez prawy
operand. Poniższa funkcja zwraca nazwę klasy obiektu przekazanego jako parametr; jeżeli jednak obiekt ten jest
obiektem klasy TEdit lub pochodnej, zwracana jest także zawartość jego właściwości Text:
function CoZaObiekt(X:TObject):String;
begin

17
W Object Pascalu zbiór wszystkich typów pochodnych w stosunku do danej klasy składa się na typ wyższego rzędu,
zwany klasowym typem referencyjnym (class-reference type) lub metaklasą (metaclass). Deklaracja metaklasy ma postać
class of typ, gdzie typ jest nazwą klasy macierzystej. Najbardziej ogólną metaklasą jest w Class of TObject
obejmująca wszystkie klasy Object Pascala; jej synonimem jest TClass. Podobnie jak inne typy, również metaklasy mogą
posiadać swoje zmienne; wartością każdej takiej zmiennej jest wskazanie na konkretną klasę (nie obiekt!), a dokładniej — na
związaną z tą klasą tablicę VMT (przyp. tłum.).
18
za pomocą nazwy typu lub zmiennej metaklasy (przyp. tłum.)
Result := X.ClassName();
if X is TEdit
then
Result := Result + '(' + TEdit(X).Text + ')';
end;

Operator as dokonuje bezpiecznego rzutowania typów obiektowych. Konstrukcja X as Y, gdzie X identyfikuje


obiekt, a Y klasę, równoważna jest konstrukcji Y(X), pod warunkiem, iż prawdziwa jest relacja X is Y; w
przeciwnym razie wynikiem operatora as jest wartość NIL. Za pomocą operatora as można by napisać funkcję
CoZaObiekt następująco:
function CoZaObiekt(X:TObject):String;
var
P: TEdit;
begin
Result := X.ClassName();
P := X as TEdit;
if P <> NIL
then
Result := Result + '(' + P.Text + ')';
end;

Informacja RTTI jest nowością Delphi; nie było jej w Turbo Pascalu — jeżeli nie liczyć funkcji TypeOf()
równoważnej (w pewnym sensie) metodzie ClassType(), lecz zwracającej (amorficzny) wskaźnik do tablicy
VMT.

Podsumowanie
W niniejszym rozdziale przedstawiliśmy najważniejsze cechy języka Object Pascal stanowiącego
„lingwistyczne” fundamenty Delphi. Opisaliśmy najważniejsze elementy składni i semantyki języka — zmienne,
operatory, funkcje, procedury, typy oraz instrukcje. Zajęliśmy się także podstawami realizacji programowania
obiektowego, wyjaśniając najważniejsze elementy i koncepcje związane z tą filozofią: pola, metody i
właściwości obiektów oraz enkapsulację, dziedziczenie i polimorfizm; zaprezentowaliśmy także proste
przykłady implementacji interfejsów przez obiekty. Na zakończenie omówiliśmy podstawowe zasady
generowania i obsługi wyjątków oraz najistotniejsze elementy związane z mechanizmem RTTI.

You might also like