5680


Rozdział 2.

Język Object Pascal

Pozostawmy na chwilę wizualne aspekty Delphi i przyjrzyjmy się bliżej sta­nowią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:

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 sprzecz­ne 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 prze­szkó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ęzy­kó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) pro­cedur 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 eg­zemplarz (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 proce­dury (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 przypi­suje się wartość domyślną — nie mogą w tej roli wystąpić rekordy, zmienne warianto­we, 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ć kompila­torowi 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 blo­kiem 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 pisow­ni 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, mia­nowicie w treści programu. Nowością, która pojawiła się w Delphi 2 jest możliwość ini­cjowania 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, zmienno­przecinkowe — 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 skalar­nych 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[Author:AG] 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 4.294.967.295

Cardinal

od 4.294.967.296 do 2^63-1

Int64

Ponadto

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. Pro­gramista 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 ta­blicy. 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 preproceso­ra 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. sym­bole 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że­nia 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 zo­stał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) opera­cje wynikające z algebry Boole'a (stąd często nazywane bywają operatorami bool­owskimi — 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

Zaprzeczenie

not

!

Not

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 dziele­nia 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 zmiennoprzecin­kowego 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 proce­sory 80×86).

Operatory bitowe

Operatory bitowe służą do operowania na poszczególnych bi­tach 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 wy­stę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 roz­poznaje operację zmniejszania/zwiększania zmiennej za pomocą zwykłej ope­racji dodawania lub odejmowania, tak więc przekład instrukcji x := x + 1 nie różni się od przekładu instrukcji Inc(x), 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 przedsta­wia 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 zmien­nych biorących udział w operacjach i będących parametrami wywołań procedur i funk­cji. 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 ze znakiem

ShortInt

byte

char

nie istnieje

całkowity 8-bitowy bez znaku

Byte

nie istnieje

BYTE, unsigned short

Byte

całkowity 16-bitowy ze znakiem

SmallInt

short

short

Short

całkowity 16-bitowy bez znaku

Word

nie istnieje

unsigned short

nie istnieje

całkowity 32-bitowy ze znakiem

Integer, LongInt

int

int, long

Integer, Long

całkowity 32-bitowy bez znaku

Cardinal, LongWord

nie istnieje

unsigned long

nie istnieje

całkowity 64-bitowy ze znakiem

Int64

long

__int64

nie istnieje

zmiennoprzecinkowy 4-bajtowy

Single

float

float

Single

zmiennoprzecinkowy 6-bajtowy

Real48

nie istnieje

nie istnieje

nie istnieje

zmiennoprzecinkowy 8-bajtowy

Double

double

double

Double

zmiennoprzecinkowy 10-bajtowy

Extended

nie istnieje

long double

nie istnieje

stałoprzecinkowy 64-bitowy

Currency

nie istnieje

nie istnieje

Currency

data/czas 8-bajtowy

TDateTime

nie istnieje

nie istnieje

Date

wariantowy 16-bajtowy

Variant, Olevariant, TVarData

nie istnieje

VARIANT** Variant†, Olevariant

Variant (domyślny)

znak 1-bajtowy

Char

nie istnieje

char

nie istnieje

znak 2-bajtowy

WideChar

char

WCHAR

Char

łańcuch znaków o ustalonej
maksymalnej długości

ShortString

nie istnieje

nie istnieje

nie istnieje

dynamiczny łańcuch znaków 1-bajtowych

AnsiString

nie istnieje

AnsiString

String

łańcuch znaków jednobajtowych z zerowym ogranicznikiem

PChar

nie istnieje

char *

nie istnieje

łańcuch znaków dwubajtowych z zerowym ogranicznikiem

PWideChar

nie istnieje

LPCWSTR

nie istnieje

dynamiczny łańcuch znaków dwubajtowych

WideString

String**

WideString

nie istnieje

boolowski 1-bajtowy

Boolean, ByteBool

boolean

(dowolny 1-bajtowy)

nie istnieje

boolowski 2-bajtowy

WordBool

nie istnieje

(dowolny 2-bajtowy)

Boolean

boolowski 4-bajtowy

BOOL, LongBool

nie istnieje

BOOL

nie istnieje

† — 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 IntegerCardinal, 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 zmien­noprzecinkowej, obsługiwany całkowicie w sposób programowy i nie mający od­powiednika w formatach danych (ko)procesorów. Począwszy od Delphi 4, iden­tyfikator Real jest synonimem typu Double; wspomnianemu for­matowi 6-bajtowemu odpowiada natomiast identyfikator Real48. Możliwe jest jednak przywró­cenie dawnego znaczenia identyfikatora Real — poprzez użycie dyrektywy kompi­latora {$REALCOMPATIBILITY ON}

Znaki

W Delphi istnieją trzy typy reprezentujące pojedynczy znak:

Fakt, że znak niekoniecznie jest teraz jednobajtowy, stanowi przesłankę nieco ostrożniejszego kodowania, a przy ustalaniu rozmiaru struktur zawierających znaki, wskazane jest korzy­stanie 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 aspek­cie języka.

W Object Pascalu obsługa łańcuchów znaków zrealizowana została w postaci następują­cych typów:

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 ograni­czenia 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żytkow­nika 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 pro­gramistę od „ręcznej” obsługi pamięci, tak uciążliwej w C++ i wersji 7.0 Borland Pas­cala (typ PChar). Strukturę łańcucha AnsiString w pamięci operacyjnej przedstawia rysunek 2.1.

0x01 graphic

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ńcu­chowej zawartości innej zmiennej nie spowoduje fizycznego kopiowania, lecz tylko po­wielenie 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, zmniej­szenie 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 wy­jaś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>[Author:AG]

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 najpopu­larniejszych 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) za­wartymi 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 po­mocy pod hasłem „String-handling routines (Pascal-style)”. Dodatkowo, kilka uży­tecznych 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 katalo­gu \Source\Utils.

Długość łańcucha i alokacja pamięci

Bezpośrednio po zadeklarowaniu, zmienna łańcuchowa nie posiada przydzielonej pa­mięci i nie reprezentuje żadnego napisu. Przypisanie jej jakiegoś napisu spowoduje przy­dział 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 wyrafino­wany, 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 przy­datność do tego celu. Potrzebny jest jedynie drobny zabieg kosmetyczny — kompilator nie zaakceptuje zmiennej typu String jako parametru aktualnego w miejscu, gdzie wy­magany 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łu­gość 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 towarzy­szą dodatkowe informacje organizacyjne. Procedury Win32 API, traktując parametry jako ciągi znaków zakończone bajtem zerowym (i nic ponadto), nie są owych „dodat­kó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łącz­nika $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 po­przedzony bajtem oznaczającym jego długość, w wersjach następnych, przy ustawionym przełącz­niku $H+, typ String reprezentuje długi łańcuch, mający diametralnie różną strukturę. W związku z tym:

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.

0x01 graphic

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

Zarządzanie krótkimi łańcuchami nie wiąże się z dynamicznym gospodarowaniem pa­mię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 ogranicz­nika. 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:

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 funk­cji 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 przy­kł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 wy­róż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 ogranicz­nikiem — 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 dodat­kowe typy omawianych łańcuchów: PWideChar i PAnsiChar.

Jak łatwo wywnioskować z powyższego opisu, łańcuch z zerowym ogranicznikiem repre­zentowany 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źni­kowymi (pointers).

0x01 graphic

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 zwal­nianie 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ą konstruk­cji 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 ka­talogu \SOURCE\UTIL na załączonym krążku CD-ROM.

Typy wariantowe

Typ Variant jest w Pascalu zupełną nowością; konsekwencją wspomnianego wcześniej bez­pieczeństwa typów jest absolutne ustalenie typu każdej zmiennej już na etapie kompila­cji. Takie podejście nie da się jednak pogodzić ze standardami programowania w Win­dows, 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ę zmien­nej, czyli zmiany jej typu stosownie do kontekstu wykonywanego programu. 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 auto­matyzacji OLE. Nieprzypadkowo więc „delphicka” implementacja typu Variant jest 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 wyko­nywania 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 skon­centrujemy się głównie na typie Variant, a typ OLEvariant będziemy przywoływać tylko w kon­tekś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 ty­pu 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 da­ty/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 strukturze:

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);

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 struk­tury 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ść nie­okreś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ńcu­chową — 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 po­mocą 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 jaw­ny 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 ty­pó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:

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ą (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 conversion.

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.

Jest rzeczą oczywistą, że zmienne wariantowe są wyraźnym odstępstwem od zasady bez­pieczeństwa typów. To prawda, jednak bez nich wykorzystanie mechanizmu OLE było­by 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 ewentual­nych 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łu­gują na obszerniejsze omówienie. Pierwszą z nich jest UNASSIGNED, oznaczająca, że zmien­na 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 reprezento­wana 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 zagad­nieniem 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 operan­du 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 NULL.

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 znalezie­niu 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 znacz­nie 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ą wa­riantową może być wskazanie na (być może heterogeniczną) tablicę. Poniższy fragment programu

var

V: variant;

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 te­go 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 prze­kazanej jako pierwszy parametr (notabene parametr ten stanowi przykład tablicy otwar­tej — tablicami otwartymi zajmiemy się w dalszej części rozdziału), na­tomiast 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, po­za 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 wa­riantowymi, 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 jed­nakowego typu — i tak, np. tablica array [ 1 .. 10 ] of real jest 10-elemento­wym 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ą ortogo­nalną, 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ę warian­tową 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 Pas­cal oferuje kilka równie użytecznych podprogramów związanych z tablicami warianto­wymi. Zdefiniowane są one w module System — oto ich nagłówki, następ­nie 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 hierar­chii wymiaru tablicy — to ten wymiar, który identyfikowany jest przez ostatni (skrajny, prawy) indeks. Istniejące elementy tablicy zostają zachowane, ewentualne nowe elemen­ty 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 funk­cji 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 oczywi­sty 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ść kosztow­ne, 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 znacz­na 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 zablokowa­nej 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 wek­tora elementów w pamięci operacyjnej, dzięki czemu możliwe jest wykonywanie pew­nych 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 wskazy­wana 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 wpi­sanie 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 wska­zany 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 za­inicjowana:

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 podpro­gramy biblioteki RTL, uruchamiane w wyniku pojedynczej instrukcji przypisania

Result := V;

Wreszcie, funkcje VarFromDateTime() i VarToDateTime() dokonują konwersji po­mię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 prze­chowywania liczb rzeczywistych reprezentujących wielkości, co do których wymagana jest bezwzględna dokładność — głównie kwot pieniężnych. Wewnętrzną jego reprezen­tacją jest 64-bitowa liczba całkowita ze znakiem, zawierająca wartość 10000 razy więk­szą 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 „prze­programowanie” na postać typu Currency danych finansowych reprezentowanych do­tychczas 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 mu­si 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 rozpoczy­nają 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żyt­kownika. 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 definio­wanie 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 znako­we (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 Pasca­la). 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 wielowy­miarowej, 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 defi­niuje liczbę jej wymiarów i typ elementów, jednak nie definiuje a priori indeksów gra­nicznych — „rozpiętości” poszczególnych wymiarów określone zostaną dopiero w trak­cie 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 jed­nakż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 oczy­wiś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 cza­sie wykonania instrukcji SetLength(…) wartością N było (powiedzmy) 5, to od tej po­ry 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 jed­nakż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 momen­cie 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, pod­stawiają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 za­wartość przestała już być potrzebna.

Innym mechanizmem charakterystycznym dla tablic dynamicznych jest oszczędność go­spodarowania 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 wspomnia­na 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 1.

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 dy­namicznych (pojęcie ortogonalności tablicy wyjaśnione zostało przy opisie tablic wa­riantowych). Jest taka możliwość, gdyż macierz może być rozpatrywana jako wektor wektorów, które w przypadku tablicy dynamicznej (i ta­blic 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 definio­wany (user-defined type) Visual Basica. Oto przykład rekordu w języku Object Pas­cal oraz jego odpowiedniki w C i Visual Basicu:

{ 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ą po­stać 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ą warianto­wą (uwaga: nie mylić ze zmiennymi typu Variant!). Interpretacja części zmiennej re­kordu 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 ty­pó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 je­go 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 zdefinio­wano — nie przekracza on więc nigdy 32 bajtów. W szczególności, zbiory oparte na ty­pach 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 łado­wane do rejestrów procesora.

Stałe oznaczające zbiory zapisuje się w nawiasach prostokątnych jako ogranicz­nikach, 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 pro­ste 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 zbio­rze 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 na­leżą do przynajmniej jednego z nich. Różnicę zbiorów A i B, oznaczaną A - B, two­rzą 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() doko­nują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żą jed­nocześ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 ob­szerny 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 „regu­larnymi” 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 posia­da swój odpowiednik wskaźnikowy (regułę tę należy stosować rekurencyjnie — istnie­ją 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ąz­ku 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 oczy­wiś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>[Author:AG]

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(), 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 do­tyczą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 zgod­nie 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 — kom­pilator nie wchodzi w szczegóły definicji typów i ewentualna identyczność dwóch różnych de­klaracji 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.

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 przy­kł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 parame­trów opatrzonych klauzulami var i out. Poniższe fragmenty zostaną więc przez kompi­lator 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 rygorystycz­nej 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 bajt — 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 przy­pisania 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 spo­só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 dal­szej części książki.

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 przy­padkową. 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 teksto­wych, 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 nie­wą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ę wybo­ru 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 za­pomnij 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), na­tomiast 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. Przedstawi­my trzy różne rodzaje pętli w Object Pascalu — w języku C istnieją podobne konstruk­cje, 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 za­broniona; 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 pobrania).

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 blo­ku instrukcji sprawdzany jest po jego wykonaniu, więc pętla zostaje wykonana przy­najmniej 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.

Procedura Break()

Wywołanie procedury Break wewnątrz pętli While, Repeat lub For powoduje jej na­tychmiastowe zakończenie. Jeżeli pętle są zagnieżdżone, następuje zakończenie najbar­dziej 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 po­rzucenie 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ślo­nego 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 nie­zwykle rygorystycznym regułom, zwiększającym co prawda bezpieczeństwo programo­wania, lecz jednocześnie ograniczającym w znacznym stopniu swobodę i naturalność wy­rażania koncepcji programistycznych — wiedzą o tym aż nadto dobrze ci, którym przy­szł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ą rewolu­cję 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 lo­kalnej 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 wyko­rzystaniem 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ć świado­mość opisanego zjawiska. Deklaracja parametru przekazywanego przez wartość nie za­wiera ż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 wy­konywane 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 klu­czowym 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 prze­ciwnym wypadku wystąpi błąd kompilacji.

Przekazywanie parametru przez zmienną nie powoduje obciążenia stosu, na którym „od­kł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ładnio­wym, parametr taki podlega tym samym regułom, co parametr przekazywany przez war­tość; jednak z drugiej strony, na stosie odkładany może być adresparametru,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ść parametru.

Deklaracja parametru przekazywanego przez stałą polega na poprzedzeniu jego nazwy słowem klu­czowym 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 najodpowied­niejszym 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ż 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 naturalne. 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;

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ól­nych deklaracji w poszczególnych fragmentach programu. I tak, zmienne globalne, de­klarowane 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:

Ponadto, w module mogą opcjonalnie wystąpić następujące elementy:

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:

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ęp­na idea, której skutki w rewolucjonizowaniu procesu projektowania oraz programowa­nia 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 (OOPObject 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:

0x01 graphic

Rysunek 2.4. Ilustracja koncepcji dziedziczenia

--> <ramka>[Author:AG]

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 bar­dziej rozbudowany; oferuje dziedziczenie od wielu obiektów jednocześnie (multi­ple 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ępu­jące terminy:

Wskazówka

Bezpośrednie operowanie na polach obiektu, choć możliwe, jest zasadni­czo 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 jed­nocześ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 definio­wania 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 klasami, 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 wy­zerowaniem 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ńcu­chy 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 kon­kretnego egzemplarza obiektu, który mamy zamiar unicestwić:

FooObject.Destroy;

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(), destruk­tora Destroy() i metody Free() w każdym typie obiektowym? Odpowiedź na to py­tanie 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).

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ła­nie 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 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 Win­dows 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ż na­tura 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:

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ą.

Interfejsy

Interfejsy (interfaces) stanowią specjalną kategorię klas, związaną z wykorzysta­niem 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 funk­cji 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 in­terfejs 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:

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 ka­tegorii 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 przypisa­nia (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 za­wierają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 wykona­nia instrukcji pierwszej grupy. Jest to bardzo wygodne w przypadku, gdy trzeba na przy­kład bezwarunkowo zwolnić przydzielone zasoby, czy też — jak w przedstawionym przy­kł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 ob­sługi wyjątku. Dokonano więc rozdziału miejsca, w którym wyjątek występuje od miej­sca, 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 za­wierającego stosowną informację. Klasą bazową dla obiektów reprezentujących wy­ją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 zdefi­niowana dla wyjątku EMathError będzie również obsługiwać wyjątki EZeroDivideEOverflow, 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ślne­j (dla aplikacji) procedury obsługi wyjątków. Standardowo obsługa ta polega na wypisaniu ko­munikatu — 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 typ

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 identyfikowanej 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

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.

Z wyjątkiem sytuacji, gdy x jest właściwością klasy (przyp. tłum.)

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.).

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.

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.).

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.).

Chyba że drugi operand posiada wartość UNASSIGNED (przyp. tłum.).

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.).

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.).

O takiej samej reprezentacji bitowej (przyp. tłum.).

a nie po nieudanej próbie odczytania linii, dlatego musi być sprawdzany przed odczytem (przyp. tłum.)

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.).

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

end;

procedure Inner(X: PMyRec);

begin

X^.a := 1;

end;

procedure Outer(const Y: TMyRec);

begin

Inner(@Y);

end;

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.).

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.).

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.).

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 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.).

za pomocą nazwy typu lub zmiennej metaklasy (przyp. tłum.)

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

84 C:\HELION\DELPHI6VP\AGpojezykowej\r02-04.doc

Tej tabeli nie ma w oryginale

ale bez kwalifikowania „wskazówka/notatka/ostrzeżenie”; jeśli jednak trzeba byłoby kwalifikować, to najbardziej pasuje „notatka”

bez kwalifikacji; w razie czego najbardziej odpowiednia „wskazówka”

bez kwalifikacji, w razie czego „notatka”



Wyszukiwarka