4. Funkcje
Idea podziału programu na fragmenty, które mogą być opracowywane odrębnie
od siebie, do kodu włączone jednokrotnie a wywoływane wielokrotnie jest zrealizowana
(w taki czy inny sposób) chyba we wszystkich językach programowania. Funkcja - to wyodrębniony opis czynności (zestaw instrukcji)
zapisany we fragmencie kodu, który może być jednokrotnie połączony (przez
wstawienie albo w fazie kompilacji lub wykonania) z programem źródłowym i
wielokrotnie użyty za pomocą odwołań z innych fragmentów programu
Funkcje definiujemy w programie za pomocą dostarczenia:
Uwaga. W klasycznym REXXie występuje pojęcie procedury, a składnia jest
zupełnie inna.
W różnych językach w różny sposób definiuje się nagłówki i ogranicza ciało funkcji. Umówmy się, że w naszym uproszczonym, zmodyfikowanym REXXie
nagłówki funkcji będziemy oznaczać słowem function, występującym jako
pierwsze słowo w wierszu, po którym następuje nazwa funkcji i w nawiasach
okrągłych lista parametrów (opis informacji, przekazywanej do funkcji) a
kod funkcji będzie się zawsze kończył instrukcją return, która zwróci
sterowanie do miejsca wywołania funkcji, ew. zwracając też wynik funkcji
jako wartość wyrażenia podanego w instrukcji return. function nazwa_funkcji ( lista_parametrów ) ins1 ins2 ... insN return [ wyrażenie ] Uwagi: Parametry są zmiennymi za pomocą których w ciele funkcji uzyskujemy
dostęp do informacji przekazanych przy wywołaniu funkcji.
Zatem, żeby zrozumieć budowę funkcji musimy też wiedzieć w jaki sposób funkcję się wywołuje. Wywołanie funkcji jest wyrażeniem o następującej postaci:
nazwa_funkcji(lista_argumentów) Przy czym:
Np. możemy zdefiniować funkcję, której zadaniem będzie porównanie dwóch wartości.
Każde wywołanie tej funkcji będzie jej przekazywać dwa argumenty - wartości
do porównania. Będą one dostępne w funkcji poprzez nazwy odpowiednich parametrów.
Po porównaniu funkcja zwróci wynik do miejsca wywołania (0 - wartości rózne,
1, - pierwszy argument większy od drugiego, -1 - pierwszy mniejszy od drugiego).
W innych językach będzie inaczej: np. w C program składa się wyłącznie
z definicji funkcji (oraz ew. z deklaracji tzw zmiennych globalnych), wśród
których jedna - o nazwie main - jest wyróżniona i stanowi odpowiednik naszego
"głównego programu". Java stanowi nieco inny przypadek, bowiem jest językiem
obiektowym i operuje wyłącznie klasami.
Definiując funkcje musimy wiedzieć nie tylko jak sformułować jej definicję, ale również w jaki sposób umieścić ją w strukturze całego programu. I znowu, różne języki stosują tutaj różne reguły. W naszym uproszczonym REXXie będziemy rozróżniać główny program i funkcje. Główny program zaczynać się będzie w pierwszym wierszu pliku źródłowego i musi kończyć się instrukcją exit (koniec wykonania programu). Dopiero potem mogą następować definicje funkcji. Zobaczmy przykład. Program korzystający ze zdefiniowanej funkcji compare
i porównujący wartości wprowadzane przez użytkownika z konsoli. Przy okazji
na tym przykładzie warto zaobserwować, że funkcja może być wywoływana
z innej funkcji. /* Test funkcji */ do forever v1 = linein(); if (v1 = '') then exit; v2 = linein(); if (v2 = '') then exit; showComparison( v1, v2); end exit function compare ( val1, val2 ) if ( val1 > val2 ) then return 1; else if (val1 < val2) then return -1; else return 0; function showComparison ( val1, val2) msg[1] = "większa od"; msg[0] = "rowna"; i = -1; msg[i] = "mniejsza od"; r = compare( val1, val2 ); say "Wartość " val1 " jest " msg[r] "wartości " val2 return Pętla do forever (słowo kluczowe języka) wykonywana jest w
nieskończoność. W petli tej użytkownik wprowadza z konsoli po kolei dwie
wartości (liczby lub napisy!). Wciśnięcie "pustego" ENTER przerywa działanie
pętli (bowiem kończy program - instrukcja exit). Po wprowadzeniu wartości
wołana jest funkcja showComparison, która sama z kolei woła funkcję compare
i wyprowadza wyniki porównania. Wyprowadzane napisy ("mniejsze od", "równe",
"większe od") umieściliśmy w tablicy, której indeksy odpowiadają wynikom
porównania zwracanym przez funkcję compare (w ten sposób unikamy instrukcji
if, które - jeśli są rozbudowane - stanowią częste źródło błędów logicznych
w programie). Na marginesie: REXX dopuszcza dowolne wartości jako indeksy
tablicy, ale wartości, które nie są nieujemnymi liczbami całkowitymi powinny
być podawane jako zmienne, dlatego użyliśmy zmiennej i z nadaną wartością
-1 do indeksowania elementu-napisu "mniejsze od". Jeżeli funkcja nie ma żadnych parametrów to w jej definicji i tak trzeba podać po nazwie nawiasy okrągłe ().
Przy wywołaniu funkcji bez podawania jakichkolwiek argumentów - po nazwie funkcji również podajemy nawiasy okrągłe () Możemy teraz stwierdzić, że w wykorzystywanych przez nas wcześniej funkcjach
linein(), time(), right() nie ma nic tajemniczego. Po prostu, niektóre użyteczne
funkcje już zostały napisane i są udostępnione jako tzw. funkcje wbudowane
(wbudowane w REXX). W innych językach mamy tzw. biblioteki funkcji standardowych,
które też już są gotowe do wykorzystania. Pakiety Javy (choć mają nieco inne
znaczenie) możemy kojarzyć pod względem funkcjonalności z bibliotekami funkcji
standardowych. Funkcje wbudowane lub standardowe - to skompilowane, gotowe do wykorzystania
w programach funkcje, spełniające wiele użytecznych zadań np. przetwarzanie
łańcuchów znakowych, wejście-wyjście, funkcje matematyczne
Przez definicję zmiennej w REXXie będziemy rozumieli pierwsze jej
użycie (np. przypisanie) jej wartości). W innych językach rozróżnia się deklarację
i definicję zmiennej; o deklaracjach będziemy mówić ucząc się Javy
Tworząc i używając funkcji musimy zdawać sobie sprawę z dwóch ważnych problemów:
W większości języków programowania zmienne definiowane w kodzie funkcji są tak zwanymi zmiennymi lokalnymi.
W naszym uproszczonym REXXie mamy do czynienia tylko z lokalnymi blokami
wprowadzanymi przez definicję funkcji. W innych językach możemy mieć zagnieżdżone
bloki lokalne wprowadzane przez instrukcję grupującą, np. w C czy Javie -
nawiasy klamrowe
Ciało funkcji jest szczególnym przypadkiem tzw. bloku lokalnego. Istotnie, definicja funkcji grupuje kod (w REXXie między nagłowkiem funkcji a słowem return włącznie) - stąd nazwa "blok", natomiast słowo "lokalny" wiąże się z właściwościami tego bloku, m.in. z tym, że każda zmienna zdefiniowana w tym bloku ma charakter lokalny) czyli: Zmienna lokalna jest zmienną zdefiniowaną w bloku lokalnym, widzianą
(przez kompilator czy interpreter) i istniejącą tylko w tym bloku, od początku
definicji zmiennej do końca bloku
Zobaczmy przykład: a = 3; b = 5; func1(); func2(); say "main a =" a "b =" b; exit; function func1() a = 7; b = 10; c = 12; func2(); say "func1 a =" a "b = " b "c = " c; return; function func2() a = 100; b = 101; c = 101; say "func2 a =" a "b = " b "c = " c; return;
Wydruk z programu
func2 a = 100 b = 101 c = 101 func1 a = 7 b = 10 c = 12 func2 a = 100 b = 101 c = 101 main a = 3 b = 5 Ważne obserwacje:
Widać tu wyraźnie, że zmienne a, b, c są lokalne w funkcjach func1 i func2
(a zatem za każdym razem istnieją tylko w ramach bloku danej funkcji). I
co potwierdza wydruk programu każda funkcja operuje na swoich lokalnych
zmiennych a, b, c. Dlatego - mimo wywołania func2 z func1 - wartości zmiennych
a, b, c na końcu func1 równe są 7, 10, 12 (a nie 100, 101,101). Tak samo
a i b na końcu działania programu (już po wywołaniu func1) mają wartości
nadane w dwóch pierwszych instrukcjach programu głównego.
W języku C podobny efekt uzyskuje się za pomocą tzw. zmiennych globalnych,
deklarowanych poza ciałem jakiejkolwiek funkcji. W Javie pewnym odpowiednikiem
zmiennych globalnych są pola klasy.
W REXXie mechanizm "dzielenia" zmiennych przez główny program i funkcje jest bardzo elastyczny. Dla każdej funkcji - indywidualnie - możemy w jej nagłówku podać jakie zmienne (w tym tablice) definiowane poza funkcją będą dostępne w jej ciele. Służy temu klauzula expose.
function (lista_parametrów) expose lista_zmiennych
gdzie:
Drugi, istotny, wspomniany wcześniej, problem dotyczy sposobu przekazywania argumentów przy wywołaniu funkcji. Mówimy, że argument przekazywany jest przez wartość, jeśli przy
wywołaniu funkcji wartość argumentu kopiowana jest do zmiennej-parametru występującej w definicji funkcji na liście parametrów
Ma to poważne konsekwencje. Otóż parametry funkcji są zmiennymi lokalnymi.
Zatem - przy przekazywaniu argumentów przez wartość - w funkcji mamy dostęp
do wartości przekazanego argumentu (poprzez zmienną-parametr), ale zmiany
tej wartości będą dotyczyć tylko zmiennej-parametru, a nie zmiennej - przekazanego
argumentu. a = 3; tryChange(a); say a; exit; function tryChange(a) a = a + 1; return; Funkcja otrzymuje - jako parametr a - wartość zmiennej a zdefiniowanej w pierwszym wierszu. Modyfikacje argumentów przekazanych przez wartość są nieskuteczne
Ze względu na implementację tablic w C, C++ i Javie elementy przekazanych
funkcji tablic mogą być modyfikowane (ale nie oznacza to, że argumenty są
przekazywane przez adres). W naszym uproszczonym REXXie nie możemy przekazywac
tablic jako argumentów, zamiast tego - dla udostępniania tablic funkcjom
- będziemy stosować klauzulę expose
W niektórych językach programowania możliwe jest przekazywania argumentów przez adres (wtedy funkcja otrzymuje nie wartość, ale adres argumentu). Mając adres możemy oczywiście nie tylko pobrać dane spod tego adresu, ale i zapisać tam jakąś nową wartość. Ten sposób przekazywania argumentów nie jest jednak możliwy w Javie ani też w naszym uproszczonym REXXie, zatem pozostawimy go na boku. Po tej dawce dość skomplikowanych rozważań warto spojrzeć na omawiane zagadnienie bardziej ogólnie.
Trzy pierwsze zadania zrealizujemy jako funkcje (wprowadzenie ceny komponentu
- inputData(..), obliczenie ceny komputera - sumPrices(), wybór numeru komponentu
z listy - choose()). Pozostałe dwa, wraz z organizacją danych i kolejności
wywołań funkcji będą stanowić treść programu głównego. /* Tablica nazw składników */ elt[0] = 6; /* liczba skladnikow - w elemencie z indeksem 0 */ elt[] = { "Procesor", "Płyta", "Pamięć", "Dysk", "Monitor", "Inne" }; cena[0] = elt[0] /* tyle samo cen ile skladnikow */ do i = 1 to cena[0] /* pobieranie cen i umieszczanie w tablicy cena */ cena[i] = inputData(elt[i]); end more = 1; /* czy powtarzać obliczenia i zmiany ? */ do while (more = 1) cenaOg = sumPrices(); /* obliczenie ceny komputera */ say "Cena komputera wynosi :" cenaOg; nrSkl = choose("Wybierz składnik, którego udział w cenie chcesz policzyć"); if (nrSkl \= '') then do /* jeżeli wybrano składnik - licz udział */ udzial = cena[nrSkl]/cenaOg; say "Udzial ceny skladnika" elt[nrSkl] "wynosi: " udzial; end nrSkl = choose("Wybierz skladnik, ktorego cene chcesz zmienic"); if (nrSkl = "") then more = 0; /* jeżeli zrezygnowano z wyboru - koniec działania*/ else cena[nrSkl] = inputData( elt[nrSkl] ); end exit; A funkcje: function inputData(nazwaSkl) say "Wprowadz cene dla: " nazwaSkl; trzebaPobracDane = 1; do while (trzebaPobracDane) cena = linein() if datatype(cena) = "NUM" then trzebaPobracDane = 0; end return cena; function sumPrices() expose cena[] sum = 0; do i = 1 to cena[0] /* umowa: cena[0] zawiera liczbę elementów tablicy cena */ sum = sum + cena[i]; end return sum; function choose(msg) expose elt[] say msg; do i = 1 to elt[0] /* umowa: elt[0] zawiera liczbę elementów tablicy elt */ say i '-' elt[i]; end do forever say "Podaj wybrany numer lub samo ENTER by zrezygnowac:" nr = linein(); if nr = "" then leave; if (nr < 1 | nr > elt[0]) then say "Zły wybór."; else leave; end return nr; Uwagi:
Proszę zapisać i uruchomić omawiany wyżej program na własnym komputerze.
|