W10 Procedury ========= Deklarowanie typów ------------------ Tak jak można w Pascalu nazywać (deklarować) stałe, tak samo można nazywać (deklarować) typy. Nazywanie typów daje dwie korzyści: - przyjemniej (i czytelniej) jest napisać: var w: wektor; zamiast var w: array[1..n] of integer; - przy deklarowaniu parametrów procedur i funkcji (o czym dalej) trzeba podawać nazwę typu parametru, a nie sam typ. Deklaracje typów zaczynamy słowem kluczowym type, po którym następuje ciąg deklaracji poszczególnych typów, każda postaci: = ; Oto przykład deklaracji kilku typów (wraz z już wspomnianym typem wektor): type wektor = array[1..n] of integer; cyfry = '0'..'9'; indeks = 0..100; Zadeklarowanych w ten sposób typów używamy tak samo jak typów poznanych poprzednio. Procedury i funkcje ------------------- Często pisząc program zauważamy, że kilkakrotnie występuje w nim taka sama (lub prawie taka sama) sekwencja instrukcji. Popatrzmy na przykład. Załóżmy, że chcemy napisać program wczytujący dwa wektory i wypisujący trzeci, będący ich sumą: {$R+,Q+} program tablice1; const n = 10; {Rozmiar wektora} type indeks = 1..n; wektor = array[indeks] of integer; {Typ wektorów} var i : indeks; a, b, c : wektor; {a i b wektory zadane, c wektor wynikowy} begin {Wczytanie pierwszego wektora} for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(a[i]); end; {Wczytanie drugiego wektora} for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(b[i]); end; {Obliczenia} for i:=1 to n do c[i] := a[i] + b[i]; {Wypisanie wyniku} for i:=1 to n do Writeln('Wynik[', i, '] = ', c[i]); end. Jak widać dwukrotnie powtarza się niemal identyczny fragment programu, służący do wczytania z wejścia wektora. Nie ma sensu pisać dwa (lub więcej) razy (prawie) tej samej rzeczy, bo: - napisanie czegoś dwa razy wymaga więcej czasu, - program staje się dłuższy, - jeśli będziemy chcieli coś poprawić (np. usunąć błąd), to będziemy musieli to robić w kilku miejscach, więc znów niepotrzebnie stracimy czas, - istnieje duże prawdopodobieństwo, że w kilku częściach programu coś poprawimy, a w jednej zapomnimy - tego typu błędy są szczególnie przykre, gdyż jesteśmy pewni, że poprawiliśmy program (bo poprawialiśmy), a program dalej nie działa tak, jak byśmy tego chcieli. Rozwiązaniem tego problemu są procedury, czyli nazwane fragmenty programu. Deklaracja procedury ma następującą postać (na razie): procedure ; begin end; Po zadeklarowaniu procedury można ją wielokrotnie wywoływać pisząc jej nazwę. Wywołanie procedury spowoduje wykonanie jedna po drugiej zawartych w niej instrukcji. Popatrzmy znów na przykład: {$R+,Q+} program tablice2; const n = 10; {Rozmiar wektora} type indeks = 1..n; wektor = array[indeks] of integer; {Typ wektorów} var x : wektor; i : indeks; a, b, c : wektor; {a i b wektory zadane, c wektor wynikowy} procedure wczytaj_wektor_na_x; begin for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(x[i]); end; end; begin {Wczytanie pierwszego wektora} wczytaj_wektor_na_x; a := x; {Wczytanie drugiego wektora} wczytaj_wektor_na_x; b := x; {Obliczenia} for i:=1 to n do c[i] := a[i] + b[i]; {Wypisanie wyniku} for i:=1 to n do Writeln('Wynik[', i, '] = ', c[i]); end. Co zyskaliśmy? Jeśli będziemy chcieli zmodyfikować wczytywanie wektora, to wystarczy to teraz zrobić tylko w jednym miejscu - w treści procedury. Jednak nie to jest najważniejszą korzyścią. To co jest najważniejsze, to zwiększenie czytelności programu: - po pierwsze, wyraźnie wskazaliśmy ten fragment programu, który wczytuje wektor, osoba czytająca program zyskuje dzięki temu dodatkowe informacje, - po drugie, jawnie zaznaczyliśmy, że wczytanie wektora a jest taką samą operacją jak wczytanie wektora b. Poprzednio żeby to zauważyć, trzeba było wnikliwie porównać oba fragmenty programu wczytujące wektory. Skoro uznaliśmy, że najważniejszą korzyścią jaką dają procedury jest zwiększenie czytelności programu, nie może dziwić, że procedury będziemy tworzyć nawet wtedy, gdy będą wywoływane tylko jeden raz. W naszym przykładzie możemy dodać dwie procedury, dodającą i wypisującą: {$R+,Q+} program tablice3; const n = 10; {Rozmiar wektora} type indeks = 1..n; wektor = array[indeks] of integer; {Typ wektorów} var x : wektor; i : indeks; a, b, c : wektor; {a i b wektory zadane, c wektor wynikowy} procedure wczytaj_wektor_na_x; begin for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(x[i]); end; end; procedure dodaj_a_do_b_i_zapisz_wynik_na_c; begin for i:=1 to n do c[i] := a[i] + b[i]; end; procedure wypisz_c; begin for i:=1 to n do Writeln('Wynik[', i, '] = ', c[i]); end; begin {Wczytanie pierwszego wektora} wczytaj_wektor_na_x; a := x; {Wczytanie drugiego wektora} wczytaj_wektor_na_x; b := x; {Obliczenia} dodaj_a_do_b_i_zapisz_wynik_na_c; {Wypisanie wyniku} wypisz_c; end. Zauważmy, że wprawdzie nasz program się wydłużył, ale stał się za to czytelniejszy. Parametry --------- Korzystanie z procedur można znacznie uprościć używając z parametrów. Parametry służą do komunikacji treści procedury ze środowiskiem, w którym ją wywołano. Mogą służyć zarówno do przekazywania informacji z procedury jak i do procedury. Z tego powodu w Pascalu mamy dwa rodzaje parametrów: - parametry przekazywane przez wartość (inf. przekazywane do procedury), - parametry przekazywane przez zmienną (inf. przekazywane z procedury i do procedury). W niektórych implementacjach Pasala (np. Free Pascal, Delphi), wprowadzono jeszcze dodatkowe tryby przekazywania parametrów, lecz (poza otwartymi tablicami) nie są one warte uwagi. Popatrzmy na procedury z naszego przykładu. We wczytaj_wektor_na_x chcemy przypisać na wektor dane wczytane z wejścia, chcemy więc by procedura przekazała jakieś informacje (tu: wektor) do środowiska w jakim ją wywołano. Zastosujemy tu przekazywanie przez zmienną. W wypisz_wektor_c jest inaczej, procedura pobiera pewną wartość ze środowiska, w którym ją wywołaliśmy (tu: wektor) i coś z nią robi (wypisuje). W tym przypadku zastosujemy przekazywanie przez wartość. Wreszcie procedura dodaj_a_do_b_i_zapisz_wynik_na_c pobiera ze środowiska wartości wektorów a i b, zaś jako wynik daje wektor c. Tu a i b przekażemy przez wartość, zaś c przez zmienną. Składnia: parametry procedury (wraz z nazwami ich typów) piszemy w nagłówku procedury (po słowie procedure, w nawiasach). Oddzielamy je od siebie średnikami, nazwę typu piszemy po dwukropku. Jeśli parametr przekazujemy przez wartość, to nic już nie musimy pisać. Jeśli przez zmienną, to piszemy jeszcze var przed nazwą parametru. Semantyka: argumenty przekazywane przez wartość są kopiowane na pomocnicze zmienne lokalne procedury, która działa na tych kopiach. Jeśli natomiast argument jest przekazywany przez zmienną, to procedura działa bezpośrednio na tym argumencie. Stosowanie: Jeśli celem działania procedury/funkcji jest zmiana wartości argumentu, to parametr trzeba przekazywać przez zmienną. W przeciwnym przypadku parametr przekazujemy przez wartość. Spójrzmy jeszcze raz na nasz przykład: {$R+,Q+} program tablice4; const n = 10; {Rozmiar wektora} type indeks = 1..n; wektor = array[indeks] of integer; {Typ wektorów} var a, b, c : wektor; {a i b wektory zadane, c wektor wynikowy} i : indeks; procedure wczytaj_wektor(var x: wektor); begin for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(x[i]); end; end; procedure dodaj(a,b: wektor; var c: wektor); begin for i:=1 to n do c[i] := a[i] + b[i]; end; procedure wypisz(x: wektor); begin for i:=1 to n do Writeln('Wynik[', i, '] = ', c[i]); end; begin {Wczytanie pierwszego wektora} wczytaj_wektor(a); {Wczytanie drugiego wektora} wczytaj_wektor(b); {Obliczenia} dodaj(a,b,c); {Wypisanie wyniku} wypisz(c); end. Uwaga, zapis: procedure dodaj(a,b: wektor; var c: wektor); jest skrótem zapisu: procedure dodaj(a: wektor; b: wektor; var c: wektor); Dodatkowe uwagi: - Kolejność parametrów na liście jest bardzo ważna: pierwszy parametr odpowiada pierwszemu argumentowi z wywołania, drugi drugiemu itd. (Czasem zamiast nazw parametr/argument używa się określeń parametr formalny/parametr aktualny). - Argumentem odpowiadającym parametrowi przekazywanemu przez wartość może być dowolne wyrażenie typu zgodnego z typem parametru. - Argumentem odpowiadającym parametrowi przekazywanemu przez zmienną może być tylko coś, co może wystąpić po lewej stronie instrukcji przypisania, i znów typ argumentu musi być zgodny z typem parametru. (Na potrzeby tego wykładu przyjmiemy, że zgodność typów oznacza, że typy muszą być identyczne.) Oto przykład: var i: integer; procedure Test1(var x: integer); begin x := 3; end; {Test1} begin Test1(i); {OK} Test1(7); {Źle, 7 nie może wystąpić po lewej stronie przypisania} end. I jeszcze jeden przykład: procedura zamieniająca wartości argumentów: procedure zamień_zła(x,y: integer); var pom: integer; begin pom := x; x := y; y := pom; end; {zamień_zła} Co trzeba w niej zmienić? Zmienne lokalne --------------- Wewnątrz procedur można deklarować zmienne lokalne, czyli takie, które są tworzone w momencie wywołania procedury i giną wraz z zakończeniem jej działania. Tak jak wszystkie zmienne w Pascalu mają one na początku nieokreśloną wartość (więc trzeba je inicjalizować). Zmienne lokalne deklarujemy dokładnie tak samo jak zmienne z programu głównego (nazywane zmiennymi globalnymi). Zmienne lokalne są wygodne, gdyż pozwalają ukryć wewnątrz procedur te zmienne, które są potrzebne jedynie lokalnie, na potrzeby danego wywołania procedury. W naszym przykładzie taką zmienną jest zmienna i: nie używamy jej nigdzie w programie głównym, a jedynie w procedurach. Oto uwzględniająca tę zmianę wersja naszego przykładu: {$R+,Q+} program tablice5; const n = 10; {Rozmiar wektora} type indeks = 1..n; wektor = array[indeks] of integer; {Typ wektorów} var a, b, c : wektor; {a i b wektory zadane, c wektor wynikowy} procedure wczytaj_wektor(var x: wektor); var i : indeks; begin for i:=1 to n do begin writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): '); readln(x[i]); end; end; procedure dodaj(a,b: wektor; var c: wektor); var i : indeks; begin for i:=1 to n do c[i] := a[i] + b[i]; end; procedure wypisz(x: wektor); var i : indeks; begin for i:=1 to n do Writeln('Wynik[', i, '] = ', x[i]); end; begin {Wczytanie pierwszego wektora} wczytaj_wektor(a); {Wczytanie drugiego wektora} wczytaj_wektor(b); {Obliczenia} dodaj(a,b,c); {Wypisanie wyniku} wypisz(c); end. Wprawdzie program się wydłużył, ale oczyściliśmy program główny ze zbędnej tam zmiennej i, rozpraszając ewentualne wątpliwości czytelnika tego programu co do roli tej zmiennej - teraz widać, że jej znaczenie jest tylko lokalne. Mając zmienne lokalne, możemy inaczej spojrzeć na parametry przekazywane przez wartość: są to po prostu zmienne lokalne, tyle że inicjalizowane (wartością argumentu). Deklaracje lokalne W Pascalu można deklarować: - stałe, - typy, - zmienne, - procedury i funkcje. (I nic innego!) Kolejność jest dowolna, o ile zawsze każda rzecz jest najpierw (tekstowo) zadeklarowana, a dopiero potem używana. Najsensowniejsza kolejność deklaracji jest taka jak podano powyżej (standard Pascala wymaga dokładnie takiej kolejności). W procedurze można deklarować wszystko to co i w programie głównym (a więc także można deklarować lokalne procedury). Zatem pełna składnia procedury jest następująca: procedure ( ); begin end; Przesłanianie nazw, zasięg lokalny i globalny ---------------------------------------------- Zwróćmy uwagę, że w procedurach wczytaj_wektor i wypisz_wektor użyto dla parametru nowej nazwy (x), zaś w procedurze dodaj użyto nazw zmiennych występujących w programie głównym. Jest to możliwe dlatego, że każda procedura wprowadza nowy zasięg widoczności. Jeśli występują w niej nazwy występujące także w jej otoczeniu, to nazwy lokalne przesłonią takie same nazwy z otoczenia. Oznacza to, że w procedurze wczytaj_wektor można używać zmiennej globalnej a, ale w procedurze dodaj już nie (bo przesłoniła ją nazwa parametru a). Podsumowując: - z programem głównym i z każdą procedurą związany jest zasięg widoczności nazw, - w jednym zasięgu nie może być dwu rzeczy o takiej samej nazwie, - w różnych zasięgach mogą być różne rzeczy o takich samych nazwach, - jeśli jeden zasięg jest zanurzony w innym, to w nim jego nazwy przesłaniają takie same nazwy z otaczającego zasięgu widoczności. Oto przykłady: 1. const x = 5; var x: Integer; {Źle! x jest już zadeklarowane w tym zasięgu.} 2. const x = 5; procedure Proc(x: real); {Poprawne, to x jest w innym zasięgu} {...} 3. procedure Proc(x: real); var x: real; {Źle! x już jest w tym zasięgu!} Funkcje ------- Funkcje są specjalną formą procedur. Często piszemy procedurę, która ma obliczyć jedną wartość, np. procedure kwadrat(x: integer; var wynik: integer); begin wynik := x * x; end; {kwadrat} Użycie takiej procedury jest czasem niewygodne: var i, pom: integer: {...} begin {...} kwadrat(i, pom); Writeln('Kwadratem ', i, ' jest ', pom); End; Możemy w takiej sytuacji użyć funkcji: function kwadrat(x: integer): integer; begin kwadrat := x * x; end; {kwadrat} której się dużo wygodniej używa: var i: integer: {...} begin {...} Writeln('Kwadratem ', i, ' jest ', kwadrat(i)); end. Składnia deklaracji funkcji: function ( ): ; begin end; Dodatkowe zasady deklarowania funkcji: - wewnątrz funkcji, przy każdym jej wywołaniu, musi nastąpić co najmniej jedno przypisanie na jej nazwę, - nie można odczytać wartości przypisanej na nazwę funkcji, - wartość funkcji musi być typu prostego lub String. Uwaga (FreePascal, Delphi): zamiast na nazwę funkcji, można przypisywać na predefiniowaną (lokalnie w każdej funkcji) zmienną result. Wartości przypisane na tę zmienną można odczytywać. (Można też mieszać przypisania na nazwę funkcji i użycia zmiennej result, ale jest to nieczytelne). Programowanie bez procedur i funkcji jest bardzo niewygodne, nie da się bez nich sensownie budować dużych programów. Kiedy używać procedur i funkcji? Zawsze wtedy, gdy mamy fragment programu o jasno określonym zadaniu. Co zyskujemy? Czytelność i łatwość modyfikacji. Zauważmy o ile bardziej czytelna jest ostatnia wersja programu głównego. Zamiast długiego ciągu instrukcji mamy kilka poleceń o jasnym (bo ściśle związanym z zastosowaniem) znaczeniu. Tworzymy w ten sposób maszynę wirtualną. Jednocześnie o wiele prościej analizuje się treść procedur - czytając je myślimy tylko o ich (jasno określonym) celu, np. czytając procedurę wczytującą wektory nie musimy myśleć o ich dodawaniu, czy wypisywaniu.