Rozdział 3. Język Delphi

Adam Boduch

Niezbędnym elementem każdego języka programowania jest składnia. Słowem tym określa się specyficzne słowa kluczowe (elementy danego języka służące do sterowania pracą programu) oraz znaki, które muszą zostać zapisane w określonym porządku.

Składnia języka Delphi (dawniej Object Pascal) nie odbiega zbytnio od tej znanej z Turbo Pascala, dlatego dla programistów Pascala niniejszy rozdział może stanowić pewną powtórkę materiału, lecz dla amatorów programowania jest wręcz obowiązkowy.

Aby nauka była łatwiejsza, zrezygnujemy tutaj z używania komponentów, formularzy i — ogólnie — projektowania wizualnego. Zamiast tego będziemy tworzyć programy od podstaw, używając najprostszych konstrukcji języka.

Rozdział ten jest poświęcony jedynie konstrukcji samego języka Delphi. Nie będziemy tutaj wnikać w funkcje charakterystyczne dla .NET — zajmiemy się wyłącznie prostymi funkcjami, które są obecne w Delphi od lat. Więcej informacji na temat funkcji oferowanych przez platformę .NET znajduje się w dalszej części książki.

W rozdziale:

*omówię podstawowe elementy języka Delphi,
*przedstawię pojęcia: programowanie proceduralne i strukturalne,
*powiem, jak pisać pętle,
*omówię, czym są instrukcje warunkowe,
*powiem, czym są schematy blokowe,
*opiszę parę sztuczek dotyczących efektownego programowania.

     1 Aplikacje konsolowe
          1.1 Zapisywanie projektu
          1.2 Po kompilacji…
     2 Najprostszy program
     3 Podstawowa składnia
          3.3 Czytanie kodu
          3.4 Wielkość liter
          3.5 Pamiętaj o średniku!
          3.6 Bloki begin i end
          3.7 Dyrektywa program
     4 Komentarze
          4.8 Umiejętne komentowanie
     5 Wypisywanie tekstu
          5.9 Położenie instrukcji
          5.10 Instrukcja Writeln
     6 Zmienne
          6.11 Deklaracja zmiennych
          6.12 Typy danych
          6.13 Deklaracja kilku zmiennych
          6.14 Przydział danych
               6.14.1 Przydział statyczny
               6.14.2 Przydział dynamiczny
          6.15 Deklaracja zakresu danych
          6.16 Restrykcje w nazewnictwie
     7 Stałe
          7.17 Domyślne typy stałych
     8 Używanie zmiennych w programie
          8.18 Łączenie danych
     9 Tablice danych
          9.19 Tablice jako stałe
          9.20 Tablice wielowymiarowe
          9.21 Tablice dynamiczne
          9.22 Polecenia Low i High
               9.22.3 Odczytywanie najmniejszej i największej wartości
     10 Operatory
          10.23 Operatory przypisania
          10.24 Operatory porównania
          10.25 Operatory logiczne
               10.25.4 Typ Boolean
          10.26 Operatory arytmetyczne
               10.26.5 Funkcje zwiększania i zmniejszania
          10.27 Operatory bitowe
          10.28 Pozostałe operatory
     11 Instrukcje warunkowe
          11.29 Instrukcja if..then
               11.29.6 Pobieranie tekstu z konsoli
               11.29.7 Kilka instrukcji po słowie then
               11.29.8 Kilka warunków do spełnienia
          11.30 Instrukcja case..of
               11.30.9 Zakresy
               11.30.10 Brak możliwości korzystania z ciągów znakowych
               11.30.11 Kilka instrukcji
          11.31 Instrukcja else
               11.31.12 Kiedy stosować średnik, a kiedy nie?
               11.31.13 Kilka instrukcji if i else
               11.31.14 Kilka instrukcji po słowie begin
               11.31.15 Instrukcja else w case..of
     12 Programowanie proceduralne
     13 Procedury i funkcje
          13.32 Procedury
               13.32.16 MessageBox
          13.33 Funkcje
          13.34 Zmienne lokalne
          13.35 Parametry procedur i funkcji
               13.35.17 Kilka parametrów procedur lub funkcji
               13.35.18 Tablica jako parametr procedury
               13.35.19 Tablice zwracane przez funkcje
          13.36 Parametry domyślne
               13.36.20 Parametry tego samego typu a wartości domyślne
               13.36.21 Kolejność wartości domyślnych
          13.37 Przeciążanie funkcji i procedur
          13.38 Przekazywanie parametrów do procedur i funkcji
               13.38.22 Przekazywanie parametrów przez wartość
               13.38.23 Przekazywanie parametrów poprzez stałą
               13.38.24 Przekazywanie parametrów przez referencję
          13.39 Procedury zagnieżdżone
          13.40 Wplatanie funkcji i procedur
     14 Własne typy danych
          14.41 Tablice jako nowy typ
     15 Aliasy typów
     16 Rekordy
          16.42 Przekazywanie rekordów jako parametrów procedury
          16.43 Deklarowanie rekordu jako zmiennej
          16.44 Instrukcja packed
          16.45 Deklarowanie tablic rekordowych
          16.46 Deklarowanie dynamicznych tablic rekordowych
     17 Instrukcja wiążąca with
     18 Programowanie strukturalne
     19 Moduły
          19.47 Tworzenie nowego modułu
          19.48 Budowa modułu
               19.48.25 Nazwa
               19.48.26 Sekcja Interface
               19.48.27 Sekcja Implementation
          19.49 Włączanie modułu
          19.50 Funkcje wbudowane
          19.51 Sekcje Initialization oraz Finalization
          19.52 Dyrektywa forward
     20 Konwersja typów
     21 Rzutowanie
          21.53 Parametry nieokreślone
     22 Pętle
          22.54 Pętla for..do
               22.54.28 Odliczanie od góry do dołu
               22.54.29 Licznik pętli
               22.54.30 Zwalnianie wykonywania pętli
          22.55 Pętla while..do
          22.56 Pętla repeat..until
          22.57 Pętla for-in
               22.57.31 Konstrukcja for-in w połączeniu z łańcuchami
          22.58 Polecenie Continue
          22.59 Polecenie Break
     23 Zbiory
          23.60 Przypisywanie elementów zbioru
          23.61 Odczytywanie elementów ze zbioru
               23.61.32 Zaprzeczanie
               23.61.33 Przekazywanie zbioru jako parametru procedury
          23.62 Dodawanie i odejmowanie elementów zbioru
               23.62.34 Include i Exclude
     24 Typy wariantowe
          24.63 VarType, VarTypeAsText
          24.64 VarToStr
          24.65 VarIs*
     25 Pliki dołączane
     26 Etykiety
     27 Dyrektywy ostrzegawcze
     28 Wstęp do algorytmiki
          28.66 Schematy blokowe
               28.66.35 Przykładowy schemat blokowy
               28.66.36 Jak tworzyć schematy blokowe?
               28.66.37 Oznaczenia figur
                    28.66.37.1 Elipsa
                    28.66.37.2 Równoległobok
                    28.66.37.3 Prostokąt
                    28.66.37.4 Romb
          28.67 Przykład — obliczanie silni
               28.67.38 Schemat blokowy
     29 Efektywny kod
          29.68 Instrukcje warunkowe
          29.69 Typ Boolean w tablicach
          29.70 Zbiory
          29.71 Łączenie znaków w ciągach
     30 Test
     31 FAQ
     32 Podsumowanie

Aplikacje konsolowe

Ten, kto programował w Turbo Pascalu, już wie, że pisane w nim programy były wykonywane w środowisku MS DOS. Można było oczywiście uruchomić taki program pod kontrolą systemu Windows — wówczas otwierało się okno DOS-a, w którym był uruchamiany taki program. Okno to będziemy nazywać konsolą.

W Delphi także istnieje możliwość wykonywania aplikacji konsolowych a’la Turbo Pascal.

W tym celu należy wykonać następujące czynności:

#Z menu File wybierz New, a następnie Other.
#Zaznacz kategorię Delphi Projects.
#Wybierz pozycję Console Application.
#Kliknij OK.

Wskutek wykonania tych czynności Delphi powinno otworzyć nowy projekt bez formularza, składający się jedynie z edytora kodu. W edytorze kodu powinien się znaleźć kod podobny do tego z listingu 3.1.

Listing 3.1. Kod źródłowy pliku projektu

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  { TODO -oUser -cConsole Main : Insert code here }
end.

W rzeczywistości program działa jako konsolowy dzięki tzw. dyrektywie {$APPTYPE CONSOLE}. Jest to specjalne oznaczenie dla kompilatora, lecz tym na razie nie trzeba sobie zaprzątać głowy.

Zapisywanie projektu

Aplikacje konsolowe charakteryzują się takimi samymi elementami jak normalne aplikacje, zawierające formularze i komponenty. Można również powiedzieć, że takie uproszczone projekty co prawda nie wykorzystują wszystkich dobrodziejstw Delphi, jednak są dzięki temu bardziej przejrzyste, zajmują mniej zasobów, nie wymagają wielu linii kodu źródłowego.

Aby zapisać projekt, należy postępować według poniższych instrukcji:

#Z menu File wybierz Save All.
#Wpisz nazwę projektu — np. My1st_proj.
#Kliknij OK.

Warto teraz zwrócić uwagę, że w katalogu, w którym zapisano projekt, znajdują się tylko cztery pliki. Jest tak dzięki temu, że nie korzystaliśmy z formularzy i komponentów:

*My1st_proj.dpr — główny plik projektu, zawierający kod źródłowy programu,
*My1st_proj.cfg — ustawienia kompilatora,
*My1st_proj.bdsproj — ustawienia projektu zapisane w języku XML,
*My1st_proj.bdsproj.local — inne ustawienia użytkownika.

Po kompilacji…

Teraz można spróbować skompilować swój program. Nie warto go nawet uruchamiać, gdyż niczego nie wykona — zakończy swoje działanie, tak że trudno będzie się zorientować, że pracował. Należy jedynie wybrać z menu Project polecenie Compile My1st_proj. Spowoduje to utworzenie pliku wynikowego .exe w katalogu głównym programu. W zależności od wcześniejszych ustawień, oprócz plików, które wcześniej znajdowały się w folderze projektu, Delphi może utworzyć tzw. kopie zapasowe. Takie pliki posiadają w rozszerzeniu nazwy znak tyldy (~) i są tworzone na wypadek, gdyby oryginalnym wersjom przydarzyło się coś złego.

Często kopie zapasowe plików stają się po prostu niepotrzebne. Aby wyłączyć tę opcję, wystarczy rozwinąć menu Tools => Options, a następnie zaznaczyć kategorię Editor Options. Na panelu powinna znaleźć się pozycja Create backup file, z której należy usunąć zaznaczenie.

Najprostszy program

Teraz rozłożę zawartość głównego pliku programu na części, dzięki czemu Czytelnik będzie miał możliwość zaobserwowania, jakie funkcje pełnią poszczególne elementy kodu źródłowego.

Kod źródłowy najprostszego do napisania programu jest przedstawiony poniżej:

end.

To nie jest żart! Najprostszy program składa się właśnie z instrukcji end z kropką na końcu. Łatwo to sprawdzić — należy po prostu nacisnąć klawisz F9, uruchamiając w ten sposób program. Oczywiście żadne polecenia oprócz end nie są wpisane, dlatego program zaraz po uruchomieniu zostanie zamknięty.

Podstawowa składnia

Kod źródłowy musi składać się z poleceń zakończonych określonymi znakami. Nie można pozostawić w kodzie żadnego bałaganu — nawet pominięcie jednego znaku czy zwykła literówka mogą spowodować brak możliwości uruchomienia programu. Takie banalne z pozoru błędy mogą być prawdziwą udręką dla początkującego programisty i w konsekwencji spowodować spowolnienie prac nad programem. Z czasem wyrabia się pewien nawyk, dzięki któremu popełnia się coraz mniej błędów, a nawet jeśli — to są one dla bardziej zaawansowanego programisty łatwiejsze do wykrycia.

Delphi dysponuje na tyle dobrym kompilatorem, że miejsce błędu zostanie wskazane, a sam problem opisany. Przykładowo, brak słowa kluczowego end przy próbie kompilacji spowoduje wskazanie błędu: [Error] My1st_proj.dpr(3): Declaration expected but end of file found.

Należy dobrze zapamiętać pierwszą zasadę: program musi być zawsze zakończony poleceniem end. (kropka na końcu!).

Czytanie kodu

Podczas kompilowania programu Delphi najpierw sprawdza poprawność kodu. Wówczas kompilator odczytuje wszystkie linie kodu źródłowego od początku do końca. Tak więc instrukcje są wykonywane kolejno od góry do dołu.

Wielkość liter

W języku Delphi — w odróżnieniu od C++ czy Javy — wielkość liter nie jest istotna. Dla kompilatora nie jest ważne, czy nazwa funkcji będzie pisana w taki czy inny sposób. Na przykład polecenie ShowMessage będzie znaczyło to samo co showmessage. Można je także zapisywać w inny sposób:

showMessage
showMEssaGe
itd....

Nie jest zalecane pisanie kodu z wykorzystaniem jedynie małych liter — np. showmessage. Wielu początkujących programistów, zafascynowanych nauką Delphi nie pamięta o sposobie pisania kodu — projektanci ci nie stosują wcięć w kodzie, a wszystkie polecenia piszą z użyciem wyłącznie małych liter. A przecież właśnie po sposobie pisania kodu można rozpoznać początkującego programistę — ci bardziej zaawansowani przyjęli takie zasady tworzenia kodu, aby był on bardziej przejrzysty.

Pamiętaj o średniku!

Kolejna istotna reguła brzmi, że każda instrukcja w Delphi musi być zakończona znakiem średnika (;). Jest to informacja dla kompilatora, że w tym miejscu kończy się pojedyncze polecenie. Znak średnika jako symbol zakończenia instrukcji obowiązuje w większości języków programowania (Java, C/C++, Delphi, PHP).

Oczywiście istnieją pewne wyjątki od tej reguły. Na samym początku tego rozdziału zaprezentowałem Czytelnikowi najprostszy program, który zakończony był znakiem kropki, a nie średnika!

Bloki begin i end

Właściwy kod programu zawsze jest wpisywany pomiędzy instrukcje begin i end.

Słowo end może oznaczać zarówno zakończenie jakiegoś bloku instrukcji, jak i zakończenie programu. W pierwszym przypadku na końcu tego słowa stawiamy znak średnika, a w drugim — znak kropki.

Program podczas uruchamiania zawsze odczytuje instrukcje rozpoczynające się od słowa begin — jest to jakby początek programu i właściwych poleceń, które mają zostać wykonane po starcie aplikacji.

Inne instrukcje i deklaracje znajdujące się przed słowem begin są — co prawda — również elementami programu, służą jednak do innych celów. W tej chwili to jest nieistotne — zajmiemy się tym później.

Należy pamiętać, aby liczba instrukcji begin była równa liczbie instrukcji end — w przeciwnym razie kompilator wyświetli błąd: [Error] My1st_proj.dpr(9): 'END' expected but end of file found.

Taki kod jest jak najbardziej prawidłowy:

begin

  begin


    begin

    end;

  end;

end.

Natomiast brak jednego ze słów end spowoduje wyżej wspomniany błąd. Jeżeli z kolei zabraknie jednego ze słów begin, Delphi wyświetli błąd: [Error] My1st_proj.dpr(10): '.' expected but ';' found.

Dyrektywa program

Typowy program powinien składać się z głównej dyrektywy program oraz słów kluczowych begin i end. Co prawda, najprostszy program stanowi jedynie słowo end wraz ze znakiem kropki, ale w prawidłowo zaprojektowanej aplikacji powinien znajdować się także nagłówek program identyfikujący jej nazwę.

Po utworzeniu projektu dyrektywa program zawiera jego nazwę (Delphi generuje ją automatycznie). Przykładowo, jeżeli plik główny projektu nosi nazwę project.dpr, to pierwszy wiersz owego pliku wygląda następująco:

program project;

Nic nie stoi na przeszkodzie, aby tę nazwę zmienić według własnego uznania, np.:

program Mój_pierwszy_program;

Dyrektywa powinna być oczywiście zakończona znakiem średnika.

Nazwa programu nie może zawierać spacji — pamiętaj o tym! Począwszy od Delphi 2005, nazwa programu może zawierać polskie znaki.

Komentarze

Komentarze są chyba najprostszym elementem każdego języka programowania. Jest to blok tekstu, który nie jest uwzględniany przez kompilator podczas budowania pliku wykonywalnego. W komentarzach można zawierać swoje przemyślenia oraz uwagi dotyczące kodu źródłowego:

program project;

begin
 { to jest komentarz }
end.

Typowy komentarz Delphi jest umieszczony pomiędzy znakami { oraz }. W edytorze kodu komentarz jest wyróżniony kursywą i kolorem zielonym (to jednak zależy od ustawień edytora kodu).

Istnieje kilka typów komentarzy, np. jednowierszowy:

// to jest komentarz jednowierszowy

Wiele osób ten rodzaj komentarzy nazywa komentarzami w stylu C, gdyż został zapożyczony z języka C. Jak sama nazwa wskazuje, komentarzem jest tylko jeden wiersz kodu źródłowego:

program project;

begin
 // tu jest komentarz,
 a tu już nie ma komentarza
end.

Drugi wiersz w bloku begin nie jest komentarzem — podczas kompilacji takiego kodu Delphi wskaże błąd.

Istnieje jeszcze jeden typ komentarzy — najrzadziej używany przez programistów:

program project;

begin
  (*

    komentowany kod

  *)
end.

Zaletą tego typu komentarza jest to, że może on zawierać również znaki {.

program project;

begin
  (*

    komentarz...

    { jakiś tekst }

  *)
end.

Jak widać, taki sposób pozwala na umieszczanie komentarzy w komentarzu, ale taka możliwość jest stosunkowo rzadko wykorzystywana.

Komentarze są usuwane przez kompilator w trakcie procesu kompilacji, można więc śmiało komentować kod bez obawy, iż rozmiar pliku wykonywalnego będzie przez to większy. Jako ciekawostkę można posłużyć się tu przykładem kodu źródłowego systemu Windows 2000, który zajmuje 40 GB. Może to świadczyć o dobrze udokumentowanym kodzie. Jednak po kompilacji cały system jest oczywiście mniejszy.

Umiejętne komentowanie

Wydaje się, że pisanie komentarzy jest banalną sprawą. Chciałbym jedynie zwrócić uwagę, że nawet na samym początku nauki programowania umieszczanie własnych komentarzy w kodzie programu jest bardzo ważne. Wydawać się może, że jeżeli nie udostępniamy kodu źródłowego osobom trzecim, to komentowanie kodu staje się zbędne. Nic bardziej mylnego. Po paru miesiącach czy latach programista może mieć problemy ze zrozumieniem działania swojego własnego programu.

Chcę wyrobić w początkującym programiście pewien nawyk komentowania działania programu, gdyż nabiera to szczególnego znaczenia w pracach zespołowych, gdzie kod jest czytany przez wielu programistów — wówczas niezwykłe znaczenie ma zrozumienie kodu napisanego przez inną osobę.

Dobrym zwyczajem jest także umieszczanie na początku każdego pliku komentarza informującego o sposobie działanie danego pliku, ewentualnie jedynie praw autorskich, daty modyfikacji itp. Przykład:

(*******************************************************)
(*               Mój pierwszy program
(*    ---------------------------------------------
(*    Copyright:           (c) by Adam Boduch
(*    Data napisania:      22.11.2003
(*    Wersja:              1.0
(*
(*******************************************************)

Wypisywanie tekstu

Istnieje pewna cecha wspólna wszystkich języków programowania wysokiego poziomu. Każdy z nich posiada jakieś polecenie, które powoduje wypisanie tekstu na ekranie. W języku Delphi (zresztą tak jak w Pascalu) służy do tego polecenie Write lub Writeln. Sposób jego użycia jest następujący:

Write('tekst, który ma zostać wyświetlony');

Sposób użycia jest więc dość prosty: tekst, który ma zostać wyświetlony na konsoli, wpisuje się między dwoma znakami apostrofu i umieszcza w nawiasie.

Osoby, które wcześniej programowały w innym języku programowania, np. w C, mogą mieć problemy z przyzwyczajeniem się do tej konwencji. W C używa się cudzysłowu ("), a w Delphi i Pascalu — apostrofów ('). Jeszcze raz więc przestrzegam przed popełnianiem tego typu błędów.

Jeżeli Czytelnik jeszcze nie zamknął ostatniego projektu, może zmodyfikować jego kod źródłowy do takiej postaci, jaką przedstawia listing 3.2.

Listing 3.2. Wyświetlanie tekstu na ekranie

(*******************************************************)
(*               Mój pierwszy program
(*    ---------------------------------------------
(*    Copyright:           (c) by Adam Boduch
(*    Data napisania:      22.11.2003
(*    Wersja:              1.0
(*
(*******************************************************)


program Mój_pierwszy_program;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  Write('Hello World');  // wypisz tekst na ekranie
  Readln; // czekaj na naciśnięcie klawisza
end.

Po wpisaniu powyższego kodu można uruchomić program, używając skrótu klawiaturowego — F9.

Po uruchomieniu powinno pokazać się okno konsoli, a w nim napis Hello World. Po naciśnięciu klawisza Enter program zakończy działanie.

Druga instrukcja, Readln, powoduje, iż program czeka na naciśnięcie klawisza Enter — dopiero wówczas zakończy swe działanie.

Podsumowując — dotychczas przedstawiłem dwie instrukcje:

*Write — wypisuje tekst na ekranie,
*Readln — czeka na naciśnięcie klawisza Enter.

Właściwie Czytelnik „napisał” swój pierwszy program, wykonując ćwiczenie pokazane już w pierwszym rozdziale, ale nie wiem, czy można było wówczas mówić o pisaniu. Całe zadanie polegało przecież jedynie na umieszczeniu etykiety na formularzu. Tradycją jest, iż pierwszy program adepta danego języka programowania powinien wyświetlać napis Hello World. My również nie odejdziemy od tej tradycji. Jako ciekawostkę podaję odnośnik do strony http://www2.latech.edu/~acm/HelloWorld.shtml, która zawiera kolekcję przykładowych programów wypisujących „Hello World” na ponad 200 sposobów (w różnych językach programowania).

Położenie instrukcji

W rzeczywistości dla Delphi nie ma znaczenia, czy poszczególne instrukcje (polecenia) będą wypisane jedna pod drugą czy też obok siebie. Część programu równie dobrze może wyglądać następująco:

begin Write('Hello World'); Readln; end.

Dla Delphi jedynym wyróżnikiem końca instrukcji jest znak średnika (;). Naturalnym jest jednak, że powyższy sposób wypisywania większych fragmentów kodu w jednej linii jest wyjątkowo nieczytelny i nikt tego nie stosuje — lepiej jest wpisywać instrukcje jedna pod drugą. Czasami warto zastosować nawet linię przerwy.

Instrukcja Writeln

Wcześniej wspomniałem o istnieniu instrukcji Writeln, która praktycznie spełnia taką samą rolę jak instrukcja Write. Jedyna różnica polega na tym, że Writeln wpisuje tekst w nowej linii.

Teraz należy zmodyfikować ostatni program do następującej postaci:

program P3_3;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  Write('Witaj');
  Write('Jak się masz?');
  Readln; // czekaj na naciśnięcie klawisza
end.

Jak widać, powyższy kod ma za zadanie wyświetlenie dwóch zdań. Po uruchomieniu programu można jednak zauważyć, że tekst ma dość niefortunną postać: WitajJak sie masz?.

Instrukcja Writeln załatwi całą sprawę, wyświetlając tekst w nowej linii.

Prawidłowa postać tego programu została przedstawiona na listingu 3.3.

Listing 3.3. Wyświetlanie tekstu z użyciem słowa Writeln

program P3_3;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  Writeln('Witaj');
  Write('Jak się masz?');
  Readln; // czekaj na naciśnięcie klawisza
end.

Zmienne

Czym byłby program, który nie mógłby zapisywać danych do pamięci komputera? Nie ma praktycznie aplikacji, która mogłaby obejść się bez zastosowania zmiennych — są one podstawowym elementem każdego programu.

Zmienne definiujemy jako obszar w pamięci komputera, który służy do przechowywania danych tymczasowych (obecnych w pamięci do czasu wyłączenia programu), mających postać liczb, tekstu itp.

Przypisywanie wartości do zmiennych w językach wysokiego poziomu odbywa się w bardzo prosty sposób — w rzeczywistości jest to dość złożony proces, ale my nie musimy się tym przejmować. Wszystko odbywa się w warstwie systemowej, na poziomie systemu operacyjnego.

Deklaracja zmiennych

Przed przydzieleniem danych do pamięci zmienną należy zadeklarować w kodzie programu. Deklaracja zmiennej powinna być umieszczona przed blokiem begin. Przykładowa deklaracja może wyglądać następująco:

program P3_4;

var
  Zmienna : String;

begin

end.

W razie potrzeby zadeklarowania zmiennej jest konieczne zastosowanie słowa kluczowego var (skrót od słowa variable — zmienna). Stanowi to informację dla kompilatora, że po tym słowie kluczowym zostanie umieszczona deklaracja zmiennych.

Zmienna zawsze musi mieć nazwę! Dzięki tej nazwie możemy łatwo odwołać się do poszczególnych danych zapisanych w pamięci. Pierwszym członem deklaracji zmiennej musi być unikalna nazwa (nie mogą istnieć dwie zmienne o takiej samej nazwie w programie). Po znaku dwukropka należy wpisać typ zmiennej (o typach zmiennych powiem później).

Z punktu widzenia kompilatora nie ma znaczenia, w jaki sposób zapiszemy (zadeklarujemy) zmienną — może więc odbyć się to tak:

var
  zmienna:string;

lub tak:

var zmienna:   string;

Dla zachowania przejrzystości kodu zaleca się jednak stosowanie deklaracji w formie przedstawionej w pierwszym przykładzie.

Trzeba zwrócić uwagę, że w odróżnieniu od języka C w Delphi najpierw jest podawana nazwa zmiennej, a dopiero później typ. Równoważna deklaracja w języku C miałaby postać następującą: char[] zmienna;

Typy danych

Typy zmiennych określają rodzaj danych, które będą zapisywane w pamięci. W poprzednim podrozdziale podczas deklarowania zmiennej skorzystałem z typu String. Ten typ danych służy do przechowywania tekstu i tylko tekstu! Tak więc, chcąc w pamięci komputera umieścić np. liczbę, należy skorzystać z innego typu zmiennej. W przeciwnym przypadku kompilator wyświetli informację o błędzie podczas próby uruchomienia: [Error] Project1.dpr(11): Incompatible types: 'Integer' and 'string'.

Typy zmiennych przedstawiłem w tabeli 3.1. Oczywiście typów tych jest więcej, ale na początku nauki programowania nie trzeba znać ich wszystkich — wystarczą te podstawowe.

Tabela 3.1. Typy zmiennych w Delphi

Typ zmiennej Opis
Integer –2 147 483 648 … 2 147 483 647
Int64 –263 … 263 – 1
SmallInt –32 768 … 32 767
ShortInt –128 … 127
Byte 0 … 255
Word 0 … 65 535
LongWord 0 … 4 294 967 295
Char pojedynczy znak
Boolean TRUE lub FALSE
ShortString do 255 znaków
AnsiString do 231 znaków
Extended 3,6 × 10–4951 … 1,1 × 104932
Double 5,0 × 10–324 … 1,7 × 10308
Single 1,5 × 10–45 … 3,4 × 1038
Currency –922 337 203 685 477,5808 … 922 337 203 685 477,5807

Niektóre z tych typów służą do przechowywania tekstu, natomiast inne do przechowywania liczb. Różni je zakres stosowania. Przykładowo, chcąc zapisać w pamięci jakąś dużą liczbę, nie można skorzystać z typu Byte, ponieważ do tego typu mogą być przypisywane jedynie wartości z zakresu od 0 do 255. Można za to skorzystać z typu Int64.

Oprócz zakresu stosowania powyższe typy danych różni także ilość zajmowanej pamięci operacyjnej. Przykładowo, typ Byte zabiera jedynie 1 bajt pamięci, a typ Int64 — 8 bajtów. Można by pomyśleć, że to nieduża różnica, ale jeśli zmiennych 8-bajtowych w aplikacji jest kilkadziesiąt (kilkaset)? Jest to zwykłe marnowanie pamięci!

Uwaga dla programistów mających wcześniej kontakt z Delphi: typ Real48 nie jest akceptowany przez platformę .NET, tak więc nie jest możliwe jego używanie w aplikacjach.

Podczas czytania tej książki oraz podczas przeglądania różnych kodów źródłowych można zauważyć, że dla typów liczbowych programiści często stosują zmienną Integer. Jest to uniwersalny typ zmiennej liczbowej, gdyż jej zakres jest w miarę duży, a nie wykorzystuje ona aż tak wiele pamięci.

Deklaracja kilku zmiennych

Po wpisaniu słowa kluczowego var można zadeklarować tyle zmiennych, ile będzie potrzebnych — nie trzeba za każdym razem używać dyrektywy var.

program varApp;

var
  Zmienna1 : String;
  Zmienna2 : String;
  Zmienna3 : String;

begin

end.

W powyższym przypadku zadeklarowałem trzy zmienne typu String. Od tej pory dla kompilatora słowa @@Zmienna1@@, @@Zmienna2@@, @@Zmienna3@@ nie są już konstrukcjami nieznanymi — wiadome będzie, że w tym przypadku chodzi o zmienne.

Podczas deklaracji kilku zmiennych tego samego typu można wpisać wszystkie zmienne razem, oddzielając ich nazwy przecinkami:

program varApp;

var
  Zmienna1, Zmienna2, Zmienna3 : String;

begin

end.

Z punktu widzenia kompilatora w tym przypadku również następuje deklaracja trzech zmiennych typu String. Chcąc jeszcze zadeklarować zmienne innego typu, należy to zrobić w następujący sposób:

program varApp;

var
  Zmienna1, Zmienna2, Zmienna3 : String;
  Liczba1, Liczba2 : Integer;

begin

end.

Przydział danych

Po uruchomieniu programu system rezerwuje taką ilość pamięci, jaka będzie potrzebna do działania owej aplikacji. Od tej pory program może przypisywać dane dowolnej zmiennej.

Przydzielanie danych do zmiennej musi odbywać się w bloku begin. Istnieje jednak możliwość przydzielenia danych w trakcie pisania programu.

Przydział statyczny

W celu określenia wartości konkretnej zmiennej, należy to zrobić podczas jej deklarowania, używając w tym celu znaku równości (=).

program varApp;

var
  Zmienna1 : String = 'Oto zmienna nr 1';

begin

end.

Taki kod spowoduje, że na samym starcie programu zmienna @@Zmienna1@@ będzie miała wartość Oto zmienna nr 1.

Każdy tekst zadeklarowany w ramach zmiennej musi być ujęty w znaki apostrofów.

Podczas pisania programu nie można przydzielać wartości kilku zmiennym naraz:

program varApp;

var
  Zmienna1, Zmienna2 : String = 'Oto zmienna nr 1';

begin

end.

Próba uruchomienia takiego programu spowoduje wyświetlenie komunikatu o błędzie: [Error] varApp.dpr(4): Cannot initialize multiple variables.

program varApp;

var
  Zmienna1 : String = 'Oto zmienna nr 1';
  Zmienna2 : String = 'Oto zmienna nr 2';

begin

end.

Natomiast kod przedstawiony powyżej jest już całkiem prawidłowy.

Przydział wartości do zmiennej podczas pisania kodu często jest nazywany przydziałem domyślnym.

W przypadku próby uruchomienia programu, w którym kompilator znajdzie zmienną bez przypisanej żadnej wartości, zostanie wyświetlone ostrzeżenie: [Hint] varApp.dpr(4): Variable 'Zmienna1' is declared but never used in 'varApp'.

Przydział dynamiczny

Możliwa jest także zmiana zawartości danej zmiennej podczas pracy programu. Jest to czynność stosunkowo prosta — polega na zastosowaniu znaku :=, tzw. operatora przydziału. Oto przykład:

program varApp;

var
  Zmienna1 : String;
  Zmienna2 : String;

begin
  Zmienna1 := 'Oto jest zmienna nr 1';
  Zmienna2 := 'Oto jest zmienna nr 2';
end.

Oczywiście ponowna zmiana wartości już raz zadeklarowanej zmiennej jest najzupełniej możliwa.

Czasami zajdzie potrzeba umieszczenia w zmiennej (w ciągu znakowym — typ String) pojedynczego znaku apostrofu. W takim wypadku należy zastosować konwencję podwójnego apostrofu, np.:

Zmienna := 'My Mother''s bag';

Deklaracja zakresu danych

Wiadomo już, że deklarując zmienną typu Byte, można do niej przypisać wartość z przedziału od 0 do 255. Istnieje jednak możliwość samodzielnego określenia zakresu zmiennej:

var
   VarA : 0..20;

Taki zapis sprawia, że do zmiennej @@VarA@@ będzie można przypisać liczbę od 0 do 20. Próba przypisania liczby spoza tego zakresu (np. 21) skończy się komunikatem o błędzie: [Error] Project1.dpr(11): Constant expression violates subrange bounds.

Niekoniecznie należy deklarować zakres zaczynający się od zera — równie dobrze może to wyglądać w ten sposób:

var
   VarA : 10..20;

W takim przypadku jedynymi wartościami, jakie mogą zostać przypisane do zmiennej, są liczby z zakresu od 10 do 20.

Restrykcje w nazewnictwie

Nie jest do końca prawdą, że nazwa zmiennej może być zupełnie dowolna. Niestety, istnieją pewne restrykcje, o których trzeba wiedzieć. Na przykład pierwszym znakiem nazwy zmiennej nie może być cyfra — musi rozpoczynać się od litery.
Nazwa zmiennej może jednak zawierać na początku znak _, ale już inne znaki, takie jak ( ) * & ^ % # @ ! / = + - [ } ] } ' " ; , . czy ?, są zabronione.

W praktyce jednak nie zachodzi potrzeba używania w zmiennej np. znaku ), więc nie ma się czym przejmować. Nowością w Delphi 2005 jest możliwość używania polskich znaków w nazwach zmiennych, czy ogólnie — w całym kodzie źródłowym. Wszystko dzięki temu, iż kompilator oraz edytor kodu obsługują kodowanie znaków w standardzie Unikod. Jest to standard kodowania, mający w zamierzeniu obejmować wszystkie rodzaje pisma używane na świecie. Nie ma więc przeciwwskazań, aby nazwa zmiennej była zapisana np. cyrylicą.

Trzeba też pamiętać, że poprzednie wersje Delphi traktowały polskie znaki w nazwie zmiennej jako błąd. Decydując się na używanie polskich znaków w kodzie źródłowym, trzeba mieć na uwadze fakt, że takie programy nie zostaną skompilowane we wcześniejszych wersjach Delphi.

Stałe

Podobnie jak zmienne, stałe również służą do przechowywania jakichś danych podczas działania aplikacji. Istnieje jednak pomiędzy nimi istotna różnica — stałe, jak sama nazwa wskazuje, nie mogą podlegać modyfikacji podczas działania programu. Czyli wartość stałych jest określana już podczas pisania programu:

program varConst;

const
  Stała1 = 'Oto jest stała...';

begin

end.

Stałe, w odróżnieniu od zmiennych, deklarujemy z użyciem słowa kluczowego const (od angielskiego słowa constanst — stała). Jak widać, nie deklarujemy typu stałej — jest on określany automatycznie na podstawie zapisanej wartości.

Domyślne typy stałych

Jeżeli, przykładowo, przypiszemy stałej jakąś liczbę:

const
  Stała2 = 12;

Delphi określi, że stała jest typu Integer (jest to domyślny typ stałych - liczby całkowitej). Programista może to w dość prosty sposób zmienić:

program varConst;

const
  Stała1 = 'Oto jest stała...';
  Stała2 : Byte = 12;

begin

end.

A zatem w tym przypadku Stała2 będzie stałą typu Byte o wartości 12.

Możliwe jest także użycie tzw. rzutowania (o tym będziemy mówić nieco później), co w praktyce wiąże się z taką deklaracją:

const
  Int = Word(342);

W tym przypadku stała Int będzie typu Word o wartości 342. W praktyce jednak takie rzutowanie daje te same rezultaty co sposób określenia typu przedstawiony wcześniej.

W razie próby przypisania jakiejś wartości stałej, przykładowo:

begin
  Stała1 := 'Inna wartość';
end.

Delphi uzna to za błąd i wyświetli podpowiedź:

[Error] varConst.dpr(8): Left side cannot be assigned to.

Przed chwilą wspomniałem, iż wartości stałych nie podlegają modyfikacji. Istnieje jednak mała „sztuczka”, która pozwala na zmianę wartości, a polega na ustawieniu odpowiedniej dyrektywy kompilatora.

Dyrektywy kompilatora wyglądają jak zwykłe komentarze. Są one poprzedzane znakiem dolara ($) i umożliwiają zmianę opcji kompilacji. Przykładowo, dyrektywa {$APPTYPE CONSOLE} określa, że aplikacja będzie uruchamiana w oknie konsoli. Kompilator napotykając na taką dyrektywę modyfikuje sposób kompilacji kodu źródłowego.
</dfn>

Umieszczenie w kodzie dyrektywy {$J+} umożliwia modyfikację wartości stałych:

{$J+}
const
  Stała1 : String = 'Początkowa wartość';

begin
  Stała1 := 'Końcowa wartość';
end.

Nie jest to jednak często spotykany sposób pracy, można więc potraktować to jako ciekawostkę.

Używanie zmiennych w programie

Skoro Czytelnik zna już pojęcie zmiennych, może nauczyć się ich praktycznego zastosowania — np. pobierania danych wprowadzanych przez użytkownika w programie. Przykładowo, niech nasz program pyta użytkownika o imię.

Pobieranie danych wprowadzanych podczas działania programu jest realizowane również przez polecenie Readln. Jego składnia jest następująca:

Readln(nazwa_zmiennej);

W nawiasie należy podać nazwę zmiennej, do której zostanie przypisana wartość podana przez użytkownika.

W dalszej części książki na dane, które należy wpisać w nawiasie, będziemy mówili parametr.

Łatwo zauważyć, że instrukcja Readln jest dość elastyczna, gdyż program zadziała zarówno wtedy, gdy zostanie podany w nawiasie jakiś parametr, ale równie dobrze może obyć się bez tego. Poniższy przykład prezentuje pobranie danych od użytkownika i przypisanie ich do zmiennej @@Imię@@:

program P3_4;

{$APPTYPE CONSOLE}

uses
  SysUtils;


var
  Imię : String;

begin
  Write('Podaj imię:');
  Readln(Imię);
end.

Najpierw program wyświetla napis Podaj imię, po czym czeka na reakcję użytkownika (w tym wypadku wymaga od niego wpisania imienia i naciśnięcia klawisza Enter).

Łączenie danych

Skoro pobraliśmy dane do pamięci komputera, możemy je równie dobrze wyświetlić — oczywiście za pomocą funkcji Writeln. Język Delphi umożliwia wygodne łączenie tekstu przy korzystaniu ze znaku +. Możemy więc wyświetlić jakiś tekst w następujący sposób:

Writeln('Delphi ' + ' 2005');

Miejmy na uwadze to, że znak + musi znaleźć się poza apostrofami.

Równie dobrze zamiast frazy 2005 można by było w tym miejscu wstawić nazwę zmiennej. Listing 3.4 prezentuje program, który pyta użytkownika o imię, po czym wyświetla je.

Listing 3.4. Jak masz na imię?

program P3_4;

{$APPTYPE CONSOLE}

uses
  SysUtils;


var
  Imię : String;

begin
  Write('Podaj imię:');
  Readln(Imię);
  Writeln('Witaj ' + Imię);

  Readln; // czekaj na naciśniecie klawisza
end.

Działanie takiego programu zostało przedstawione na rysunku 3.1.

3.1.jpg
Rysunek 3.1. Działanie programu

Należy pamiętać o tym, aby nazwy zmiennej nie otaczać apostrofami!

Działa to w następujący sposób: program, odnajdując w kodzie nazwę zmiennej (w tym przypadku Imię), odczytuje jej wartość z pamięci i wyświetla ją w danym miejscu. Bardziej szczegółowo opiszemy to podczas omawiania wskaźników.

Tablice danych

Można sobie wyobrazić sytuację, gdy w programie trzeba użyć wielu, naprawdę wielu zmiennych. Czy wygodne jest w takim przypadku deklarowanie dużej liczby zmiennych, z inną nazwą dla każdej? Rozwiązaniem tego problemu są tablice. Tablice są deklarowane jako zmienne za pomocą słowa kluczowego array.

program arrayApp;

uses
  Dialogs;

var
  Tablica : array[0..1] of String;

begin

end.

Konstrukcja tablic jest dość specyficzna. Po słowie kluczowym array w nawiasach kwadratowych należy wpisać liczbę elementów, z których będzie się składać tablica, a konkretniej — numery indeksów:

Nazwa_Tablicy : array[Numer_indeksu..Numer_indeksu] of Typ_danych;

W powyższym przypadku tablica składa się z dwóch elementów o indeksach 0 i 1. Należy tu rozróżnić pojęcia element oraz indeks. Popatrzmy na poniższy przykład:

Tablica : array[100..200] of String;

Nic nie stoi na przeszkodzie, aby zadeklarować tablicę 100-elementową o indeksach z zakresu od 100 do 200. W takim przypadku najmniejszym indeksem jest 100, a największym — 200.

Przydział wartości do zmiennych umieszczonych w tablicy odbywa się także z zastosowaniem nawiasów kwadratowych:

program arrayApp;

var
  Tablica : array[0..1] of String;

begin
  Tablica[0] := 'Pierwszy element tablicy';
  Tablica[1] := 'Drugi element tablicy';
end.

Podsumujmy: z tablic korzysta się tak samo jak ze zmiennych. Jedyną różnicą jest to, że należy zawsze podawać numer elementu (indeks), do którego chce się zapisać lub odczytać dane.

Tablice jako stałe

Wcześniej powiedzieliśmy, że tablice należy deklarować jako zmienne. Owszem, jednak jest możliwe deklarowanie tablic jako stałych. Można więc przyjąć, że tablice to „specjalny” typ danych, który może być deklarowany zarówno jako zmienna, jak i stała.

Tak jak w przypadku „zwykłych” stałych, dane także należy przypisać do tablicy podczas projektowania aplikacji:

program arrayConst;

const
  Tablica : array[0..1] of String = (
  ('Pierwszy element'), ('Drugi element')
  );

begin

end.

Także w tym przykładzie tablica składa się z dwóch elementów. Dodatkowe nawiasy zostały wprowadzone jedynie po to, aby zwiększyć czytelność kodu — równie dobrze można by zapisać program w ten sposób:

program arrayConst;

const
  Tablica : array[0..1] of String = (
  'Pierwszy element', 'Drugi element');

begin

end.

Obowiązkowy jest jedynie jeden nawias, w którym wypisujemy elementy tablicy, oddzielając je przecinkami.

Należy uważać na przydział danych — zgodnie z liczbą elementów, jakie zostały zadeklarowane w kodzie. Poniższy przykład:

program arrayConst;

const
  Tablica : array[0..2] of String = (
  'Pierwszy element', 'Drugi element');

begin

end.

nie będzie mógł zostać skompilowany — zadeklarowano trzy elementy, a dane przydzielono jedynie do dwóch. Delphi wyświetli komunikat o błędzie:

[Error] arrayConst.dpr(5): Number of elements differs from declaration.</dfn>

Tablice wielowymiarowe

Delphi umożliwia także deklarowanie tablic tzw. wielowymiarowych. Po zadeklarowaniu takich tablic, do konkretnego elementu możemy odwołać się w następujący sposób:

Tablica[0][0] := 'Przypisanie danych';

W powyższym przypadku skorzystałem jedynie z tablic dwuwymiarowych, których deklaracja wygląda tak:

var
  Tablica : array[0..1, 0..1] of String;

Deklaracja jest także specyficzna — polega bowiem na wypisywaniu indeksów w nawiasie kwadratowym, przy czym poszczególne elementy są oddzielone przecinkami. W przedstawionej wyżej deklaracji mamy aż 4 elementy! Przydział danych odbywa się w następujący sposób:

program arrayarray;

var
  Tablica : array[0..1, 0..1] of String;

begin
  Tablica[0][0] := 'Element 1';
  Tablica[0][1] := 'Element 2';
  Tablica[1][0] := 'Element 3';
  Tablica[1][1] := 'Element 4';
end.

Istotę działania tablic dwuwymiarowych można zrozumieć lepiej, przeglądając listing 3.5.

Listing 3.5. Program deklarujący dwuwymiarowe tablice

program arrayarray;

var
  Tablica : array[0..1, 0..2] of String;

begin
  Tablica[0][0] := 'Fiat';
  { marka samochodu }

    Tablica[0][1] := 'Uno';
    Tablica[0][2] := 'Punto';
    { modele samochodów }

  Tablica[1][0] := 'Audi';

    Tablica[1][1] := 'A4';
    Tablica[1][2] := 'A8';

end.

W tym przypadku nastąpiła deklaracja tablicy 2x3. Dwa główne elementy to element Fiat oraz element Audi. Kolejne dwa „podpola” określają modele samochodów.

Powyższy program poza przydzieleniem danych do tablicy nie wykonuje niczego konkretnego — prezentuje jedynie, jak należy przypisywać dane do tablicy wielowymiarowej.

Opisując tablice wielowymiarowe, mówiłem tylko o dwóch wymiarach. Istnieje jednak możliwość zadeklarowania tablic, które będą miały wiele wymiarów.

program arrayx3;

var
  Tablica : array[0..1, 0..1, 0..1] of String;

begin
  Tablica[0][0][0] := 'Wartość';
  { itd. }
end.

W tym przypadku nasza tablica to tablica 3x2 typu String. W jaki sposób dane są przydzielane do tej tablicy? Odpowiedni przykład znajduje się w powyższym kodzie źródłowym.

Tablice dynamiczne

Nieraz podczas pracy z Delphi będzie wymagane zadeklarowanie w programie tablicy o niewiadomej liczbie elementów. Znaczy to, że programista w trakcie pisania programu nie jest w stanie określić, ilu elementów tablicy będzie potrzebował. W tym celu w Delphi 4 zaimplementowano możliwość tworzenia tablic dynamicznych. Tablice dynamiczne deklaruje się bez podania liczby elementów:

program dynArray;

var
  Tablica : array of String;

begin

end.

Przy tej okazji opiszę nowe polecenie — SetLength. Służy ono do określania liczby elementów tablicy podczas działania programu. Pierwszym parametrem tego polecenia jest nazwa tablicy dynamicznej, natomiast drugim — liczba elementów, z których tablica ma się składać. Parametry przekazywane do polecenia muszą być oddzielane przecinkami:

program dynArray;

var
  Tablica : array of String;

begin
  SetLength(Tablica, 2);
end.

Od tej pory po uruchomieniu programu tablica będzie się składała z dwóch elementów. Wypełnianie elementów danymi odbywa się tak samo jak w przypadku zwykłych tablic:

program dynArray;

var
  Tablica : array of String;

begin
  SetLength(Tablica, 2);
  Tablica[0] := 'Wartość 1';
  Tablica[1] := 'Wartość 2';
  Tablica[2] := 'Wartość 3';
end.

W chwili tworzenia programu nie jest możliwe określenie przez kompilator, z ilu elementów ostatecznie będzie składać się tablica. Stąd próba odczytu elementu spoza tablicy w trakcie działania programu może skończyć się komunikatem o błędzie, a nawet zawieszeniem działania aplikacji.

Polecenia Low i High

Oba polecenia — Low i High — użyte w połączeniu z tablicami zwracają najmniejszy (Low) indeks tablicy oraz indeks największy (High). Warto je znać, gdyż czasem mogą się przydać. Należy jednak zauważyć, że owe funkcje nie zwracają wartości najmniejszego oraz największego elementu w tablicy. Najlepiej wytłumaczyć to na przykładzie.

Deklaracja tablicy może, na przykład, wyglądać następująco:

Tablica : array[10..100] of Integer;

Wywołanie polecenia Low(Tablica) spowoduje, że funkcja zwróci wartość 10, natomiast funkcja High zwróci wartość 100.

Odczytywanie najmniejszej i największej wartości

Skoro Low i High wyznaczają najmniejszy oraz największy indeks z tablicy, to pytanie brzmi: jak odczytać wartość najmniejszego i największego elementu?

Rozwiązanie tego problemu nie jest trudne, ale również wymaga zastosowania funkcji Low oraz High. Otóż najmniejszym elementem tablicy może być element o indeksie 0, ale równie dobrze może to być indeks 20. Trzeba więc pobrać numer elementu, a dopiero później go odczytać:

Tablica[Low(Tablica)]; // zwraca wartość najmniejszego elementu

Przykładowy program zaprezentowany jest na listingu 3.6.

Listing 3.6. Odczytywanie wartości najmniejszego oraz największego elementu tablicy

program P3_6;

{$APPTYPE CONSOLE}

var
  Tablica : array[10..100] of String;

begin
  Tablica[10] := 'Wartość 10';
  Tablica[100] := 'Wartość 100';

  Writeln('Wartość najmniejszego elementu tablicy: ' + Tablica[Low(Tablica)]);
  Writeln('Wartość największego elementu tablicy: ' + Tablica[High(Tablica)]);
  Readln;

end.

Operatory

Operatory są specjalnymi znakami, elementami języka programowania, służącymi do manipulowania danymi i sterowania pracą programu. Operatory są używane niezwykle często — już wcześniej użyliśmy dwóch: operatora przypisania (:=) oraz operatora porównania (=). Poniżej przedstawiam rodzaje operatorów wraz z ich odpowiednikami w języku C.

Operatory przypisania

Przypisanie danych jest jedną z najważniejszych i zarazem najczęściej wykonywanych czynności podczas programowania w Delphi. Jak sama nazwa wskazuje, operator przypisania powoduje przypisanie nowych danych do zmiennej:

Zmienna := 20;

Budowa jest bardzo prosta. Po lewej stronie należy wpisać nazwę zmiennej, do której zostanie przydzielona dana — po prawej musi znaleźć się przypisywana wartość.

W języku C odpowiednikiem operatora przypisania z Delphi jest znak równości (=), tak więc przypisanie jest w C jeszcze prostsze:

zmienna = 10;

Zastosowanie większości operatorów wymaga znajomości instrukcji warunkowych, zatem najpierw przedstawię dostępne w Delphi operatory, a później pokażę przykłady ich użycia.

Operatory porównania

Czynność porównywania jest stosowana w codziennym życiu człowieka. Jesteśmy w stanie na przykład określić, który człowiek z dwojga jest wyższy, a na podstawie liczby koni mechanicznych silników jesteśmy w stanie ocenić, który z nich ma większą moc.

Podobnie w matematyce obowiązują takie znaki porównania jak > (znak większości) i < (znak mniejszości). Identyczne symbole są wykorzystywane w językach programowania (patrz tabela 3.1).

Tabela 3.1. Operatory porównania

Operator Język Delphi Język C
Nierówności <> !=
Równości = ==
Większości > >
Mniejszości < <
Większe lub równe >= >=
Mniejsze lub równe <= <=

W Delphi możemy porównywać wartości typów — np. wartości zmiennych typu Integer. Przykłady porównywania wartości pokażę podczas omawiania tzw. instrukcji warunkowych w dalszej części tego rozdziału.

Operatory logiczne

Operatory logiczne często są nazywane operatorami boolowskimi (ang. Boolean operators). Wynika to z tego, że realizują one operacje właściwe dla algebry Boole’a.

Faktycznym zastosowaniem tych operatorów jest testowanie kilku warunków. Weźmy jakiś przykład z życia codziennego: „Jeżeli będę miał 18 lat i 20 tysięcy zł, kupie sobie samochód”. W tym zdaniu operatorem jest i. Do spełnienia kryterium (kupna samochodu) jest niezbędne zatem spełnienie łącznie dwóch warunków (posiadania 20 tysięcy zł oraz skończenia 18 lat). Jeżeli któryś z tych warunków nie będzie prawdziwy — kryterium, czyli kupno samochodu, nie zostanie spełnione.
Podobny przykład można przenieść na platformę programową. Na przykład jeżeli zmienna @@X@@ posiada wartość 20, a zmienna @@Y@@ wartość 10, to zrób to i tamto. Takie przykłady będziemy realizować nieco dalej — tymczasem przyjrzyjmy się dostępnym w Delphi operatorom logicznym (tabela 3.2).

Tabela 3.2. Operatory logiczne

Operator Język Delphi Język C
Logiczne i and &&
Logiczne lub or `
Zaprzeczenie not !

Typ Boolean

Mówiąc o operatorach boolowskich, warto wspomnieć o ważnym, często w Delphi używanym typie Boolean. Jest to specyficzny typ danych, który może przyjmować jedynie wartości True lub False. Deklarowanie takiego typu sprowadza się do deklaracji zwykłej zmiennej:

var
  B : Boolean;

Przydzielanie danych do takiej zmiennej odbywa się w standardowy sposób z wykorzystaniem operatora := :

B := False;

Być może Czytelnik zastanawia się, do czego jest używana taka zmienna? Otóż, przykładowo, zmienna taka może mówić o tym, czy dana operacja została zakończona (True) czy też trwa dalej (False). Praktyczne zastosowanie tego typu zmiennej przedstawię w dalszej części książki — z pewnością każdy jeszcze nieraz z niego skorzysta.

Operatory arytmetyczne

Nauka, jaką jest matematyka, dawno temu wykształciła pewne symbole umowne, opisujące pewne działania, jak np. dodawanie czy odejmowanie. Oczywiście komputer jako maszyna umożliwia wykonywanie tych czynności w bardzo prosty sposób przy wykorzystaniu symboli identycznych jak w matematyce (tabela 3.3).

Tabela 3.3. Operatory arytmetyczne

Operator Język Delphi Język C
Dodawanie + +
Odejmowanie - -
Mnożenie * *
Dzielenie rzeczywiste / /
Dzielenie całkowite div /
Reszta z dzielenia mod %

Można tu zauważyć pewną różnicę w porównaniu z tym, czego uczyliśmy się w szkole podstawowej. W szkole bowiem jako znaku mnożenia używaliśmy kropki, natomiast komputerowym symbolem mnożenia jest gwiazdka (*). To samo tyczy się dzielenia (w szkole znak :) — w Delphi symbol /.

Ciekawą operacją dostępną w Delphi jest dzielenie całkowite. Otóż stosując operator div w miejsce standardowego /, podzielimy liczby, lecz reszta z ewentualnego dzielenia zostanie pominięta.

To samo tyczy się operatora mod — jeżeli chcemy uzyskać jedynie resztę z dzielenia, możemy użyć mod w miejsce /.

W Delphi oczywiście można dodawać, dzielić i wykonywać wszystkie pozostałe działania arytmetyczne, korzystając ze zmiennych. Przykładowo: pobieramy dane od użytkownika i dodajemy dwie wartości. Dodawanie wartości zmiennych wygląda identycznie jak w przypadku zwyczajnych liczb:

X + Y; // dodanie wartości zmiennej X do zmiennej Y

W prosty sposób można także przypisać wynik jakiejkolwiek operacji:

Z := X * Y; // do zmiennej Z przypisz wynik mnożenie X i Y

Funkcje zwiększania i zmniejszania

Programowanie w języku C charakteryzuje się mniej czytelną składnią, ale jednocześnie dużą elastycznością oraz szybkością tworzenia kodu. W Turbo Pascalu w celu zwiększenia wartości zmiennej o 1 należało wykonać:

X := X + 1; // zwiększenie zmiennej o 1

W C/C++ istnieją natomiast operatory upraszczające cały proces:

++i; // zwiększ liczbę i o 1
i--; // zmniejsz liczbę i o jeden

W Delphi z kolei wprowadzono funkcje zwiększania (ang. increment) oraz zmniejszania (ang. decrement) wartości:

Inc(X); // zwiększ wartość X o 1
Dec(X); // zmniejsz wartość X o 1

Standardowe wywołanie tych funkcji zwiększa lub zmniejsza daną wartość o 1. Możliwe jest jednak zastosowanie opcjonalnego drugiego parametru, który określa, o ile funkcja ma zwiększyć wartość, np.:

Inc(X, 2); // zwiększ wartość X o 2
Dec(X, 4); // zmniejsz wartość X o 4

Listing 3.7 prezentuje prostą aplikację działającą jako prościutki kalkulator — umożliwia ona dodawanie dwóch liczb na podstawie wartości podawanych przez użytkownika.

Listing 3.7. Dodawanie dwóch liczb

program P3_8;

{$APPTYPE CONSOLE}


var
  A, B, Z : Integer;
begin
  Writeln('Program dodaje do siebie dwie liczby');
  Writeln('Podaj liczbę A: ');
  Readln(A); // pobierz pierwszą liczbę

  Writeln('Podaj liczbę B: ');
  Readln(B);      // pobierz drugą liczbę

  Z := A + B;
  Writeln(Z);
  Readln; // czekaj na Enter...

end.

Operatory bitowe

Operatory bitowe oferują nieco bardziej zaawansowane działania na liczbach binarnych. Nie będziemy się tym zajmować — ważne, aby wiedzieć, że takie manipulacje są w Delphi możliwe, lecz wymagana jest do tego większa wiedza na temat działania komputera oraz różnych systemów liczbowych (dziesiętny, dwójkowy itp.).

Operatory bitowe prezentuje tabela 3.4.

Tabela 3.4. Operatory bitowe

Operator Język Delphi Język C
Koniunkcja and &
Zaprzeczenie not -
Alternatywa or `
Dysjunkcja xor ^
Przesunięcie w lewo shl <<
Przesunięcie w prawo shr >>

Pozostałe operatory

W rzeczywistości teraz omówiłem tylko część operatorów Delphi, tą najczęściej używaną. Istnieje jeszcze wiele innych operatorów, które są wykorzystywane rzadziej — będę opisywał sposoby ich wykorzystywania w dalszej części tej książki, podczas prezentacji pozostałych tematów.

Instrukcje warunkowe

Przed chwilą była mowa o operatorach w języku Delphi, jednak głupie byłoby opisywanie operatorów bez wzmianki o instrukcjach warunkowych. Są to konstrukcje, które służą do sprawdzania, czy dany warunek został spełniony. Jest to praktycznie podstawowy element języka programowania — dzięki instrukcjom warunkowym możemy odpowiednio zareagować na istniejące sytuacje i sterować pracą programu. Przykładowo: trzeba, aby użytkownik wpisał swoje imię na samym początku działania programu. Może się jednak zdarzyć, że użytkownik specjalnie lub omyłkowo wpisze liczbę. Jeżeli programista nie uwzględni tej możliwości i nie wprowadzi odpowiednich zabezpieczeń, może się to skończyć źle dla programu lub (w przypadku większych aplikacji) — spowodować błędy związane z bezpieczeństwem systemu!

Ostatnimi czasy wykryto poważne błędy w przeglądarce Internet Explorer, związane z bezpieczeństwem, których powodem był… pasek adresów. Zagrożenie powstawało w chwili wpisania w pasku adresów odpowiedniego ciągu znaków. Jest oczywiste, jak ważne jest sprawdzanie danych dostarczanych przez użytkownika. Podstawowa zasada brzmi: nie wolno ufać danym podawanym aplikacji przez użytkownika — zawsze należy je sprawdzać przed dalszym działaniem programu.

Instrukcja if..then

Podstawową instrukcją warunkową jest instrukcja if..then. Jej budowa jest następująca:

if { tu następuje sprawdzanie warunku } then { wykonaj pewną operację }

Po słowie if musi się znaleźć pewien warunek do sprawdzenia, przykładowo:

if 4 > 3 then

Taki warunek będzie spełniony zawsze, gdyż wiadomo, że liczba 4 jest większa od 3. Tak więc za każdym razem zostanie wykonany kod umieszczony po słowie then.

program if_then;

{$APPTYPE CONSOLE}

begin
  if 4 > 3 then
    Writeln('Tak... warunek został spełniony!');

  Readln;
end.

Po uruchomieniu powyższego programu za każdym razem wyświetlony zostanie tekst Tak... Warunek został spełniony!.

Pobieranie tekstu z konsoli

Wykonajmy małe ćwiczenie. Otóż po uruchomieniu programu użytkownik zostanie poproszony o wpisanie pewnego tekstu — imienia. Jeśli zostanie wpisane imię Adam, nastąpi wykonane pewnych czynności. W przeciwnym razie zostanie wyświetlony tekst powitalny. Kod programu realizującego to zadanie wygląda następująco:

program if_then;

{$APPTYPE CONSOLE}

var
  Imię : String;
  
begin
  Writeln('Wpisz swoje imię...');
  Readln(Imię);  // pobranie wpisanej wartości

  if Imię = 'Adam' then
    Writeln('Super! Ja też mam na imię Adam!');

  if Imię <> 'Adam' then
    Writeln('Cześć ' + Imię);

  Readln;
end.

Na samym początku programu są pobierane dane wpisane przez użytkownika programu. Realizuje to procedura Readln. Za jej pomocą dane wpisane przez użytkownika są przypisywane do zmiennej Imię. Celem naszego ćwiczenia jest sprawdzenie, czy wpisanym tekstem jest Adam.

Kilka instrukcji po słowie then

Istnieje pewna zasada, którą należy zapamiętać i która będzie stosowana w dalszej części tego rozdziału. Otóż wiele instrukcji następujących po słowie then musi dodatkowo znaleźć się w bloku tekstu między begin a end (przykład znajduje się na listingu 3.8).

Listing 3.8. Pobieranie danych z konsoli i porównywanie ich za pomocą operatora if

program if_then;

{$APPTYPE CONSOLE}

var
  Imię : String;
  
begin
  Writeln('Wpisz swoje imię...');
  Readln(Imię);  // pobranie wpisanej wartości

  if Imię = 'Adam' then
  begin // dodatkowa instrukcja begin
    Writeln('Super! Ja też mam na imię Adam!');
    Writeln('Hahhahaa...');
  end;

  Readln;
end.

W całym powyższym listingu interesuje nas jedynie ten fragment kodu:

if Imię = 'Adam' then
  begin // dodatkowa instrukcja begin
    Writeln('Super! Ja też mam na imię Adam!');
    Writeln('Hahhahaa....');
  end;

Oznacza on, że gdy wartość zmiennej jest równa Adam, następuje wykonanie dwóch instrukcji Writeln. Taka sytuacja zmusza nas do umieszczenia dodatkowo słów kluczowych begin oraz end. Jeżeli blok begin..end nie znalazłby się w kodzie, to druga instrukcja Writeln byłaby wykonana niezależnie od tego, czy zmienna @@Imię@@ posiadałaby wartość Adam, czy też nie.

Kilka warunków do spełnienia

To, czy dany kod zostanie wykonany, może zależeć od wielu czynników. Niekoniecznie musi to być jeden warunek do spełnienia — może być ich wiele. Jednak w takich przypadkach konieczne jest umieszczenie wszystkich tych warunków w nawiasie (listing 3.9).

Listing 3.9. Kilka warunków w instrukcji if

program if_then;

{$APPTYPE CONSOLE}

var
  Imię : String;
  
begin
  Writeln('Wpisz swoje imię...');
  Readln(Imię);  // pobranie wpisanej wartości

  Randomize; // procedura losująca

  if (Imię = 'Adam') and (Random(10) = 5) then
    Writeln('Super! Ja też mam na imię Adam!');

  Readln;
end.

Kod ten można przetłumaczyć następująco: „jeżeli zmienna @@Imię@@ zawiera wartość Adam i rezultat losowania jednej liczby z przedziału od 0 do 9 wynosi 5, wyświetl tekst na konsoli”. Jak widzisz, aby warunek został spełniony, muszą zostać wykonane dwie czynności.

Wielu początkujących programistów często zapomina o zastosowaniu nawiasów podczas sprawdzania kilku warunków. W tym przypadku spowoduje to wyświetlenie komunikatu o błędzie: [Error] if..then.dpr(14): Incompatible types: 'String' and 'Integer'.

Przedstawiłem przy tej okazji znaczenie kolejnej procedury, a mianowicie Random. Polecenie to realizuje proces losowania spośród liczb całkowitych, z których najwyższa jest podana w nawiasie. A zatem wywołanie procedury w ten sposób — Random(100) — spowoduje wylosowanie liczby z zakresu od 0 do 99. Tak! Nie pomyliłem się! Możliwe jest przecież, że w wyniku losowania zostanie zwrócona wartość 0.

Pamiętaj, aby zawsze przed wywołaniem procedury Random umieszczać w kodzie instrukcję Randomize. Powoduje ona zainicjowanie procesu losowania.

Instrukcja case..of

Drugą instrukcją warunkową jest case..of. Również ta konstrukcja realizuje proces porównywania danych. Często jest stosowana w przypadku, gdy mamy do sprawdzenia wiele warunków — instrukcja if..then nie zdaje wówczas egzaminu, gdyż należałoby porównać wiele wartości ze sobą. Idealnie za to nadaje się do tego instrukcja case..of. Jej składnia jest następująca:

case Nazwa_Zmiennej of
  0: { instrukcje wykonywane w przypadku, gdy zmienna ma wartość 0 }
  1: { instrukcje wykonywane w przypadku, gdy zmienna ma wartość 1 }
end;

Jak widać, powyższy schemat jest raczej prosty. Nie należy zapomnieć o słowie kluczowym end; kończącym instrukcję warunkową. Przykładowy program zaprezentowałem na listingu 3.10.

Listing 3.10. Program wykorzystujący instrukcję case..of

program case_of;

{$APPTYPE CONSOLE}

var
  Mandat : Integer;

begin
  Mandat := 50; 

  case Mandat of
    10: Writeln('E, 10 zł. Może jeszcze zapłacę?');
    20: Writeln('20 zł – nie będę miał na chleb!');
    50: Writeln('50 zł – Panie władzo... może dogadamy się w inny sposób?');
  end;

  Readln;
end.

Po uruchomieniu programu zostaną wykonane instrukcje podane przy ostatnim warunku, gdyż zmiennej @@Mandat@@ nadałem „sztywną” wartość 50. Chciałem jednak tylko zaprezentować sposób użycia instrukcji case..of.

Zakresy

Dużą zaletą instrukcji case..of jest możliwość sprawdzania wartości „od – do”. Przykładowo, jeżeli wartość zmiennej @@Mandat@@ zawiera się między 10 a 15, zostanie wykonana konkretna czynność. Mówiliśmy o tym przy okazji deklarowania zmiennych — tam także można było zadeklarować zmienną posiadającą pewien zakres (to tak w ramach przypomnienia).

Zmodyfikujemy poprzedni program do takiej postaci, aby wartość zmiennej była losowana:

program case_of;

{$APPTYPE CONSOLE}

var
  Mandat : Integer;

begin
  Randomize;
  Mandat := Random(50)+1; // dodajemy 1, aby nie zostało wylosowane 0

  case Mandat of
    1..10: Writeln('E, 10 zł. Może jeszcze zapłacę?’);
    11..20: Writeln('20 zł – nie będę miał na chleb!');
    21..50: Writeln('50 zł – Panie władzo... może dogadamy się w inny sposób?');
  end;

  Readln;
end.

Dzięki zastosowaniu zakresów pierwsza instrukcja zostanie zrealizowana w przypadku, gdy zmienna @@Mandat@@ będzie miała wartość od 1 do 10.

Brak możliwości korzystania z ciągów znakowych

Wadą instrukcji case..of jest brak możliwości porównywania danych tekstowych. Najlepiej sprawdzić to na przykładzie — spójrzmy na poniższy kod:

program case_strings;

{$APPTYPE CONSOLE}

var
  S : String;

begin
  S := 'Hę';

  case S of
    'Hę': Writeln('???');
  end;

  Readln;
end.

Próbujemy tu sprawdzić, czy zmienna @@S@@ ma wartość . Niestety, próba kompilacji takiego programu zakończy się komunikatem o błędzie: [Error] case_strings.dpr(11): Ordinal type required.

Język C#, który powstał praktycznie jako język w pełni zgodny ze standardami .NET, ma możliwość korzystania z ciągów znakowych w instrukcji case (w C# odpowiednikiem instrukcji case jest switch), dziwi więc fakt, że firma Borland nie wprowadziła tej funkcji w nowym Delphi. Więcej informacji na temat programowania w języku C# znajduje się w dodatku A.

Kilka instrukcji

Tak samo jak w przypadku instrukcji warunkowej if..then instrukcja case..of wymaga umieszczenia kodu w bloku begin..end w sytuacji, gdy kod ten zawiera więcej niż jedną instrukcję.

program case_of;

{$APPTYPE CONSOLE}

var
  Mandat : Integer;

begin
  Randomize;
  Mandat := Random(50)+1; // dodajemy 1, aby nie zostało wylosowane 0

  case Mandat of
    1..10: Writeln('E, 10 zł. może jeszcze zapłacę');
    11..20: Writeln('20 zł. – nie będę miał na chleb');
    21..50:
    begin
      Writeln('50 zł. – Panie władzo... może dogadamy się w inny sposób?');
      Writeln('Nieeeee...');
    end;
  end;

  Readln;
end.

Sami więc widzimy — jeżeli zostanie wylosowana wartość pomiędzy 21 a 50, program wykona dwie instrukcje. W takim przypadku należy umieścić kod w dodatkowym bloku begin..end.

Instrukcja else

Angielskie słowo else można w tym kontekście przetłumaczyć jako w przeciwnym wypadku. Konstrukcja else jest zawsze stosowana w połączeniu z instrukcją if..then oraz case..of.

Jeżeli, przykładowo, dany warunek if nie został spełniony, można odpowiednio na to zareagować, dodając blok else. Oczywiście wszystko najlepiej wyjaśnić na przykładach — napiszmy zatem przykładowy program. Czytelnik pamięta pewnie pierwszy program, jaki zaprezentowałem podczas omawiania instrukcji if. Zmodyfikujmy go do takiej postaci:

program if_then_else;

{$APPTYPE CONSOLE}

var
  Imię : String;
  
begin
  Writeln('Wpisz swoje imię...');
  Readln(Imię);  // pobranie wpisanej wartości

  if Imię = 'Adam' then
    Writeln('Super! Ja też mam na imię Adam!') // <–– uwaga! Brak średnika!
  else Writeln('Cześć ' + Imię);

  Readln;
end.

W poprzedniej wersji tego programu to, czy zmienna @@Imię@@ nie zawiera wartości Adam, było sprawdzane w kolejnym warunku if. Teraz w przypadku, gdy zmienna nie będzie zawierać tej wartości, zostanie wyświetlony tekst powitalny. Wszystko możliwe jest za sprawą instrukcji else. Mam nadzieję, że ten przykład pomógł Czytelnikowi w zrozumieniu sposobu działania instrukcji else.

Zauważmy, że w wierszu nad instrukcją else nie ma średnika na końcu! To jest właśnie ta wyjątkowa sytuacja, kiedy nie stawiamy średnika!

Kiedy stosować średnik, a kiedy nie?

Poprzedni przykład z użyciem instrukcji if i else zawierał instrukcję niezakończoną znakiem średnika. Istnieją jednak sytuacje, kiedy średnik jest wymagany — bez niego program nie zostanie skompilowany. Kiedy więc stosować średnik, a kiedy nie? Poniższy fragment kodu już wymaga postawienia średnika przed else:

  if Imię = 'Adam' then
  begin
    { jakaś inna instrukcja }
    Writeln('Super! Ja też mam na imię Adam!');
  end else Writeln('Cześć ' + Imię);

Także w przypadku, gdy po słowie then stosujemy blok begin..end, średnik musi znaleźć się w kodzie! Ale zasada pozostaje taka sama — nie stosujemy go przed else!

end else Writeln('Cześć ' + Imię);

Sprawa początkowo może wydać się nieco skomplikowana, ale po dokładniejszym przeanalizowaniu kodu można rozróżnić, kiedy należy stosować średnik na końcu, a kiedy nie.

Kilka instrukcji if i else

Możliwe jest połączenie kilku instrukcji if oraz else. W takim przypadku stosujemy następującą konstrukcję:

else if { warunek }

Jeżeli jeden warunek nie zostanie spełniony, nastąpi sprawdzenie drugiego. Jeżeli także drugi nie zostanie spełniony — analizowane będą kolejne warunki. Spójrzmy na poniższy kod:

program if_else;

{$APPTYPE CONSOLE}

var
  I : Integer;
  
begin
  Randomize;
  I := Random(50);

  if i = 10 then Writeln('I = 10')
  else if i = 20 then Writeln('I = 20')
  else if i = 30 then Writeln('I = 30')
  { kod w wykonany przypadku, gdy żaden warunek nie zostanie spełniony }
  else Writeln('Żadna wartość nie jest odpowiednia!');

  Readln;
end.

Na samym początku następuje sprawdzenie, czy wylosowaną liczbą jest 10. Jeśli nie, to program sprawdza kolejno liczby 20 i 30, co zapisano w kolejnych warunkach if. Jeżeli żaden z poprzednich warunków nie zostanie zrealizowany, zostanie wykonany kod umieszczony po słowie else.

Kilka instrukcji po słowie begin

Jak wspomniałem wcześniej, często będziemy stosować się do zasady mówiącej, że gdy po jednym słowie kluczowym (np. else lub then) wystąpi więcej niż jedna instrukcja, cały fragment kodu należy umieścić w dodatkowym bloku begin..end.

  if i = 10 then Writeln('I = 10')
  else if i = 20 then Writeln('I = 20')
  else if i = 30 then Writeln('I = 30')
  { kod wykonywany w przypadku, gdy żaden warunek nie zostanie spełniony }
  else
  begin
    Writeln('Żadna wartość nie jest odpowiednia!');
    Writeln('Spróbuj jeszcze raz');
  end;

Jak widać w powyższym przykładzie, średnik nie został wstawiony ani po słowie else, ani przed nim.

Instrukcja else w case..of

Możliwe jest także zastosowanie słowa kluczowego else w instrukcji case..of. Jeżeli żaden ze sprawdzanych warunków case nie zostanie spełniony, można odpowiednio na to zareagować. Posłużę się jednym z poprzednich przykładów (listing 3.11).

Listing 3.11. Instrukcja else zagnieżdżona w case..of

program case_of_else;

{$APPTYPE CONSOLE}

var
  Mandat : Integer;

begin
  Randomize;
  Mandat := Random(100)+1; // dodajemy 1, aby nie zostało wylosowane 0

  case Mandat of
    1..10: Writeln('E, 10 zł. Może jeszcze zapłacę?');
    11..20: Writeln('20 zł – nie będę miał na chleb!');
    21..50:
    begin
      Writeln('50 zł – Panie władzo... może dogadamy się w inny sposób?');
      Writeln('Nieeeee...');
    end;
      { dodajemy else }
    else Writeln('Jakiś inny mandacik?'); 
  end;

  Readln;
end.

Tym razem losowano liczbę z zakresu od 1 do 100. Dzięki temu instrukcja case może nie uwzględniać tej liczby — zostanie wykonany blok kodu umieszczony po słowie kluczowym else.

Programowanie proceduralne

Idea programowania proceduralnego zaczęła się pojawiać wraz z bardziej zaawansowanymi aplikacjami. Tradycyjny moduł projektowania nie sprawdzał się dobrze, gdy programy zaczęły być bardziej skomplikowane — wówczas ich konserwacja i naprawianie błędów były niezwykle trudne.

Ktoś mądry wymyślił wtedy, że można by było dzielić program na mniejsze części — tzw. procedury. Przykładowo, jeżeli napisano kod, który wyświetla pewien komunikat i kończy działanie programu, a ów fragment jest używany wiele razy w tym programie, to należałoby go dublować wiele razy. Powoduje to nie tylko zwiększenie objętości kodu, ale również potęguje podatność na błędy. Bo co się stanie, jeżeli właśnie w tym małym, wielokrotnie powtórzonym w aplikacji fragmencie, wystąpi błąd? Należałoby wówczas przeszukać cały kod i w każdym miejscu poprawiać usterkę.

Teraz, w nowoczesnych językach programowania, można pewien fragment kodu umieścić w procedurze i — za każdym razem, kiedy zajdzie potrzeba jego wykonania — wywołać procedurę!

Procedury i funkcje

Podczas czytania tego rozdziału Czytelnik mógł zauważyć, że nieraz posługiwałem się słowem procedura lub funkcja w odniesieniu do poleceń języka Delphi. Można by odnieść wrażenie, że wszystkie te słowa są synonimami — jednak sprawa wygląda nieco inaczej.

Procedury

Procedura to wydzielony blok kodu realizujący określone zadanie.

Spójrzmy na program z listingu 3.12.

Listing 3.12. Wyświetlanie okna informacyjnego

program P3_13;

uses
  Windows;

begin
  MessageBox(0, 'Jestem oknem informacyjnym', 'Witaj!', 0);
end.

Warto zwrócić uwagę, że w tym programie brakuje dyrektywy {$APPTYPE CONSOLE}. Pojawił się za to nowy, nieomawiany dotychczas element — sekcja uses. Tym jednak nie będziemy sobie zaprzątać na razie głowy.

Jeżeli skompilujemy i uruchomimy program, zauważymy brak okna konsoli. Zamiast tego pojawi się okno informacyjne z tekstem: Jestem oknem informacyjnym. Po kliknięciu przycisku OK okno zostanie zamknięte, a działanie programu — zakończone.

W tym przypadku procedurą jest MessageBox, która realizuje wyświetlenie okna informacyjnego. Musimy podać jedynie parametry — nie interesuje nas wnętrze procedury, a jedynie sposób jej działania — wyświetlanie okno informacyjnego.

Wyobraźmy sobie, że w danym programie trzeba często wykonywać ten sam blok kodu — np. wyświetlanie kilku tekstów w konsoli. W takim przypadku za każdym razem należałoby wpisywać po kolei instrukcje Writeln. Dzięki zastosowaniu procedur możesz wpisać te instrukcje tylko raz — później pozostaje już tylko umieszczenie w odpowiednim miejscu nazwy procedury, aby wykonać kod w niej zawarty. Przykładowa deklaracja procedury wygląda następująco:

  procedure Nazwa_Procedury;
  begin
    { kod procedury }
  end;

Jak widać, procedurę deklaruje się z użyciem słowa kluczowego procedure, po którym następuje jej nazwa. Nazwa procedury musi być unikalna dla każdego programu — nie może się powtarzać.

Od teraz za każdym razem, gdy gdzieś w kodzie wpiszemy słowo Nazwa_Procedury, zostanie wykonany kod umieszczony w jej wnętrzu. Przykładowy program z wykorzystaniem procedur przedstawiam poniżej.

program ProcApp;

{$APPTYPE CONSOLE}

  procedure Quit;
  begin
    Writeln('Wyjście z programu! Naciśnij Enter, aby zakończyć!');
    Readln;
  end;

var
  Key : Char;

begin

  Writeln('Naciśnij literę Q, aby zakończyć!');
  Readln(Key);

  if Key = 'Q' then 
    Quit 
  else
  begin
    Writeln('Fajnie, że jesteś z nami');
    Readln;
  end;
end.

Trzeba zapamiętać, że procedury deklaruje się poza blokiem begin..end! W powyższym programie zadeklarowałem procedurę Quit. Od tej pory za każdym razem, gdy w programie znajdzie się słowo Quit, nastąpi wyświetlenie informacji i — po naciśnięciu klawisza Enter — zakończenie jego działania.

W programie tym zadeklarowałem nowy typ zmiennej, jakiego do tej pory nie używałem. Typ Char, bo to o nim mowa, umożliwia zapisywanie w pamięci danych w postaci tylko jednego znaku.

MessageBox

Skoro powiedzieliśmy o MessageBox, to wypadałoby wspomnieć coś o parametrach tej procedury. Pierwszym parametrem jest tzw. uchwyt okna. Jest to dość skomplikowany temat, więc na razie nie będziemy o tym mówić — można przyjąć, że jako pierwszy parametr należy podać 0.

Kolejnym parametrem jest tekst, który ma zostać wyświetlony we wnętrzu okna informacyjnego. Pamiętajmy o tym, aby tekst wpisać między znakami apostrofu!

Trzecim parametrem jest tekst, który zostanie wyświetlony na pasku tytułowym okna informacyjnego.

Ostatni parametr określa przyciski, które mają pojawić się we wnętrzu okna — domyślnie (po podaniu liczby 0) będzie to jedynie przycisk OK. Istnieje jednak możliwość podawania innych wartości oraz łączenie tych wartości za pomocą znaku +.

Tabela 3.5. Możliwe wartości ostatniego parametru

Parametr Opis
MB_OK Przycisk OK.
MB_ABORTRETRYIGNORE Przyciski: Przerwij, Ponów, Ignoruj.
MB_OKCANCEL Przyciski: OK oraz Anuluj.
MB_RETRYCANCEL Przyciski: Ponów oraz Anuluj.
MB_YESNOCANCEL Przyciski: Tak, Nie, Anuluj.

W celu zasięgnięcia szczegółowych informacji na temat MessageBox odsyłam do pomocy Delphi.

Funkcje

Funkcje są w swoim działaniu bardzo podobne do procedur. Właściwie w innych językach programowania, takich jak C/C++ czy Java, procedury w ogóle nie istnieją — dostępne są jedynie funkcje. Podstawowa różnica pomiędzy procedurami a funkcjami polega na tym, że te drugie zwracają jakąś wartość.

Deklaracja funkcji jest bardzo specyficzna, następuje bowiem poprzez użycie słowa kluczowego function. Oprócz tego funkcja, jak już mówiłem, musi zwracać jakąś wartość — po znaku dwukropka należy wpisać typ zwracanej wartości:

  function GetName : String;
  begin

  end;

Być może ktoś w dalszym ciągu nie rozumie, na czym polega różnica między funkcją a procedurą — najłatwiej napisać przykładowy program — spójrzmy na listing 3.13.

Listing 3.13. Przykładowy program zawierający funkcję

program FuncApp;

{$APPTYPE CONSOLE}

  function GetName : String;
  begin
    Result := 'Jasio Stasio';
  end;

begin

  Writeln('Nazywam się ' + GetName);
  Readln;

end.

Po uruchomieniu takiego programu na konsoli zostanie wyświetlony tekst Nazywam się Jasio Stasio. To wszystko stanie się za sprawą wiersza:

Result := 'Jasio Stasio';

Słowo Result oznacza jakby ukrytą zmienną — po przypisaniu jej wartości zostanie ona zwrócona przez funkcję.

W Turbo Pascalu nie istniało takie słowo kluczowe jak Result. Zwracanie wartości przez funkcję następowało poprzez wykonanie takiego fragmentu:

nazwa_funkcji := wartosc;

W miejsce Result należało wstawić nazwę funkcji — wówczas program się kompilował, a funkcja zwracała odpowiednią wartość.</dfn>

Jeśli by spróbować to samo zrobić z procedurami — po prostu się to nie uda, ponieważ procedura nie może zwrócić wartości. Program nie zostanie zatem uruchomiony.

Tak naprawdę popełniłem pewną nieścisłość, mówiąc wcześniej o MessageBox jako o procedurze, gdyż jest to funkcja. Ona również zwraca jakiś rezultat (przycisk naciśnięty przez użytkownika), lecz wtedy ten rezultat nie był nam do niczego potrzebny. Nie chciałem jednak Czytelnikowi mieszać w głowie, a MessageBox wydawał się dobrym przykładem na wykorzystanie procedury czy funkcji.

Zmienne lokalne

Tak jak w kodzie programu można zadeklarować zmienne i stałe, tak można je również zadeklarować wewnątrz (w ciele) procedury lub funkcji. Takie zmienne nazywa się zmiennymi lokalnymi o określonym czasie istnienia (bardzo często zwanym czasem życia).

function GetValue : String;
var
  S : String;
begin
  S := 'Khem...';
  Result := S;
end;

Zasada deklarowania zmiennych jest taka sama — deklaracja jest umieszczana przed blokiem begin. Zmienne lub stałe deklarowane w ciele procedury nie są dostępne poza ową procedurą. A zatem w przypadku, gdy w kodzie poza procedurą wystąpi odwołanie do zmiennej umieszczonej w procedurze, Delphi wyświetli komunikat o błędzie.

Dlaczego takie zmienne nazywane są zmiennymi o określonym czasie życia? Przyczyną tego jest fakt, że pamięć dla nich jest alokowana w momencie wywołania procedury, a zwalniania w momencie zakończenia jej działania.

Jeżeli więc piszemy program, który korzysta z wielu procedur, zalecane jest używanie — o ile to możliwe — zmiennych lokalnych zamiast globalnych. Dzięki temu oszczędzamy pamięć komputera.

W dalszej części tej książki używam określenia zmienne globalne. Takie zmienne są po prostu dostępne dla całego programu.

To, że zmienne zawarte w ciele procedury nie są dostępne na zewnątrz, nie oznacza, że procedura nie może korzystać ze zmiennych globalnych. Ta zasada działa tylko w jedną stronę!

Parametry procedur i funkcji

Wraz z wywołaniem danej procedury lub funkcji jest możliwe przekazanie do niej danych — tzw. parametrów. Parametry przekazywaliśmy już nieraz w trakcie realizacji ćwiczeń z tej książki — chociażby w podanej ostatnio przykładowej funkcji MessageBox.

Deklaracja procedury lub funkcji zawierającej parametry wygląda następująco:

  procedure SetName(Name : String);
  begin
    { kod procedury }
  end;

Od tej pory, chcąc wywołać procedurę SetName, należy podać wartość parametru. Parametr ten musi być typu String.

Poniższy, przykładowy program wyświetli na konsoli za pomocą procedury SetName tekst z parametru @@Name@@:

program ProcParam;

{$APPTYPE CONSOLE}

  procedure SetName(Name : String);
  begin
    Writeln('Nazywam się ' + Name);
  end;

begin

  SetName('Adam Boduch');
  Readln;

end.

Procedura SetName odczytuje wartość przekazanego do niej parametru i wyświetla go wraz z pozostałym tekstem na konsoli.

Kilka parametrów procedur lub funkcji

Możliwe jest przekazanie do procedury lub funkcji kilku parametrów tego samego bądź innego typu. W takim przypadku wszystkie parametry należy oddzielić znakami średnika:

function SetName(FName : String; SName : String; Age : Byte) : String;

Zatem aby program został prawidłowo skompilowany, należy przekazać funkcji aż trzy parametry. Jeżeli jednak występuje kilka parametrów tego samego typu, można oddzielić je przecinkami w ten sposób:

function SetName(FName, SName : String; Age : Byte) : String;

Dzięki temu zapis funkcji jest nieco krótszy. Naturalnie z punktu widzenia kompilatora nie ma znaczenia, w jaki sposób jest zapisana deklaracja —jest więc możliwe zapisanie tego w taki, czytelny dla nas sposób:

  function SetName(FName,
                   SName : String;
                   Age : Byte
                   ) : String;
  begin

  end;

Tablica jako parametr procedury

Istnieje możliwość deklaracji parametru procedury jako tablicy. Istnieje jednak pewne ograniczenie — nie można podawać rozmiaru tablicy, czyli parametrem musi być tablica dynamiczna:

procedure ArrayProc(A : array of String);
begin
  Writeln(High(A));
end;

W takim przypadku procedura wyświetli na konsoli największy indeks z tablicy. Tablica jest przekazywana do procedury w standardowy sposób:

var
  A : array[0..1] of String;

begin
  A[0] := 'Delphi';
  A[1] := '2005';
  ArrayProc(A); // wywołanie funkcji
  Readln;

end.

Tablice zwracane przez funkcje

Nie można w standardowy sposób zadeklarować funkcji, która zwracałaby tablicę jako rezultat operacji:

function ArrayFunc : array of String;
begin

end;

Istnieje jednak sposób rozwiązania tego problemu, co wiąże się jednak z zadeklarowaniem nowego, własnego typu danych. O deklaracji własnych typów powiem w dalszej części rozdziału.

Parametry domyślne

Nieco wcześniej wspominałem o funkcjach Inc oraz Dec. Pierwszy ich parametr musiał być nazwą zmiennej, a drugi był opcjonalny. Delphi oferuje przydatną możliwość deklarowania parametrów domyślnych. Oznacza to, że podczas wywoływania procedury lub funkcji taki parametr może ale nie musi zostać wpisany — w takim przypadku Delphi zastosuje wartość domyślną, ustaloną przez programistę. Oto przykład deklaracji takiej procedury:

  procedure SetName(FName : String;
                    SName : String = 'Nieznany';
                    Age : Byte = 0
                   );
  begin

  end;

Parametry domyślne wpisuje się po znaku równości. W przypadku powyższej procedury możliwe jest albo wywołanie jej w ten sposób:

SetName('Moje imię');

albo wpisanie wszystkich parametrów:

SetName('Moje imię', 'Moje nazwisko', 100);

Obydwa sposoby są prawidłowe — jeżeli nie wpiszemy dwóch ostatnich parametrów, Delphi za domyślne wartości uzna te, które zostały wpisane w deklaracji procedury. Zatem dla parametru @@SName@@ będzie to wartość Nieznany, a dla @@Age@@ — 0.

Parametry tego samego typu a wartości domyślne

Niestety, nie jest możliwe deklarowanie kilku parametrów tego samego typu i jednoczesne przypisanie wartości jednemu z nich:

  procedure SetName(FName,
                    SName : String = 'Wartość';
                    Age : Byte = 0
                   );

Powyższa instrukcja spowoduje błąd — zawsze podczas określania wartości domyślnych należy wpisać typ parametru.

Kolejność wartości domyślnych

Należy jeszcze poczynić jedno istotne zastrzeżenie dotyczące deklaracji wartości domyślnych. Otóż błędna jest deklaracja wyglądająca tak:

  function Funkcja(X : Integer = 0; Y : Integer) : Integer;
  begin

  end;

Próbujemy w ten sposób przypisać wartość domyślną pierwszemu parametrowi, a drugiemu nie. Delphi zaprotestuje i wyświetli błąd: [Error] OverProc.dpr(18): Default value required for 'Y'. Jedynym wyjściem jest umieszczenie domyślnych parametrów na samym końcu lub deklaracja wartości domyślnych dla każdego parametru. Taki kod będzie już jak najbardziej prawidłowy:

  function Funkcja(Y : Integer; X : Integer = 0) : Integer;
  begin

  end;

Przeciążanie funkcji i procedur

Stosunkowo nową techniką w Delphi jest możliwość przeciążania procedur i funkcji. Przeciążanie polega na opatrzeniu funkcji i procedury specjalną klauzulą. Dzięki temu kompilator nie będzie protestował, gdy zadeklarujemy kilka funkcji lub procedur o tej samej nazwie! Warunkiem jest jednak to, że parametry muszą być różne, jednak nazwa może pozostać ta sama.

Napiszmy program, który będzie wykorzystywał funkcję wykonującą operację mnożenia — przykładowa funkcja może wyglądać tak:

  function Mnozenie(X, Y : Integer) : Integer;
  begin
    Result := X * Y;
  end;

Zasada działania jest prosta. Funkcja mnoży dwa parametry — @@X@@ i @@Y@@, a następnie zwraca rezultat tego działania. Jednak podane w funkcji parametry mogą być tylko typu Integer, czyli mogą być wyłącznie liczbami całkowitymi. W przypadku gdy chcemy do parametrów przekazać wartości zmiennoprzecinkowe, kompilator zasygnalizuje błąd. Można oczywiście zamiast typu Integer zastosować chociażby typ Currency. Można także wykorzystać dwie funkcje Mnozenie o różnych parametrach:

function Mnozenie(X, Y : Integer) : Integer; overload;
  begin
    Result := X * Y;
  end;

  function Mnozenie(X, Y : Currency) : Currency; overload;
  begin
    Result := X * Y;
  end;

Żeby wszystko zostało dobrze skompilowane, obie funkcje należy oznaczyć klauzulą overload. Od tej chwili podczas wywoływania funkcji Mnozenie kompilator sam — na podstawie otrzymanych parametrów — ustali, jaką funkcję chcemy wywołać:

  Mnozenie(2, 2);
  Mnozenie(2.5, 2.5);

Przekazywanie parametrów do procedur i funkcji

To, co powiedziałem wcześniej na temat prostego przekazywania parametrów do funkcji i procedur, nie wyczerpuje tematu. Istnieje możliwość przekazywania parametrów przez wartość, referencję lub przez stałą.

Przekazywanie parametrów przez wartość

Przekazywanie parametrów przez wartość jest sposobem najprostszym z możliwych. Deklaracja procedury nie musi zawierać żadnych dodatkowych słów kluczowych — wystarczy poniższa konstrukcja nagłówka procedury bądź funkcji:

procedure Foo(S : String);

Przekazanie parametru przez wartość wiąże się utworzeniem jego kopii lokalnej do wykorzystania jedynie przez procedurę lub funkcję. Oryginalna wartość zmiennej przekazanej do procedury nie zostaje w żaden sposób naruszona.

Przekazywanie parametrów poprzez stałą

Idea przekazywania parametrów przez stałą pojawiła się już w Turbo Pascalu 7.0. Umieszczenie w deklaracji parametrów słowa kluczowego const spowoduje przekazywanie parametrów jako stałych.

procedure Show(const Message : String);
begin
  Writeln(Message);
end;

Co to oznacza? Procedura nie może w żaden sposób wpływać na zawartość parametru. Próba nadania przez procedurę jakiejś wartości spowoduje komunikat o błędzie: [Error] ConstParam.dpr(7): Left side cannot be assigned to. Nie można więc zapisać tego w ten sposób:

procedure Show(const Message : String);
begin
  Message := 'Nowa wartość';
  Writeln(Message);
end;

Przekazując parametry przez stałą, pozwalamy kompilatorowi na maksymalną optymalizację kodu.

Przekazywanie parametrów przez referencję

Przekazywanie parametrów przez referencję polega na umieszczeniu przed parametrami słowa kluczowego var. Dzięki temu kod znajdujący się wewnątrz procedury może zmienić wartość parametru. Aby lepiej to zrozumieć, wykonajmy małe ćwiczenie. Spójrzmy na poniższy listing.

Listing 3.14. Wartości przekazywane przez referencję

program VarParam;

{$APPTYPE CONSOLE}

procedure SetValue(Message : String);
begin
{ próba nadania parametrowi nowej wartości }
  Message := 'Hello there!';
end;

var
  S : String;

begin
  S := 'Hello World';
  SetValue(S);  // przekazanie zmiennej
  Writeln(S); // odczyt wartości zmiennej
  Readln;

end.

Teraz zagadka: jaką wartość będzie miała zmienna @@S@@? Uruchommy program i sprawdźmy! Na ekranie konsoli zostanie wyświetlony napis Hello World, co oznacza, że procedurze nie udało się zmienić wartości parametru. Wszystko dlatego, że w przypadku przekazywania parametru przez wartość, jest tworzona lokalna kopia zmiennej.

Zmieńmy teraz nieco deklarację procedury SetValue:

procedure SetValue(var Message : String);
begin
{ próba nadania nowej wartości dla parametru }
  Message := 'Hello there!';
end;

Dzięki dodaniu słowa var i ponownemu uruchomieniu programu można przekonać się, że zmienna @@S@@ będzie miała wartość Hello there. Oznacza to, że naszej procedurze udało się zmienić wartość tego parametru.

Sposób przekazywania danych przez referencję jest optymalny, gdyż nie jest tworzona kopia zmiennej. W związku z tym jest możliwe przekazywanie danych z procedury na zewnątrz (co pokazałem na poprzednim przykładzie).

Procedury zagnieżdżone

Nic nie stoi na przeszkodzie, aby daną procedurę lub funkcję umieścić w innej procedurze lub funkcji. Wygląda to mniej więcej tak:

procedure A;
  procedure B;
  begin

  end;
begin

end;

Z powyższego zapisu wynika, że procedura lub funkcja zagnieżdżona (w tym wypadku procedura B) musi zostać umieszczona przed blokiem begin.

W takim przypadku nadal obowiązują zasady o zmiennych lokalnych. Oznacza to, że zmienna umieszczona w procedurze B nie będzie dostępna dla procedury A.

Wplatanie funkcji i procedur

Nowością w Delphi 2005 jest możliwość wplatania funkcji i procedur. Polega to na opatrzeniu funkcji/procedury słowem kluczowym inline — np.:

procedure Foo; inline;
begin
  Console.WriteLine('Procedura wplatana...');
end;

begin
  Foo;
end.

W powyższym przykładzie procedura Foo nie jest wywoływana, jej kod jest kopiowany do miejsca wywołania. Można powiedzieć, że kompilator zastępuje powyższy kod na następujący:

begin
  Console.WriteLine('Procedura wplątana...');
end.

W niektórych przypadkach pozwala to na zwiększenie wydajności aplikacji, lecz zwiększa się przez to rozmiar aplikacji wykonywalnej. Dobrym rozwiązaniem będzie umieszczenie w kodzie programu dyrektywy kompilatora:

{$INLINE AUTO}

Dzięki temu kompilator sam oceni, czy należy wplatać daną procedurę/funkcję, czy też nie.

Własne typy danych

Delphi umożliwia deklarowanie własnych typów danych, które następnie można wykorzystywać w programie. Własny typ danych można zadeklarować za pośrednictwem słowa kluczowego type. Przykładowa deklaracja własnego typu mogłaby wyglądać następująco:

type
  TMediumValue = 0..20;

W świecie programistów Delphi przyjęło się już, że każdy nowy typ danych jest poprzedzony dużą literą T — ja nie zamierzam od tej reguły odstępować.

Od tej pory możemy w swoim programie używać własnego typu danych — TMediumValue. Ten typ może przybierać wartości liczbowe od 0 do 20.

Wykorzystanie takiego typu danych (powołanie zmiennej) jest proste:

var
  MediumValue : TMediumValue;

Można już korzystać z naszej zmiennej, tak jak z każdej innej:

begin
  MediumValue := 10;
end.

Możliwe jest także zadeklarowanie swojego własnego typu wyglądającego na przykład tak:

TSamochody = (tsFiat, tsMercedes, tsOpel);

Po utworzeniu zmiennej wskazującej na ten typ będzie mogła ona zawierać jedną z podanych w nawiasie wartości.

Tablice jako nowy typ

Możliwe jest przekazywanie całych tablic jako parametrów do funkcji lub procedury. Jednak w przypadku funkcji nie da się tego uczynić bezpośrednio — należy w tym celu utworzyć nowy typ danych, np. taki:

type
  TMyArray = array[0..20] of String;

Dopiero teraz można przekazać go jako parametr, tak jak przedstawiono to w poniższym przykładzie:

program OwnType;

{$APPTYPE CONSOLE}

type
  TMyType = array of String;

function ArrayFunc : TMyType;
begin
  SetLength(Result, 2);
  Result[0] := 'Delphi';
  Result[1] := '2005';
end;

var
  A : TMyType;

begin
  SetLength(A, 2);
  A := ArrayFunc;
  Writeln(A[0]);
  Writeln(A[1]);
  Readln;

end.

W programie został zadeklarowany nowy typ danych — TMyType, który jest w rzeczywistości tablicą. Typ ten został użyty w funkcji ArrayFunc jako wartość zwracana przez funkcję. Jedyne co trzeba teraz zrobić, to zainicjalizować tablicę oraz przydzielić do niej elementy. W bloku begin..end należy z funkcji odebrać owe elementy — także poprzez inicjalizację tablicy typu TMyType.

Teraz można skompilować program i sprawdzić jego działanie — powinien działać bez problemu.

Uważaj na inicjalizację liczby elementów tablicy za pomocą instrukcji SetLength. Nieprawidłowe podanie liczby elementów w drugim parametrze może spowodować błąd związany z pamięcią, co w konsekwencji uniemożliwi dalsze wykonywanie programu.

Aliasy typów

Aliasy służą do tworzenia nowego typu, który w rzeczywistości wskazuje na inny typ i jest mu równoważny:

Type
  TMojTyp = Integer;

Od tego momentu TMojTyp będzie równoważny typowi Integer. Z aliasów można korzystać, jeśli projektant chce zwiększyć czytelność kodu swojego programu. Ogólnie rzecz biorąc, nie jest to często wykorzystywana możliwość.

Rekordy

Rekordy są zorganizowaną strukturą danych, połączona w jedną całość. Jest to jakby zestaw zawierający określone elementy. Rekordy również można przekazywać — jako zestaw elementów — do funkcji czy procedur w formie parametru.

Nowe rekordy można deklarować jako nowy typ danych lub jako zmienną przy użyciu słowa kluczowego record.

type
  TMyRecord = record
    X : Integer;
    Y : Integer;
  end;

Budowa deklaracji rekordu, jak widać, jest specyficzna — najpierw należy wpisać jego nazwę, a potem po znaku równości słowo kluczowe record (uwaga, brak średnika na końcu!). Następnie wypisujemy elementy, z których ma się składać nasz rekord.

Jako że powyżej zadeklarowaliśmy rekord jako nowy typ danych, należy utworzyć dodatkowo zmienną wskazującą na ten typ. Przy tej okazji przedstawię nowy operator Delphi — kropkę (.). Do poszczególnych pól rekordu odwołujemy się następująco:

MyRecord.X := 1;

po uprzednim utworzeniu zmiennej wskazującej na rekord.

Po co stosować rekordy? Czasem trzeba przekazać określoną strukturę jako parametr funkcji bądź procedury. Można by oczywiście przekazywać pojedynczo kolejne elementy do procedury, jednak często wygodniej przekazać jeden parametr — w formie rekordu. Usprawnia to pracę i jest dość często stosowanym elementem programów.

Przekazywanie rekordów jako parametrów procedury

Napiszmy prosty program, który pobierze dwie liczby wpisane przez użytkownika i przekaże do procedury cały rekord.
Samo przekazanie rekordu do funkcji przebiega w sposób dość prosty:

type
  TMyRecord = record
    X : Integer;
    Y : Integer;
  end;

  function Dzielenie(MyRecord : TMyRecord) : Integer;
  begin
    Result := MyRecord.X div MyRecord.Y;
  end;

Delphi wymaga, aby do funkcji Dzielenie został przekazany rekord TMyRecord. Kolejno następuje podzielenie elementu @@X@@ przez element @@Y@@ rekordu i zwrócenie wartości dzielenia. Cały program wygląda tak jak na listingu 3.15.

Listing 3.15. Rekord przekazany jako parametr procedury

program Recordapp;

{$APPTYPE CONSOLE}

type
  TMyRecord = record
    X : Integer;
    Y : Integer;
  end;

  function Dzielenie(MyRecord : TMyRecord) : Integer;
  begin
    Result := MyRecord.X div MyRecord.Y;
  end;

var
  MyRecord : TMyRecord;
  Result : Integer;

begin
  Writeln('Podaj pierwszą liczbę');
  Readln(MyRecord.X);

  Writeln('Podaj drugą liczbę');
  Readln(MyRecord.Y);

  Result := Dzielenie(MyRecord);

  Writeln('Rezultat dzielenia');
  Writeln(Result);
  Readln;
end.

Na samym początku tego programu zadeklarowano nowy typ — TMyRecord, który jest rekordem. Teraz, żeby takiego rekordu użyć, należy zadeklarować zmienną, która wskazuje na ten typ.

Dalsza część programu powinna być oczywista — pobieramy tutaj dwie wartości od użytkownika i przypisujemy je do elementów rekordów, tak jak robimy to ze zwykłą zmienną. Następnie cały rekord przekazujemy do funkcji, która dokonuje dzielenia.

Deklarowanie rekordu jako zmiennej

Nie jest konieczne tworzenie nowego typu dla rekordu. Oznacza to, że zamiast deklarować kolejny rekord jako nowy typ (type), można zadeklarować go jako zmienną:

var
  Rec : record
    X, Y : Integer;
  end;

Wówczas nie jest też konieczne tworzenie nowej zmiennej — od razu można zabrać się za przypisywanie danych do elementów rekordu.

Instrukcja packed

W celu zapewnienia szybszego działania rozmiary rekordów są zaokrąglane do wartości 1, 2, 4 lub 8 bajtów. Oznacza to, że po zsumowaniu wszystkich elementów rekordu i określeniu, ile miejsca zajmie on w pamięci — całość jest dodatkowo zaokrąglana.
Umieszczenie instrukcji packed podczas deklaracji rekordu powoduje, że zostanie on „skompresowany”. Minusem takiej kompresji jest wolniejsze działanie gotowej aplikacji.

type
  TMyRec = packed record
    X, Y : Integer;
  end;  

Z instrukcji packed można także korzystać w połączeniu z tablicami. Na platformie .NET działanie tej instrukcji zostało nieco zmodyfikowane (patrz rozdział 11. „Migracja do .NET”).

Deklarowanie tablic rekordowych

Jeżeli zadeklarujemy rekord jako nowy typ danych (z użyciem klauzuli type), to możemy utworzyć tablicę rekordów.
Postanowiłem to opisać, ponieważ początkującym programistom uzyskiwanie dostępu do danych w takiej formie może sprawiać problem. Deklaracja takich danych odbywa się w następujący sposób:

type
  TMyRecord = record
    X, Y : Integer;
    Str : String;
  end;

var
  MyArray : array[0..10] of TMyRecord;

Tablice w takich przypadkach deklarujemy tak, jakby były to zwykłe dane.

Następnie w celu przypisania danych do takiej tablicy należy użyć nawiasów kwadratowych oraz operatora przypisania (kropki) jednocześnie:

  MyArray[0].X := 1;
  MyArray[0].Y := 2;
  MyArray[0].Str := 'ABC';

  MyArray[1].X := 13;
  MyArray[1].Y := 23;
  MyArray[1].Str := 'ABCDEEF';

  {...}

Deklarowanie dynamicznych tablic rekordowych

Deklarowanie tablic dynamicznych z użyciem rekordów jest bardzo podobne do standardowego deklarowania tablic dynamicznych:

var
  MyArray : array of TMyRecord;
begin
  SetLength(MyArray, 2);
  MyArray[0].X := 1;
  MyArray[1].X := 2;
  {...}
  Readln;
end.

Delphi ma to do siebie, że ułatwia pracę, gdyż jest bardzo intuicyjne. Jak widać w powyższym przykładzie, uzyskanie takiego efektu łączy się ze znajomością deklarowania rekordów oraz tablic dynamicznych.

Instrukcja wiążąca with

Instrukcja with jest przeważnie używana wraz z rekordami lub obiektami (o obiektach będzie mowa w rozdziale 6.). Nie pełni ona żadnej znaczącej roli — uwalnia za to programistę od pisania zbędnego kodu. Załóżmy, że program zawiera następujący rekord:

var
  Rec : packed record
    X, Y : Integer;
    Imię : String[20];
    Nazwisko : String[20];
    Wiek : Byte;
  end;

Prawidłowe wypełnienie rekordu jest przedstawione poniżej:

  Rec.X := 12;
  Rec.Y := 24;
  Rec.Imię := 'Jan';
  Rec.Nazwisko := 'Kowalski';
  Rec.Wiek := 20;

Dzięki zastosowaniu instrukcji wiążącej with kod ten można skrócić do następującej postaci:

  with Rec do
  begin
    X := 12;
    Y := 24;
    Imię := 'Jan';
    Nazwisko := 'Kowalski';
    Wiek := 20;
  end;

Nie wspomniałem do tej pory o jednym elemencie pojawiającym się podczas deklarowania zmiennej typu String. Możliwe jest bowiem określenie długości zmiennej String, co znalazło swoje odzwierciedlenie w powyższym przykładzie. Wystarczy w deklaracji wpisać w nawiasach kwadratowych maksymalną długość, jaką może mieć zmienna.

Programowanie strukturalne

Nieco wcześniej w tym rozdziale wspomniałem o programowaniu proceduralnym, które jest znaczącym ułatwieniem pracy podczas pisania programów.

Podział na procedury i funkcje jednak nie wystarczył, gdy programy stawały coraz dłuższe. Wówczas ktoś wpadł na pomysł, aby części kodu źródłowego podzielić na mniejsze pliki. Taką możliwość wprowadzono także w jednej z wcześniejszych wersji Turbo Pascala.

Ideę dzielenia kodu na mniejsze pliki nazwano programowaniem strukturalnym.

Moduły

Moduł (ang. unit) jest plikiem tekstowym zawierającym polecenia przetwarzane przez kompilator. Inaczej mówiąc, jest to fragment kodu źródłowego.

Zastosowanie modułów pozwala na podział kodu na osobne pliki. Przypomnijmy sobie materiał zawarty w rozdziale 1. Podczas tworzenia pierwszego programu i zapisywania pierwszego projektu na dysku znalazł się plik *.pas. Jest to właśnie plik modułu. Każdemu formularzowi odpowiada jeden moduł, ale z kolei moduł nie musi być zawsze formularzem.

Jakie są zalety programowania strukturalnego (modularnego)? Otóż programista ma możliwość umieszczenia części kodu (np. paru procedur) w osobnych modułach. Wyobraźmy sobie, że tworzymy dość spory program i cały kod źródłowy jest zawarty w jednym pliku, podzielony na procedury. W takim pliku trudno się będzie później odnaleźć — przede wszystkim ze względu na jego długość. Koncepcja programowania modularnego polega na podzieleniu takiego programu na kilka plików i włączaniu ich po kolei do głównego pliku źródłowego.

Dzięki temu możemy w jednym pliku, np. interfaces.pas, zawrzeć jedynie procedury związane z tworzeniem interfejsu użytkownika, a z kolei w pliku db.pas umieścić procedury związane z obsługą bazy danych.

Tworzenie nowego modułu

#Z menu File wybierz New/Other.
#Następnie zaznacz Delphi Projects i wybierz Console Application.
#Z menu File wybierz polecenie New => Unit — Delphi for Win32. Spowoduje to stworzenie w Edytorze kodu nowej zakładki (nowego modułu).
#Z menu File wybierz polecenie Save As — plik zapisz pod nazwą MainUnit.

Budowa modułu

Zaraz po utworzeniu nowego modułu Delphi generuje potrzebne instrukcje, które są niezbędnym elementem tego modułu.

unit MainUnit;

interface

implementation

end.

Nazwa

Pierwszy wiersz zawiera instrukcję unit, która określa nazwę modułu. Nie należy jej zmieniać — Delphi generuje tę nazwę automatycznie w momencie zapisywania pliku na dysku. Podczas próby zmiany nazwy modułu wystąpi błąd: [Error] MainUnit.pas(1): Unit identifier 'MainUnitx' does not match file name.

Dzieje się tak, gdyż nazwa modułu jest jednocześnie nazwą pliku *.dcu i *.pas w projekcie. Przykładowo, jeżeli moduł nosi nazwę MainUnit, to Delphi zapisze plik źródłowy pod nazwą MainUnit.pas oraz plik skompilowany pod nazwą MainUnit.dcu.

Sekcja Interface

Jest to tzw. część publiczna modułu. Tutaj należy umieszczać deklaracje procedur i funkcji, które mają być dostępne „na zewnątrz”, dla innych modułów. W tym momencie należy rozróżnić pewne pojęcia, takie jak deklaracja oraz definicja.

Deklaracja jest udostępnieniem jedynie nagłówka funkcji lub procedury. Dzięki temu procedury i funkcje są dostępne dla innych modułów. Np. deklaracja procedury ProcMain może wyglądać następująco:

procedure ProcMain(Param1, Param2 : Integer);
</dfn>

Definicja to cały kod procedury lub funkcji zawarty w sekcji Implementation.

Oczywiście można pominąć deklarację i od razu w sekcji implementation wpisać całe ciało procedury czy funkcji, ale trzeba liczyć się z tym, że nie będzie ona dostępna poza modułem.

Sekcja Implementation

Sekcja Implementation stanowi część prywatną modułu. Kod w niej zawarty w (procedury, funkcje, zmienne, tablice czy stałe) nie jest dostępny dla innych modułów — z punktu widzenia kompilatora dane zawarte w tej sekcji nie istnieją dla innych modułów.

W sekcji Implementation należy także umieszczać definicję procedur i funkcji. Przykład prawidłowego modułu (deklaracji i definicji) to:

unit MainUnit;

interface

  procedure ProcMain(Param1, Param2 : Integer);

implementation

procedure ProcMain(Param1, Param2 : Integer);
begin

end;

end.

Trzeba pamiętać o tym, że również moduł musi być zakończony instrukcją end. (kropka na końcu!).

Włączanie modułu

Podczas poprzedniego ćwiczenia, którego celem było utworzenie nowego modułu, można było się dowiedzieć, że język Delphi zwolnił programistę z obowiązku włączenia tego modułu do głównego pliku *.dpr.

Włączenie modułu do programu (lub do innych modułów) realizowane jest za pomocą dyrektywy uses.

uses
  MainUnit in 'MainUnit.pas';

Od tej pory wszystkie deklaracje z modułu MainUnit będą dostępne także w obrębie programu głównego. Taka wygenerowana przez Delphi konstrukcja nie jest jedyną poprawną konstrukcją. Wystarczy napisać tylko:

uses
  MainUnit;

Aby włączyć do programu kilka modułów, należy je wypisać jeden po drugim, oddzielając ich nazwy przecinkami.

Funkcje wbudowane

Od momentu włączenia modułu do aplikacji można korzystać z funkcji zawartych w owym module — to jasne. Jednak na początku tego rozdziału korzystaliśmy z funkcji typu Writeln, Readln czy Inc. Są to tzw. funkcje wbudowane — ich deklaracja nie jest umieszczona w żadnym module, więc są dostępne w każdym miejscu programu — stanowią po prostu element języka Delphi.

Różnica między zwykłymi funkcjami a tymi zaimplementowanymi dodatkowo w Delphi jest wyraźnie widoczna na przykładzie funkcji MessageBox, z której korzystaliśmy wcześniej. Żeby użyć tej funkcji ( tak aby była dostępna dla kompilatora), trzeba było dołączyć do programu moduł Windows.

Sekcje Initialization oraz Finalization

Dwie wspomniane sekcje — Initialization oraz Finalization — są opcjonalnymi sekcjami modułu.

Umieszczenie kodu bezpośrednio za sekcją Initialization powoduje wykonanie go zaraz po uruchomieniu programu, natomiast kod znajdujący się za sekcją Finalization zostanie wykonany po zakończeniu pracy z modułem. Przykład przedstawiono na listingu 3.16.

Listing 3.16. Przykład użycia initialization oraz finalization

unit MainUnit;

interface

uses Windows; // włączamy moduł Windows

  procedure ProcMain(Param1, Param2 : Integer);

implementation

procedure ProcMain(Param1, Param2 : Integer);
begin

end;

initialization
  MessageBox(0, 'Rozpoczynamy pracę z modułem...', 'OK', MB_OK);

finalization
  MessageBox(0, 'Kończymy pracę z modułem...', 'OK', MB_OK);


end.

Po uruchomieniu programu zostanie wyświetlone okno dialogowe. Tak samo stanie się po zamknięciu naszej aplikacji.

Najczęściej sekcje Initialization oraz Finalization są używane do przypisywania lub zwalniania danych (pamięci), przydzielanych w ramach konkretnego modułu.

Jeżeli w programie korzysta się z sekcji Finalization, to konieczne staje się umieszczenie także sekcji Initialization. W przeciwnym przypadku próba uruchomienia programu zakończy się błędem: [Error] MainUnit.pas(17): Declaration expected but 'FINALIZATION' found. Sekcja Initialization może nie zawierać żadnego kodu — istotna jest tylko jej obecność w programie.

Jeżeli chodzi o sekcję Finalization, to jest ona opcjonalna. Jeżeli np. w sekcji Finalization należy umieścić jakiś kod zwalniający pamięć, to nie będzie to konieczne w środowisku takim jak .NET, które samodzielnie dba o zwalnianie pamięci.

Dyrektywa forward

Nie wspomniałem o tej dyrektywie podczas omawiania procedur i funkcji, gdyż wiąże się ona w jakimś stopniu z modułami.
Po zadeklarowaniu nagłówka procedury czy funkcji należy także umieścić jej definicję w sekcji implementation, aby była dostępna na zewnątrz modułu — to już wiemy. Trochę skłamałem wcześniej, wskazując, że deklarowanie tych samych parametrów funkcji zarówno w definicji, jak i w deklaracji jest konieczne, tak jak to pokazano poniżej:

...
interface
  function Main(const : String) : Integer;
implementation
...
function Main(const : String) : Integer;
begin

end;
...

Można pominąć cały nagłówek funkcji w sekcji implementation, skracając zapis do następującej postaci:

...
interface
  function Main(const : String) : Integer;
implementation
...
function Main;
begin

end;
...

Warto zwrócić uwagę, że w sekcji implementation nie jest konieczne nawet wpisanie typu zwracanego przez funkcję Main — wszystko to jest określone w definicji w sekcji interface.

Dyrektywa forward wiąże się z deklarowaniem procedur bądź funkcji jedynie w sekcji implementation albo w ogóle w pliku głównym programu (**.dpr*). Wiadomo, że kompilator odczytuje kod od góry do dołu, wiec taki zapis będzie dla niego niepoprawny:

program Example;

{$APPTYPE CONSOLE}

procedure Main;
begin
{ wywołanie procedury, która jest nie widoczna, gdyż jej
  deklaracja znajduje się poniżej }
  DoIt('Test');
end;

procedure DoIt(var S : String);
begin

end;

begin

end.

Wczujmy się na chwilę w sposób pracy kompilatora. Analizując kod od góry do dołu, napotyka on na nieznaną instrukcję — DoIt — w ciele procedury Main. Owa instrukcja jest dla niego nierozpoznawalna, więc może przyjąć, że jest to jakaś funkcja. W tym momencie sprawdza kod powyżej i nie na potyka na procedurę ani funkcję o nazwie DoIt, a zatem zgłasza błąd. Czasem można spotkać się z takim problemem, jeśli zadeklarowanie procedury zaraz na początku jest niemożliwe, gdyż oddziałuje ona z kolei na inną procedurę, której kod znajduje się poniżej. W takim przypadku można zastosować dyrektywę forward i napisać tak:

program Example;

{$APPTYPE CONSOLE}

uses Windows;

procedure DoIt(const S : String); forward;

procedure Main;
begin
{ wywołanie procedury, która jest nie dostępna, gdyż jej
  deklaracja znajduje się poniżej }
  DoIt('Test');
end;

procedure DoIt;
begin
  Writeln(S);
end;

begin
  Main; // wywołanie głównej procedury
end.

Powyższy kod zostanie skompilowany bez problemu, gdyż na samej górze zadeklarowaliśmy jedynie nagłówek procedury oraz opatrzyliśmy ją dyrektywą forward. Dalszy kod owej procedury można zapisać w dalszej części programu.

Zwróćmy uwagę, że jedynie nagłówek zawiera listę parametrów procedury. Ciało procedury zawiera tylko nazwę. Jest to przydatny zapis, gdyż niejeden raz projektant jest zmuszony do zmiany listy parametrów. Wówczas trzeba by zmienić te parametry w deklaracji procedury oraz w definicji, a dzięki dyrektywie forward parametry są zapisane jedynie w definicji, stąd ich zmiana jest szybka i bezproblemowa.

Konwersja typów

Środowisko Delphi zostało tak zaprojektowane, aby nie dopuszczać do sytuacji, gdy zmienna typu Integer jest przekazywana np. do procedury MessageBox, która jako parametru wymaga danych typu String. W takich przypadkach z pomocą przychodzą nam funkcje konwersji, które pozwalają przekształcić dane do innego typu. W rezultacie, aby przekazać do procedury MessageBox zmienną typu Integer, wystarczy umieścić w kodzie taki zapis:

MessageBox(0,
  IntToStr(Zmienna_Integer),
  '',
  0);

Polecenie IntToStr powoduje konwersję danych w postaci Integer do formatu String (innymi słowy, przyjmuje jako parametr wartość Integer, a w rezultacie zwraca wartość typu String).

Funkcje konwersji, przedstawione w tabeli 3.6, są zadeklarowane w module SysUtils — koniecznym staje się więc włączenie nazwy tego modułu do listy uses.

Tabela 3.6. Funkcje konwersji

Nazwa funkcji Opis funkcji
IntToStr Konwertuje typ Integer na String.
StrToInt Konwertuje typ String na Integer.
CurrToStr Konwertuje typ Currency na String.
StrToCurr Konwertuje typ String na Currency.
DateTimeToStr Konwertuje typ TDateTime na String.
StrToDateTime Konwertuje typ String na TDateTime.
DateToStr Konwertuje typ TDate na String.
StrToDate Konwertuje typ String na TDate.
TimeToStr Konwertuje typ TTime na String.
StrToTime Konwertuje typ String na TTime.
FloatToStr Konwertuje typ Extended na String.
StroToFloat Konwertuje typ String na Extended.
IntToHex Konwertuje typ Integer do postaci heksadecymalnej.
StrPas Konwertuje typ String na PChar.
StrPCopy Konwertuje typ PChar na String.
StrToBool Konwertuje typ String na Boolean.
StrToInt64 Konwertuje typ String na Int64.

Przyznam, że tych funkcji jest sporo. Nie trzeba ich wszystkich pamiętać — zawsze można sięgnąć do tej książki. Jednak większość z tych nazw jest intuicyjna i stanowi skrót nazw konwertowanych typów.

W poprzednim przykładzie specjalnie popełniłem błąd. Do funkcji MessageBox przekazujemy parametr, który jest konwertowany z typu Integer do String. Jednak parametrem funkcji MessageBox jest PChar i taki kod nie zostanie skompilowany. Skoro nie ma funkcji konwertującej typ Integer na PChar, należy dodatkowo zastosować rzutowanie.

Rzutowanie

Przy rzutowaniu należy zachować szczególną ostrożność. Jest to bowiem sposób na „oszukanie” kompilatora. Jeżeli nie jesteśmy pewni, co robimy, możemy w konsekwencji doprowadzić do wystąpienia poważnych błędów podczas działania programu.

Najlepiej omówić to na przykładzie. Oto prosty kod źródłowy, który na pewno nie zostanie prawidłowo skompilowany:

var
  VarC : Char;
  VarB : Byte;

begin
  VarC := 'A';
  VarB := VarC;
end. 

Dane w postaci Char (pojedynczy znak) próbujemy tu przypisać do zmiennej @@VarB@@, która jest zmienną typu Byte. Oczywiście kompilator wskaże błąd: [Error] typcast.dpr(12): Incompatible types: 'Byte' and 'Char'. Po drobnej modyfikacji cały program zostanie skompilowany prawidłowo i zadziała bez problemu:

var
  VarC : Char;
  VarB : Byte;

begin
  VarC := 'A';
  VarB := Byte(VarC); // <–– rzutowanie
end.

Rzutowaniem jest właśnie przypisanie danych w ten sposób: Byte(VarC). W takim przypadku rzutujemy typ Char na Byte, w wyniku czego zmienna @@VarB@@ będzie posiadać wartość 65 (kod ASCII litery A).

Funkcja MessageBox wymaga typu PChar (który również jest typem znakowym), zatem należy zastosować następujące rzutowanie:

MessageBox(0,
  PChar(IntToStr(Zmienna_Integer)),
  '',
  0);

Parametry nieokreślone

Przy okazji omawiania rzutowania należałoby wspomnieć o pewnej możliwości deklarowania funkcji i procedur z parametrami nieokreślonymi, np.:

procedure Main(const S);

Powyższa procedura Main posiada jeden parametr — @@S@@, który jest nieokreślony, ale kompilator Delphi uzna ten kod jako prawidłowy.

Dlaczego wspominam o tym podczas omawiania rzutowania? Otóż wykorzystanie takiego parametru nie jest możliwe bez rzutowania — oto przykład:

program Typecast;

{$APPTYPE CONSOLE}

procedure Main(var S);
begin
  Writeln(String(S));
end;

var
  S : String;
begin
  S := 'To jest String';
  Main(S);
  Readln;
end.

Program na początku deklaruje zmienną @@S@@, przypisuje do niej dane i przekazuje procedurze @@S@@. Procedura ta z kolei powinna wyświetlić wartość @@S@@ na ekranie. Jednak bez użycia rzutowania nie jest to możliwe — kompilator wyświetli komunikat o błędzie: [Error] P2_22.dpr(9): Illegal type in Write/Writeln statement.

Pętle

W świecie programistów pod słowem pętla kryje się pojęcie oznaczające wielokrotne wykonywanie tych samych czynności. Jest to bardzo ważny element każdego języka programowania, dlatego konieczne jest zrozumienie istoty działania tego elementu.

Wyobraźmy sobie sytuację, w której trzeba kilka razy wykonać tę samą czynność. Może to być na przykład wyświetlenie kilku linii tekstu. Zamiast wielokrotnie pisać instrukcję Writeln, można skorzystać z pętli. Za chwilę przekonamy się, że zastosowanie pętli w programie wcale nie jest trudne.

Pętla for..do

Jest to chyba najprostsza z możliwych pętli. Używa się jej zawsze wtedy, gdy wiadomo dokładnie, ile wykonań (iteracji) danej czynności należy zastosować. Podczas korzystania z pętli for trzeba zadeklarować zmienną, która za każdym wykonaniem danej czynności będzie przybierać wartość aktualnej iteracji. Żeby to lepiej zrozumieć, spójrzmy na przykładową pętlę:

for Zmienna := 1 to 10 do { instrukcje }

Pierwszą instrukcją musi być słowo for. Po nim następuje nazwa zmiennej, która musi przybrać wartość początkową. Następnie kolejne słowo kluczowe — to, po którym następuje wartość końcowa pętli. Powyższa konstrukcja spowoduje 10-krotne wykonanie poleceń umieszczonych po słowie do. Podsumowując, budowa pętli przedstawia się następująco:

for Zmienna := {Wartość początkowa} to {Wartość końcowa} do {instrukcje }

Przykładowy program wyświetla na ekranie konsoli następujący tekst:

Odliczanie 1
Odliczanie 2
...

W celu zrealizowania tego zadania bez korzystania z pętli należałoby 10 razy przepisać instrukcję Writeln, co jest po prostu stratą czasu. Przykładowy listing 3.17 prezentuje wykorzystanie pętli for w celu 10-krotnego wyświetlanie tekstu.

Listing 3.17. Zastosowanie pętli for
''program ForLoop;

{$APPTYPE CONSOLE}

uses
SysUtils;

var
I : Integer; // deklaracja zmiennej

begin

for I := 1 to 10 do
Writeln('Odliczanie... ' + IntToStr(i));

Readln;
end.''

Teraz można uruchomić program i sprawdzić jego działanie. Można pozmieniać wartość początkową i końcową, aby sprawdzić, jakie będzie zachowanie programu.

Warto zauważyć, że zastosowaliśmy tutaj funkcję IntToStr w celu wyświetlenia zarówno liczby i tekstu, korzystając z jednej instrukcji — Writeln.

Odliczanie od góry do dołu

Pętla, jaką przedstawiłem powyżej, realizuje odliczanie od dołu (wartości mniejszej) do góry (wartość wyższa). Istnieje możliwość odliczania odwrotnego, czyli od wartości wyższej do mniejszej. W tym celu słowo kluczowe to należy zastąpić słowem downto. Wystarczy zatem drobna zmiana pętli:

  for I := 10 downto 1 do
    Writeln('Odliczanie... ' + IntToStr(i));

Teraz program wykona pętlę z wartością początkową równą 10.

Z pętlą for wiąże się jedno ostrzeżenie kompilatora. Otóż w przypadku, gdy pętla for znajduje się wewnątrz procedury lub funkcji, zmienna pomocnicza musi być zmienną lokalną. Przykładowo, próba skompilowania takiego kodu:

var
I: Integer; // deklaracja zmiennej

procedure Loop;
begin
for I := 0 to 100 do { instrukcje }
end;

wiąże się z wystąpieniem ostrzeżenia: [Warning] LoopBreak.dpr(10): For loop control variable must be simple local variable. Oznacza to, iż zmienna (w tym przypadku @@I@@) może być modyfikowana przez inne procedury, a to nie jest zalecane. Umieszczenie deklaracji zmiennej wewnątrz procedury Loop pozwoli na prawidłową kompilację programu.</dfn>

Licznik pętli

W pętli for obowiązkowe jest użycie jednej zmiennej, która przy każdej iteracji pętli będzie stanowiła licznik wykonania (czyli określała liczbę już wykonanych iteracji). Tutaj uwaga dla programistów C: w Delphi pętla for nie jest aż tak elastyczna jak w językach C/C++ — nie jest możliwy „przeskok” pętli od razu o dwie pozycje [#]_. W Delphi zmienna pomocnicza (w tym wypadku @@I@@) zawsze będzie się zwiększać o jedną pozycję.

Zwalnianie wykonywania pętli

Pętla jest wykonywana w takim krótkim czasie, że niekiedy trudno zauważyć jej realne działanie — na ekranie komputera obserwujemy już rezultat wykonania całej pętli. Istnieje możliwość lekkiego opóźnienia kolejnej iteracji, w tym celu należy wykorzystać procedurę Sleep. Mówię o tym, gdyż użycie tej instrukcji może się przydać nie tylko w związku z pętlami, ale również w innych sytaucjach.

Do funkcji Sleep należy przekazać wartość czasu, o którą dalsze wykonywanie programu ma zostać wstrzymane, np.:

Sleep(1000);

Wartość w nawiasie podajemy w milisekundach, tak więc w powyższym przykładzie działanie programu zostanie zwolnione o 1 sekundę.

W Turbo Pascalu odpowiednikiem funkcji Sleep jest komenda Delay.

Pętla while..do

Niekiedy nie jesteśmy w stanie określić liczby wymaganych iteracji w pętli. Być może nie będzie wymagana żadna iteracja, a może potrzebne będą ich setki? W takim przypadku należy skorzystać z pętli while. Budowa takiej pętli jest następująca:

while {Warunek do spełnienia} do
  { instrukcje }

Pętla będzie wykonywana, dopóki warunek zapisany pomiędzy słowami kluczowymi while i do nie zostanie spełniony.

Napiszmy prosty program, którego działanie polega na pobieraniu hasła dostępu. Jeżeli hasło będzie błędne, pętla zostanie wykonana po raz drugi; jeżeli hasło będzie poprawne — nastąpi zakończenie działania programu (patrz listing 3.18).

Listing 3.18. Prezentacja pętli while

program WhileLoop;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  Password : String; // deklaracja zmiennej

begin

  while Password <> 'delphi' do
  begin
    Writeln('Podaj hasło...');
    Readln(Password);
  end;

  Writeln('Hasło poprawne!');
  Readln;
end.

Omówię pokrótce działanie tego programu. Na samym początku umieszczamy warunek:

while Password <> 'delphi' do

Za jego pomocą sprawdzamy, czy zmienna @@Password@@ jest różna od delphi — jeżeli tak, następuje wykonanie instrukcji pętli znajdującej się w bloku begin..end. Jeżeli wpisane hasło jest niepoprawne, wykonywana zostaje kolejna iteracja. W przeciwnym przypadku pętla kończy swoje działanie.

Pętla repeat..until

Efekt zastosowania pętli repeat jest bardzo podobny do działania pętli while — pętla ta także może być wykonywana ogromną liczbę razy. Jedyna różnica polega na tym, że w pętli repeat warunek zakończenia sprawdzany jest dopiero po wykonaniu instrukcji. Oznacza to, że pętla repeat zawsze będzie wykonana co najmniej raz. Dopiero po tej iteracji program sprawdzi, czy można zakończyć działanie pętli. W przypadku pętli while warunek jest sprawdzany bezpośrednio przed jej wykonaniem, co w rezultacie może spowodować, że taka pętla nigdy nie zostanie wykonana.

Budowa pętli repeat jest następująca:

repeat
  { instrukcje do wykonania }
until { warunek zakończenia }

Pętla repeat, analogiczna do pętli while pokazanej w poprzednim przykładzie, wyglądałaby następująco:

  repeat
    Writeln('Podaj hasło...');
    Readln(Password);
  until Password = 'delphi';

Rezultat działania byłby identyczny.

Z pętlami wiąże się niebezpieczeństwo „zapętlenia”. Należy uważać, aby nie dopuścić do wykonywania przez program tych samych czynności w kółko — może do tego dojść, jeżeli warunek zakończenia nigdy nie zostanie spełniony.

Pętla for-in

Nowością w Delphi 2005 jest nowa wersja pętli for, która umożliwia operowanie na zbiorach, tablicach czy ciągach znakowych. Odpowiednikiem pętli for-in w innych językach programowania jest pętla foreach (np. w PHP). Przykładowo, chcielibyśmy przeanalizować, przeszukać każdy element tablicy. Możemy w tym celu skorzystać ze zwykłej pętli for:

program Loop_app;

{$APPTYPE CONSOLE}

uses SysUtils;

var
  I : Integer;

const
  A : array[1..10] of String = ('Jeden', 'Dwa', 'Trzy', 'Cztery',
  'Pięć', 'Sześć', 'Siedem', 'Osiem', 'Dziewięć', 'Dziesięć');

begin
  for I := Low(A) to High(A) do
  begin
     Writeln(IntToStr(i) + ': ' + A[i]);
  end;

  Readln;
end.

W programie użyłem wcześniej omówionych konstrukcji, czyli pętli for oraz tablicy deklarowanej jako stała. Zobaczmy teraz, w jaki sposób można napisać tak samo działający program z użyciem składni for-in:

program Loop_app;

{$APPTYPE CONSOLE}

uses SysUtils;

var
  S : String;

const
  A : array[1..10] of String = ('Jeden', 'Dwa', 'Trzy', 'Cztery',
  'Pięć', 'Sześć', 'Siedem', 'Osiem', 'Dziewięć', 'Dziesięć');

begin
  for S in A do
  begin
    Writeln(S);
  end;

  Readln;
end.

Składnia for-in posiada prostą konstrukcję. Najpierw podaje się nazwę zmiennej, do której zostanie przypisany aktualny element zbioru lub tablicy. Po słowie kluczowym in należy umieścić nazwę tablicy/zbioru. Wydaje mi się że taka konstrukcja jest czytelniejsza.

Przy użyciu pętli for mamy możliwość odczytania wartości zmiennej @@I@@, która określa numer iteracji. Takie rozwiązanie możemy także wprowadzić w naszym przykładzie, deklarując dodatkową zmienną @@I@@:

begin
  I := 1;

  for S in A do
  begin
    Writeln(IntToStr(i) + ': ' + S);

    Inc(i);
  end;

  Readln;
end.

Konstrukcja for-in w połączeniu z łańcuchami

Konstrukcja for-in może być wykorzystywana wraz z tablicami (co zaprezentowałem w poprzednich przykładach) oraz zbiorach czy ciągach znakowych String. Do każdego znaku ciągu można odwołać się podając numer indeksu w nawiasie kwadratowym, podobnie jak odwołujemy się do elementu tablicy. Czyli np. do pierwszego znaku ciągu można odwołać się w ten sposób:

  S := 'Delphi';
  Writeln(S[1]); // wyświetlona litera: D

Operacji na poszczególnych znakach ciągu @@S@@ można dokonać za pomocą konstrukcji for-in:

program Loop_app;

{$APPTYPE CONSOLE}

uses SysUtils;

var
  S : String;
  C : Char;

begin
  Write('Wpisz tekst: ');
  Readln(S);

  Writeln('---');

  for C in S do
  begin
    Writeln(C);
    Sleep(1000);
  end;
end.

Ten prosty program w pętli wyświetla każdy znak ciągu znakowego @@S@@, jeden pod drugim w odstępach sekundowych.

Polecenie Continue

Polecenie Continue może być używane tylko wraz z pętlami. Powoduje ono przejście do następnego wywołania pętli bez wykonywania dalszych instrukcji.

Oczywiście najlepiej istotę działania instrukcji Continue poznamy na przykładzie. Załóżmy, że mamy pętlę for, która zostanie wykonana 10 razy. Za każdym razem program losuje liczbę z przedziału od 1 do 3 i na podstawie tej wylosowanej liczby wyświetla jakiś tekst. Dzięki poleceniu Continue można sprawić, aby w przypadku, gdy wylosowaną liczbą będzie 1, ominąć wyświetlenie tekstu i przejść do następnej iteracji (listing 3.19).

Listing 3.19. Zastosowanie instrukcji Continue w pętli for

program LoopContinue;

{$APPTYPE CONSOLE}

var
  I, Number : Integer; // deklaracja zmiennej

begin
  Randomize;

  for I := 1 to 10 do
  begin

    Number := Random(3)+1;
    if Number = 1 then Continue;

    case Number of
      1: Writeln('Uuu, wylosowałeś 1');
      2: Writeln('No, dwa... jeszcze może być');
      3: Writeln('Dobrze');
    end;
  end;
  
  Readln;
end.

Interesujący nas warunek znajduje się w następującym miejscu: if Number = 1 then Continue;. Kompilator odczytuje to tak: jeżeli zmienna @@Number@@ zawiera wartość 1, pomiń wykonywanie dalszych instrukcji i przejdź od razu do kolejnej iteracji.

Polecenie Break

Polecenie Break również może być wykonywane tylko w połączeniu z pętlami. W odróżnieniu od procedury Continue, Polecenie Break umożliwia zakończenie działania pętli (opuszczenie jej). Po napotkaniu instrukcji Break dalsze wykonywanie pętli zostaje wstrzymane, a program przechodzi do poleceń znajdujących się za pętlą.

Dzięki poleceniu Break czekanie na planowe zakończenie pętli nie wydaje się konieczne — zawsze można zrobić to samemu w dowolnym momencie.

Zmodyfikujmy ostatni przykład, zastępując procedurę Continue poleceniem Break:

program LoopBreak;

{$APPTYPE CONSOLE}

var
  I, Number : Integer; // deklaracja zmiennej

begin
  Randomize;


  for I := 1 to 10 do
  begin

    Number := Random(3)+1;
    if Number = 1 then
    begin
      Writeln('Wylosowano 1 – opuszczamy pętle...');
      Break;
    end;

    case Number of
      1: Writeln('Uuu, wylosowałeś 1');
      2: Writeln('No, dwa... jeszcze może być');
      3: Writeln('Dobrze');
    end;
  end;

  Readln;
end.

Jeżeli program wylosuje cyfrę 1, wyświetli stosowną informację i zakończy działanie pętli. Żaden kod umieszczony poniżej instrukcji Break nie zostanie wykonany.

Zbiory

Zbiory są kolekcją danych tego samego typu. To zdanie zapewne niezbyt dobrze wyjaśnia funkcję zbiorów, spójrzmy więc na poniższy kod:

type
TCar = (tcFiat, tcSkoda, tcOpel, tcFerrari, tcPorshe, tcPeugeot);
TCarSet = set of TCar;

W drugim wierszu znajduje się deklaracja nowego typu danych — TCar. Zmienna korzystająca z tego typu może zawierać jedną z wartości podanych w nawiasie. Natomiast typ TCarSet jest zbiorem danych TCar. Nowy zbiór deklaruje się za pomocą konstrukcji set of. Jak już mówiłem, zbiory są konstrukcją, w której mogą się znaleźć elementy określonych danych. Znaczy to, że zmienna korzystająca z typu TCarSet może zawierać wszystkie elementy lub tylko kilka spośród nich. Przy wykorzystaniu zwykłych zmiennych nie jest to możliwe, gdyż do takiej zmiennej można przypisać tylko jeden element TCar.
Możliwa jest także deklaracja bezpośrednia, czyli deklaracja bez tworzenia dodatkowego typu TCar:

type
  TCarSet = set of (tcFiat, tcSkoda, tcOpel, tcFerrari, tcPorshe, tcPeugeot);

Zbiory mogą być również zbiorami liczbowymi lub zawierającymi pojedyncze znaki:

program Sets;

{$APPTYPE CONSOLE}

type
  TCarSet = set of (tcFiat, tcSkoda, tcOpel, tcFerrari, tcPorshe, tcPeugeot);
  TNumberSet = set of 0..9;
  TCharSet = set of 'A'..'Z';

Przypisywanie elementów zbioru

Aby przypisać elementy zbioru do danej zmiennej, trzeba skorzystać z nawiasu kwadratowego.

var
  CarSet : TCarSet;
begin
  CarSet := [tcSkoda, tcOpel];

Powyższy przykład powoduje przypisanie do zbioru dwóch elementów — tcSkoda i tcOpel. Możliwe jest oczywiście utworzenie kilku zmiennych korzystających z danego zbioru:

var
  Tanie,
  Średnie,
  Drogie : TCarSet;

begin
  Tanie := [];
  Średnie := [tcFiat, tcSkoda, tcOpel, tcPeugeot];
  Drogie := [tcPorshe, tcFerrari];
end.

Do zmiennej @@Tanie@@ przypisujemy zbiór pusty — nie zawiera on elementów, symbolizują go więc jedynie dwa nawiasy.

Odczytywanie elementów ze zbioru

Wraz ze zbiorami często jest używany operator in. Służy on do sprawdzania, czy dany element należy do określonego zbioru, przykładowo:

if (tcFiat in Cars) then { czynności }

Zwróćmy uwagę na konstrukcję. Na początku należy wpisać nazwę elementu, a dopiero później zmienną wskazującą na zbiór. Jeżeli dany element należy do zbioru, zostanie wykonany kod znajdujący się po słowie then.

Zaprzeczanie

Można by zapytać: „w jaki sposób sprawdza się, czy dany element nie należy do zbioru?”. W takim przypadku nie możemy zamiast operatora in wpisać out, ale możliwe jest zastosowanie operatora not, który jest zaprzeczeniem (o operatorach pisałem wcześniej).

if not (tcFiat in Cars) then { czynności }

Jeżeli element tcFiat nie należy do zbioru Cars, warunek zostanie spełniony.

Przekazywanie zbioru jako parametru procedury

Często podczas programowania w Delphi spotykamy się z konstrukcją, która wymaga przekazania zbioru jako parametru procedury. Jeżeli więc podczas kompilacji zostanie wyświetlony komunikat o błędzie: [Error] Sets.dpr(20): Incompatible types: 'TCarSet' and 'Enumeration', oznacza to, że podany parametr jest zbiorem, czyli musi być przekazany w nawiasach kwadratowych. Przykład takiego programu znajduje się na listingu 3.20.

Listing 3.20. Przykładowy program prezentujący zastosowanie zbiorów

program Sets;

{$APPTYPE CONSOLE}

type
  TCarSet = set of (tcFiat, tcSkoda, tcOpel, tcFerrari, tcPorsche, tcPeugeot);

  procedure CoKupujemy(Cars : TCarSet);
  begin
    if (tcFiat in Cars) then Writeln('Kupujemy Fiata!');
    if (tcSkoda in Cars) then Writeln('Kupujemy Skodę!');
    if (tcOpel in Cars) then Writeln('Kupujemy Opla!');
    if (tcFerrari in Cars) then Writeln('Kupujemy Ferrari!');
    if (tcPorsche in Cars) then Writeln('Kupujemy Porsche!');
    if (tcPeugeot in Cars) then Writeln('Kupujemy Peugeota!');
  end;

begin

  CoKupujemy([tcPorsche, tcFerrari]);
  Readln;
end.

Można uruchomić taki program i sprawdzić jego działanie. Łatwo sprawdzić, że w konsoli zostaną wyświetlone dwie linie:

Kupujemy Porsche!
Kupujemy Ferrari!

Dodawanie i odejmowanie elementów zbioru

W celu dodania do zbioru lub odjęcia od niego jakiegoś elementu można skorzystać z operatorów + i –. Trzeba to jednak zapisać w specyficzny sposób:

CarSet := CarSet + [tcFiat];

Za pomocą tego polecenia dodajemy do zbioru element tcFiat. Taka konstrukcja jest wymagana, gdyż gdybyśmy napisali tak:

CarSet := [tcFiat];

spowodowałoby to usunięcie elementów poprzednio znajdujących się w zbiorze i dodanie jedynie tcFiat.

Include i Exclude

Zalecaną metodą dodawania oraz odejmowania elementów zbioru są funkcje Include oraz Exclude. Pierwsza z nich włącza element do zbioru, a druga — odejmuje. Ich stosowanie jest zalecane ze względu na to, że działają o wiele szybciej niż operacje z zastosowaniem znaków + i –. Przykład wykorzystania:

Include(CarSet, tcFiat); // dodawanie
Exclude(CarSet, tcPorshe); // odejmowanie

Typy wariantowe

Języki takie jak PHP czy Perl mają pewną specyficzną funkcję — brak typów danych. Znaczy to, że jest możliwe określenie typu zmiennej, ale przeważnie to kompilator na podstawie wartości przypisanej do zmiennej sam określi, jaki to typ.

Typy wariantowe pojawiły się po raz pierwszy w języku Clipper i do teraz są obecne także w Delphi. Są bardzo wygodne, gdyż do zmiennej typu Variant możesz przypisać i tekst, i liczby — każdy rodzaj danych.

Aby móc korzystać ze zmiennych typu Variant, w sekcji uses trzeba dodać moduł: Variants.

Nowością w Delphi 8 była przestrzeń nazw (ang. namespace), która wiąże się ze specyficznymi nazwami modułów oddzielonymi kropkami. Owa zdolność wiąże się ze środowiskiem .NET, ale o tym będziemy jeszcze mówić w dalszej części książki.

Od chwili dołączenia modułu Variants do sekcji uses możemy korzystać z typu Variant — deklarować zmienne tego typu:

var
  V: Variant;

Następnie możesz przypisywać do tej zmiennej dane różnego typu:

uses Variants;

var
  V: Variant;
begin
  V := 10;
  V := 'Adam';
  V := TRUE;
end.

Powyższy kod zostanie zaakceptowany przez kompilator jako prawidłowy.
Z typami wariantowymi wiąże się kilka funkcji — niektóre z nich pokrótce omówię.

VarType, VarTypeAsText

function VarType(const AValue: Variant): Integer;

Funkcja zwraca typ zmiennej na podstawie parametru @@AValue@@. Oto przykład:

  V := True;
  if VarType(V) = varBoolean then
    Writeln('Typ Boolean');

W tym przypadku wartość będzie zawsze spełniona, ponieważ najpierw przypisujemy do zmiennej @@V@@ wartość True (co już jest informacją dla kompilatora, że od teraz zmienna @@V@@ jest typu Boolean). Następnie, jeżeli funkcja VarType zwróci wartość varBoolean, oznacza to, że jest to zmienna boolowska.

Zwracane zmienne mogą mieć wartości: varBoolean, varInteger, varDouble, varString itd. W takim przypadku lepiej zastosować funkcję VarTypeAsText, która zwraca typ zmiennej w postaci tekstowej:

  V := True;
  Writeln(VarTypeAsText(V))

;

Rezultatem wykonania takiego programu będzie wyświetlenie na konsoli tekstu: Boolean.

VarToStr

function VarToStr(const AValue: Variant): string;

Funkcja konwertuje wartość podaną w nawiasie na tekst — zmienną String. Oto przykład:

program VarTypes;

{$APPTYPE CONSOLE}

uses Variants;

var
  V: Variant;
begin
  V := False;
  Writeln(
    VarToStr(V)
  );

  Readln;
end.

Po uruchomieniu takiego programu na ekranie zostanie wyświetlony napis False.

VarIs*

W module Variants są zadeklarowane funkcję, które sprawdzają dany typ zmiennej, np. VarIsStr:

function VarIsStr(const AValue: Variant): Boolean;

Jeżeli parametr podany w nawiasie jest ciągiem znakowym, funkcja zwróci wartość True. Istnieje szereg funkcji tego typu: VarIsStr, VarIsNumeric, VarIsEmpty (czy zmienna jest pusta), VarIsFloat (wartość zmiennoprzecinkowa), VarIsArray (czy zmienna jest tablicą).

Pliki dołączane

Idea plików dołączanych jest bardzo prosta — polega na włączeniu w odpowiednim miejscu modułu pliku tekstowego, który jest traktowany jak integralna część tego modułu. Pewnie Czytelnikowi nasunie się w tym momencie porównanie z modułami — nie jest to jednak identyczny mechanizm.

Plik dołączany to nic innego jak zwykły plik tekstowy. Z menu File wybierzmy New/Other. W oknie dialogowym zaznacza się pozycję Other Files, a następnie klika ikonę Text. W edytorze kodu pojawi się nowa zakładka — zapiszmy ten plik pod nazwą SHOW.INC, ale uprzednio trzeba wpisać w edytorze kodu prostą instrukcję:

Writeln('Hello World');

Plik główny DPR powinien wyglądać tak jak na listingu 3.21.

Listing 3.21. Program demonstrujący zastosowanie plików dołączanych

program IncludeInc;

{$APPTYPE CONSOLE}

begin
  {$I SHOW.INC}
  Readln;
end.

Dzięki dyrektywie {$I} można włączyć plik do programu. Będzie to równoważne wstawieniu w tym miejscu zawartości owego pliku SHOW.INC.

Etykiety

Obecnie stosowanie etykiet nie jest wskazane — korzystanie z nich jest uważane za przestarzałe — teraz zaleca się używanie pętli z użyciem poleceń Continue i Break. Mimo to postanowiłem opisać w tym rozdziale etykiety, tak aby każdy wiedział, że coś takiego istnieje, i to nie tylko w języku Delphi.

Etykieta, mówiąc prosto, jest miejscem w kodzie (rodzaj zakładki), do którego można zawsze przejść (przeskoczyć). Najpierw jednak taką etykietę należy zadeklarować — np. tak jak zmienne, tyle że za pomocą słowa kluczowego label.

label
  Moja_Etykieta;

Dzięki takiej deklaracji kompilator otrzymuje informację, że ma do czynienia z etykietą. Przejście do takiej etykiety ogranicza się do wywołania słowa kluczowego goto Nazwa_Etykiety:

program Labels;

{$APPTYPE CONSOLE}

uses SysUtils;

label
  Moja_Etykieta;

var
  I : Integer;

begin
  Randomize;

  Moja_Etykieta: I := Random(10);

  Writeln('Wylosowałem ' + IntToStr(I));
  if I = 5 then goto Moja_Etykieta; // jeżeli komputer wylosuje 5 – ponów losowanie

  Readln;
end.

Samą „zakładkę” ustawia się, wpisując jej nazwę, a po dwukropku umieszcza się dalszą część kodu. Powyższy program realizuje losowanie liczby. Po wylosowaniu liczby 5 program przechodzi do ustawionej wcześniej etykiety.

Dyrektywy ostrzegawcze

Wiadomo już, w jaki sposób można tworzyć moduły, na czym polega programowanie strukturalne oraz proceduralne. Można powiedzieć, że Czytelnik zna ogólne podstawy języka Delphi. Należy jeszcze wspomnieć o dodatkowych dyrektywach ostrzegawczych, wprowadzonych w Delphi 8. Umożliwiają one opatrzenie odpowiednim słowem kluczowym procedury czy modułu, dzięki czemu kompilator podczas próby kompilacji wygeneruje ostrzeżenie.

Dyrektywy platform, deprecated i experimental przydają się szczególnie w trakcie prac zespołowych nad jednym projektem. Przykładowo, można oznaczyć moduł klauzulą experimental, dzięki czemu w oknie komunikatów błędu wyświetlony zostanie napis: [Warning] Project3.dpr(8): Unit 'Unit1' is experimental. Stąd będzie wiadomo, że dany moduł jest w fazie testów (eksperymentów).

Klauzulami ostrzegawczymi można oznaczać różne elementy języka Delphi: moduły, typy, procedury. Na przykład:

unit Unit1 experimental;

W takim przypadku moduł Unit1 jest oznaczony jako eksperymentalny. Proszę, zwróćmy uwagę na to, iż pomiędzy nazwą modułu a dyrektywą nie występuje średnik!

Ta zasada nie obowiązuje jednak w przypadku oznaczania procedur bądź funkcji:

program Example;

procedure SomeProc; deprecated;
begin

end;

begin
  SomeProc;
end.

Tutaj pomiędzy nazwą procedury a klauzulą należy umieścić średnik!

Dyrektywa deprecated oznacza, iż dany symbol (procedura, moduł) został uznany za niedoskonały (przestarzały) i został zachowany jedynie ze względów kompatybilności. Nie gwarantuje się wobec tego poprawnego działania tego elementu w przyszłych wersjach aplikacji. Należy stosować klauzulę deprecated w razie uznania, że dana procedura bądź typ danych są przestarzałe i należy je zastąpić innymi, nowszymi rozwiązaniami.

Klauzula platform jest ściśle związana z platformą .NET Framework. W Delphi wiele modułów, takich jak Windows czy Messages jest oznaczonych dyrektywą platform z tego względu, iż nie są one związane z .NET, a jedynie ściśle z Delphi. Nie są one więc wykorzystywane w innych platformach (urządzeniach), w których może być uruchamiany .NET, np. w palmtopach.

Nic nie stoi na przeszkodzie, aby jeden element oznaczyć naraz wieloma dyrektywami:

procedure SomeProc; deprecated; platform;
begin

end;

Jeżeli wyświetlanie komunikatów ostrzegawczych jest z jakichś względów niewskazane, można je wyłączyć, stosując specyficzny symbol:

program Example;

{$WARNINGS OFF}

Dyrektywa kompilatora

{$WARNING OFF}

wyłącza wyświetlanie ostrzeżeń w programie.</dfn>

Wstęp do algorytmiki

Każdy na pewno nieraz słyszał słowo algorytm. Jak podaje definicja, algorytm jest to ciąg instrukcji stworzony w celu rozwiązania jakiegoś problemu. Przykładowym problemem może być obliczenie silni z liczby N. Do nas należy rozwiązanie tego problemu, inaczej mówiąc — napisanie programu, który obliczy silnię liczby N.

O algorytmach wspominam dlatego, że zapewne nieraz spotkamy się z potrzebą pisaniem własnych algorytmów, czy to w szkole, czy w pracy.

Schematy blokowe

Nadal wielu programistów korzysta z tzw. schematów blokowych przed przystąpieniem do właściwego kodowania. Również w szkołach, gdzie uczy się programowania, schemat blokowy stanowi wprowadzenie do pisania własnych algorytmów.

Schematy blokowe stanowią tzw. metajęzyk — służy on do obrazowego przedstawiania działania danego algorytmu za pomocą figur geometrycznych.

Wiadomo — każdy język programowania ma inną składnię. Schematy blokowe są zapisane tak, aby mógł je odczytać każdy i aby można było je zaimplementować w dowolnym języku.

Przykładowy schemat blokowy

Najprostszy przykładowy schemat blokowy został przedstawiony na rysunku 3.2.

3.2.jpg
Rysunek 3.2. Przykładowy schemat blokowy

Kierunek postępowania algorytmu jest zaznaczony strzałkami.

Początek i koniec algorytmu jest oznaczony elipsą — w środku musi znaleźć się tekst START bądź STOP (ewentualnie KONIEC). Równoległobok oznacza pobieranie danych od użytkownika lub wyświetlanie końcowego rezultatu działania programu.
Jak można się domyślić, algorytm ten nie wykonuje żadnego działania poza wczytaniem danych i późniejszym wyświetleniem ich na ekranie.

Jak tworzyć schematy blokowe?

Schematy blokowe najwygodniej rysować na kartce. Rysowanie ich za pomocą programów komputerowych to duża udręka.

Jeżeli jednak będzie konieczne utworzenie schematu blokowego w formie elektronicznej, można użyć do tego popularnych programów graficznych typu Paint Shop Pro lub Adobe Photoshop. Istnieje także możliwość rysowania schematów w programie MS Word (służą do tego opcje znajdujące się na pasku Rysowanie).

Oznaczenia figur

Schematy blokowe mają do to siebie, że poszczególne figury geometryczne przyjmują umowne znaczenie. Pasek narzędzi w programie Microsoft Word zawiera pozycję Autokształty, w której znajduje się menu Schemat blokowy. Owe menu obejmuje cały spis figur (wraz z opisami), jakie używane są podczas projektowania schematów blokowych. Ja omówię tylko kilka z nich.

Przy większych i skomplikowanych algorytmach rysowanie schematu blokowego staje się praktycznie niemożliwe ze względu na dużą liczbę figur i ogólny rozmiar obrazka.

Elipsa

Początek i koniec algorytmu jest oznaczany poprzez elipsę, a często także poprzez prostokąt z zaokrąglonymi rogami. Początek algorytmu określa elipsa z napisem START, a koniec — z napisem STOP.

Często popełnianym błędem jest umieszczenie kilku elips z napisem STOP. Jest to błąd, ponieważ zakończenie programu jest tylko jedno (w programie występuje tylko jedno end z kropką na końcu).

Równoległobok

Równoległobok symbolizuje pobieranie lub wypisywanie danych na ekranie (tak jak to zostało przedstawione na rysunku 3.2).

Prostokąt

Ta figura symbolizuje wszelkie podstawiania — np. podstawienie do zmiennej jakiejś wartości. Do tej figury zawsze prowadzi jedna strzałka wchodząca oraz jedna strzałka wychodzącą.

Romb

Figurę rombu można przyrównać do instrukcji if, czyli jest to blok decyzyjny. We wnętrzu tej figury należy umieszczać wszelkie instrukcje porównania. Od rombu zawsze odchodzi jedna strzałka w lewo (gdy warunek nie zostanie spełniony — FALSE) oraz jedna strzałka w prawo (gdy warunek zostanie spełniony — TRUE).

Przykład — obliczanie silni

Jako przykład zaprezentuję algorytm, który będzie liczył silnię liczby N.

Silnia to iloczyn wszystkich dodatnich liczb naturalnych nie większych niż n. Przykładowo: 4! = 4 x 3 x 2 x 1 = 24. Ponadto silnia liczby 0 to 1.

Schemat blokowy

Na rysunku 3.3 został przedstawiony schemat blokowy algorytmu.

3.3.jpg
Rysunek 3.3. Schemat algorytmu liczącego silnię

Omówię teraz cały algorytm. W programie należy zadeklarować trzy zmienne:

*@@N@@ — liczba podana przez użytkownika, np. 4,
*@@Counter@@ — licznik wykonań (iteracji) pętli,
*@@Result@@ — rezultat całej operacji (wynik).

Do zapisania tego algorytmu konieczne staje się użycie pętli, gdyż warunkiem zakończenia jest poprawność instrukcji Counter = N. Zapisałem ten algorytm z użyciem pętli while, ale równie dobrze można to zrobić za pomocą pętli repeat oraz for.
Nie należy także zapominać o pierwszej instrukcji warunkowej if, która sprawdza, czy podana przez użytkownika wartość nie jest liczbą 0. Cały kod gotowego programu został przedstawiony na listingu 3.22.

Listing 3.22. Kod programu obliczającego silnię

program P3_23;

{$APPTYPE CONSOLE}


var
  Counter, Result, N : Integer;


begin
  Writeln('Program oblicza silnię z liczby N');
  Writeln('Podaj liczbę');
  Readln(N); // pobieramy liczbę


  if (N = 0) then  // silnia z 0 = 1
  begin
    Result := 1;
  end
  else
  begin
     Counter := 0;  // zmienna licznik
     Result := 1; // rezultat operacji

     while (Counter < N) do
     begin
       Inc(Counter); // za każdym razem zwiększaj licznik
       Result := Result * Counter; // mnóż wartości
     end;
  end;

  Writeln('Wynik: ');
  Writeln(Result);

  Readln;
end.

Można skompilować i uruchomić powyższy program. Po wpisaniu jakiejś wartości, np. 4, program obliczy i wyświetli prawidłową wartość — w tym przypadku 24.

Naturalnie był to jedynie prosty przykład. Bardzo prawdopodobne jest, że Czytelnik w trakcie swojej kariery programistycznej będzie rozwiązywał o wiele trudniejsze zadania — np. sortowania czy kompresji danych.

Efektywny kod

Początkujący programiści często umieszczają w swoich kodach wiele niepotrzebnych instrukcji. Dzięki temu kod staje się mniej przejrzysty, bardziej zagmatwany i przede wszystkim — wykonuje się wolniej. Procesor musi wówczas przetworzyć więcej instrukcji do wykonania tego samego zadania. Należy się w tym momencie zastanowić, czy nie dałoby się danego problemu rozwiązać prościej? Często takie pułapki wynikają po prostu z niewiedzy początkujących programistów. Pragnę przedstawić parę przykładów pokazujących, jak można uprościć niektóre zapisy kodu Delphi.

Instrukcje warunkowe

Częstym niedociągnięciem (nie można nazwać tego błędem) jest sprawdzanie w warunku if, czy zmienna ma wartość True:

if X = True then { kod } 

Taki zapis nie jest konieczny, gdyż domyślnie warunek w tym przypadku sprawdza, czy zmienna nie ma wartości True. Można więc taki kod zastąpić następującym:

if X then { kod }

Zwiększana jest tym samym czytelność kodu.

Spójrzmy na poniższy kod:

if X = True then
  Y := True
else
  Y := False;

Czytelnikowi taki fragment kodu może wydać się śmieszny, ale niestety wielu początkujących programistów stosuje taki niepotrzebny zapis, który spowalnia wykonywanie programu. Procesor musi bowiem wykonać instrukcję porównującą zawartość danej zmiennej i dopiero później przypisać nową wartość do zmiennej. Taki zapis, choć z punktu widzenia kompilatora jest poprawny, może zostać uproszczony do takiej postaci:

Y := X;

Sytuacja odwrotna:

if X = True then
  Y := False
else
  Y := True;

Taki z kolei zapis można skrócić do jednej linii kodu, używając operatora not:

Y := not X;

W obu przypadkach zmienne muszą być oczywiście typu Boolean.

Wyobraźmy sobie program, w którym użytkownik musi podać hasło dostępu (wpisać je do kontrolki Edit) i nacisnąć przycisk Button, aby się zalogować. Chcemy jednak, aby przycisk pozostał nieaktywny (właściwość Enabled), dopóki użytkownik nie wpisze w kontrolce choćby jednego znaku. W tym rozdziale nie będę jeszcze mówił o zdarzeniach i projektowaniu wizualnym (o tym w kolejnym rozdziale), ale zaprezentuję przykładowy kod z użyciem instrukcji if:

var
  Imię : String;
  Zaakceptowane : Boolean;

begin
  Zaakceptowane := False;
  Writeln('Podaj imię');
  Readln(Imię);  // odczyt wartości

  if Length(Imię) = 0 then // sprawdzenie długości ciągu
    Zaakceptowane := False
  else
    Zaakceptowane := True;


end.

Funkcja Length w tym przykładzie sprawdza, czy rzeczywiście długość wprowadzonego tekstu równa się 0. Taki kod można skrócić do takiej postaci:

var
  Imię : String;
  Zaakceptowane : Boolean;

begin
  Zaakceptowane := False;
  Writeln('Podaj imię');
  Readln(Imię);  // odczyt wartości

  Zaakceptowane := ( Length(Imię) <> 0 );
end.

Taki zapis może wydawać się niezrozumiały. Warto jednak wiedzieć, że Delphi daje takie możliwości. Zmienna @@Zaakceptowane@@ zmieni wartość na True, jeśli długość zmiennej Imię będzie różna od 0. W ten sposób osiągamy taki sam rezultat jak w poprzednim przykładzie.

Typ Boolean w tablicach

Według wielu opinii programowanie jest sztuką, umiejętnym tworzeniem kodu. W Delphi co prawda nie mamy tylu możliwości prezentowania „sztuczek” co w języku C, lecz warto skorzystać z paru ułatwień. Oto przykład kodu nawiązującego do poprzedniego listingu:

program Project3;

{$APPTYPE CONSOLE}

var
  Imię : String;
  Zaakceptowane : Boolean;

begin
  Zaakceptowane := False;
  Writeln('Podaj imię');
  Readln(Imię);  // odczyt wartości

  if Length(Imię) = 0 then
  begin // sprawdzenie długości ciągu
    Zaakceptowane := False;
    Writeln('Imię jest za krótkie!');
  end else
  begin
    Zaakceptowane := True;
    Writeln('Imię ma odpowiednią długość!');
  end;

  Readln;
end.

Oprócz zmiany wartości zmiennej @@Zaakceptowane@@ na ekranie konsoli jest wyświetlany tekst informacyjny. Korzystając z tablic, można skrócić ten kod do kilku linijek. Przede wszystkim należy zadeklarować tablicę dwuelementową:

const
  Wiadomość : array[ Boolean ] of String = ('Imię jest za krótkie!', 'Imię ma odpowiednią długość!');

Konstrukcja tej tablicy jest specyficzna, bo zamiast zakresu podałem typ Boolean. Oznacza to, że tablica może mieć dwa elementy, True lub False, tak więc odwołanie się do danego elementu wygląda następująco:

Writeln(Wiadomość[True]);

Łatwo już się domyślić, jak będzie wyglądał zapis naszego przykładu w zmienionej formie:

program Project3;

{$APPTYPE CONSOLE}

var
  Imię : String;
  Zaakceptowane : Boolean;

const
  Wiadomość : array[ Boolean ] of String = ('Imię jest za krótkie!', 'Imię ma odpowiednią długość!');

begin
  Zaakceptowane := False;
  Writeln('Podaj imię');
  Readln(Imię);  // odczyt wartości

  Zaakceptowane := ( Length(Imię) <> 0 );
  Writeln(Wiadomość[Zaakceptowane]);

  Readln;
end.

Dzięki temu skróciliśmy zapis kodu, zwiększyliśmy jego czytelność oraz przyspieszyliśmy działanie programu.

Zbiory

Zastosowanie zbiorów często upraszcza zapis związany z instrukcjami warunkowymi. Przykładowo, trzeba sprawdzić, czy wartość zawarta w zmiennej jest liczbą czy ciągiem znakowym. Można także sprawdzić, czy dany ciąg zawiera określone znaki:

  if ((Znak = 'A') or (Znak = 'a') or (Znak = 'B')) then
  { kod }

Jeżeli chcemy porównać wiele znaków, to taki zapis jest mało profesjonalny. Właśnie dzięki zbiorom oraz operatorowi in można go zastąpić w ten sposób:

  if Znak in ['A', 'a', 'b', 'B'] then { kod }

Niestety, z użyciem zbiorów i operatora in nie można porównywać całych ciągów, lecz tylko pojedyncze znaki. I tak jednak daje to pewne ułatwienie w porównaniu z zastosowaniem operatora and.

Innym prostym sposobem na sprawdzenie, czy podany znak jest alfanumeryczny, jest zastosowanie poniższego kodu:

  if Key in ['a'..'z', 'A'..'Z', '0'..'9'] then
   { kod }

Warto też pamiętać o możliwości stosowania zakresów w przypadku zbiorów! W takim przypadku następuje sprawdzenie, czy zmienna @@Key@@ należy do przedziału znaków od a do z lub od A do Z, a także, czy zmienna @@Key@@ zawiera liczbę.

Łączenie znaków w ciągach

Może nie jest to zbyt pomocna wskazówka, ale pozwala na zaoszczędzenie miejsca w kodzie źródłowym poprzez usunięcie operatora +. Chodzi o łączenie danych w ciągach znaków, a konkretniej — kodów ASCII poprzedzanych znakiem #. Poniższy kod:

  MessageBox(0, 'To jest ciąg znakowy... ' + #13 + #13 + '...druga linia', '', MB_OK);

Może być równie dobrze zastąpiony następującym:

  MessageBox(0, 'To jest ciąg znakowy... ' + #13#13 + '...druga linia', '', MB_OK);

Oba zapisy mają takie same działanie — tworzenie podwójnej linii w oknie informacyjnym.

Test

  1. Czy w kodzie źródłowym można używać polskich znaków?
    a) Tak,
    b) Nie,
    c) Tylko w stosunku do nazw zmiennych.
  2. Który z poniższych fragmentów kodu jest prawidłowy:
    a) if (X = 45) and (Y = 546) then
    b) if X = 45 and Y = 546 then
    c) if (X = 45 and Y = 546) then
  3. Czy operator not może być łączony z innymi operatorami logicznymi?
    a) Nie,
    b) Tak,
    c) Tylko w przypadku zbiorów danych.
  4. Czy blok else jest obowiązkowym elementem instrukcji warunkowej if?
    a) Tak,
    b) Tak, pod warunkiem gdy mamy kilka warunków do sprawdzenia,
    c) Nie.
  5. Zmienna jakiego typu umożliwia przechowywanie liczb zmiennoprzecinkowych:
    a) LongWord,
    b) Currency,
    c) Int64.
  6. Jaka dyrektywa określa przeciążenie procedur i funkcji?
    a) override,
    b) forward,
    c) overload.
  7. Czy sekcja initialization jest obowiązkowym elementem modułu?
    a) Nie,
    b) Tak,
    c) Nie, tylko pod warunkiem, że zadeklarowana została sekcja finalization.
  8. Jakiego rodzaju dane może przechowywać typ PChar?
    a) Liczby rzeczywiste,
    b) Ciągi znakowe,
    c) Ciągi znakowe, ale z ograniczeniem do 255 znaków.
  9. Continue i Break to:
    a) Polecenia języka programowania,
    b) Funkcje zadeklarowane w module System.pas,
    c) Operatory.
  10. Typy wariantowe (Variant):
    a) Mogą być używane po dodaniu do sekcji uses modułu Variants,
    b) Mogą zostać użyte w każdym miejscu programu,
    c) Nie ma takiego typu.

FAQ

Jak poznać długość tekstu (zmiennej String)?

Umożliwia to funkcja Length, która podaje długość zmiennej w znakach:

var
  S : String;
  L : Integer;
begin
  S := 'Delphi';
  L := Length(S); // długość tekstu (6 znaków) 
end.

**Jak głęboko można zagnieżdżać instrukcję if? **

Nie ma ograniczenia, lecz ze względu na zwiększenie czytelności, zbyt skomplikowane instrukcje należy zastąpić instrukcją case.

Czy w programie może być zadeklarowanych kilka zmiennych o tej samej nazwie?

Chociaż nie jest to zalecane i nie wspominałem o tym w tym rozdziale, to tak, jest to możliwe. Jedynym warunkiem jest umieszczenie tych zmiennych w różnym zakresu widzialności. Przykładowo: jedna zmienna @@X@@ może być zmienną globalną, a druga — o tej samej nazwie — zmienną lokalną:

var
  X : Integer;

procedure Foo; 
var
  X : Integer;
begin

end; 

**Czy instrukcja case musi zawierać słowo else? **

Nie, fraza else przy instrukcji case jest opcjonalna.

Moja pętla blokuje program. Co robię źle?

Pętla, która blokuje program, jest nazywana pętlą forever, gdyż warunek jej zakończenia nie jest nigdy spełniany. Przyczyna blokowania programu przez pętlę leży w jej złym zaprogramowaniu. W takim przypadku warunek jej zakończenia nie jest spełniany, co sprowadza się do ciągłego jej działania, w wyniku czego pozostałe funkcje programu są zablokowane.

W jaki sposób przerwać działanie pętli?

Służą do tego polecenia: Break, które przerywa pętle, oraz Continue — powodujące przejście do następnej iteracji pętli.
Instrukcje te mogą być użyte jedynie w ciele pętli.

Jaka jest różnica pomiędzy pętlami repeat i while?

Pętla repeat jest zawsze wykonywana co najmniej raz. Pętla while przed wykonaniem sprawdza warunek zakończenia, co może spowodować, iż pętla ta nie zostanie wykonana ani razu.

Ile może być w programie funkcji i procedur?

Nie ma formalnego ograniczenia.

Jaka jest zaleta używania procedur przeciążanych?

Jedyną zaletą stosowania funkcji i procedur przeciążanych jest uproszczenie procesu programowania. Jedna funkcja może realizować kilka procesów, w zależności od podanych parametrów.

Podsumowanie

Po lekturze tego rozdziału Czytelnik powinieneś zyskać pewne rozeznanie odnośnie do języka Delphi. Opisałem jego podstawowe elementy, takie jak pętle, instrukcje warunkowe czy operatory. Jeżeli ktoś programował wcześniej w innym języku, przyswojenie tej wiedzy nie powinno sprawiać problemu, gdyż pewne elementy języka programowania, takie jak pętle czy operatory, są obecne w każdym języku.
W kolejnych rozdziałach nadal będziemy poznawać budowę języka Delphi, jednak w bardziej zaawansowanych aspektach. Skupimy się także na platformie .NET i konstrukcjach języka, ściśle związanych z tą platformą.

.. [#] Trochę skłamałem. Przeskok taki jest możliwy z użyciem triku związanego ze wskaźnikami (jest to sposób na „oszukanie” kompilatora).

[[Delphi/Vademecum|Spis treści]]

[[Delphi/Vademecum/Prawa autorskie|©]] Helion 2005. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy