D/Funkcje
Funkcje
edytujFunkcje są bardzo istotną częścią programowania. Występują w prawie wszystkich współczesnych językach programowania. Dzięki funkcjom nie musimy przepisywać tego samego kodu kilkukrotnie a nasze programy stają się czytelniejsze. Ponadto wbudowane w język funkcje sprawiają, że nie musisz pisać np. algorytmu pierwiastkującego - wystarczy, że wywołasz odpowiednią funkcję. Z funkcji zacząłeś korzystać pisząc swój pierwszy program - instrukcja writefln("Witaj Świecie!");
to właśnie wywołanie funkcji.
Pojęcie i definicja funkcji
edytujFunkcja jest elementem programu wykonującym pewne operacje. Może przyjmować argumenty oraz zwracać wynik. Funkcję, która nie zwraca żadnego wyniku, nazywamy procedurą.
Definicja funkcji w języku D wygląda następująco:
typ_zwracany nazwa_funkcji(typ_argumentu nazwa_argumentu1, typ_argumentu nazwa_argumentu2, ...) {
//instrukcje
}
Gdzie kropki oznaczają, że jeśli możemy zadeklarować funkcję o dowolnej ilości argumentów. Nazwy argumentów, będą zmiennymi dostępnymi wewnątrz funkcji o odpowiednich typach. Czasami nazwy argumentów, nazywamy parametrami formalnymi. Jest tak dlatego, że nazwy argumentów z punktu widzenia kody wywołującego są nie istotne. Są one istotne jedynie dla samej funkcji. Mimo wszystko, nazwy argumentów, powinny wskazywać na ich przeznaczenia, między innymi dlatego, że mogą stanowić cześć automatycznie generowanej dokumentacji (ddoc).
Na przykład:
double odleglosc(double x1, double y1, double x2, double y2) {
auto dx = x1-x2;
auto dy = y1-y2;
return math.sqrt(dx*dx + dy*dy);
}
Użycie deklaracji funkcji w języku nie jest wymagane, ponieważ kompilator potrafi znaleźć funkcje niezależnie od ich kolejności w plikach źródłowych. Jest to dość znaczne usprawnienie w stosunku do języka C.
Aby wywołać funkcję podajemy jej nazwę i argumenty umieszczone w nawiasach. Jak każdą instrukcję, wywołanie funkcji należy zakończyć średnikiem:
nazwa_funkcji(argument1, argument2, ...);
na przykład:
double o = odleglosc(3.0, 3.0, 6.0, 7.0); // o == 4.0, bo odleglosc od (3,3) do (6,7) to 4.
Typ zwracany
edytujFunkcje mogą również zwracać wyniki - przykładowo funkcja double sqrt(double x) zawarta w module std.math zwraca wartość typu double, czyli liczbę zmiennoprzecinkową.
Prosty przykład funkcji
// Funkcja zwracająca wartość typu int
int funkcja1() {
// coś robi
return 2;
}
Jeżeli nie chcemy lub nie potrzebujemy, aby funkcja zwracała wartość, zamiast typu zwracanego podajemy void:
// Funkcja niezwracająca wartości
void funkcja2() {
// coś robi, ale nie zwraca wyniku
writefln("Nic ciekawego.");
}
W powyższym przykładzie pojawiło się słowo kluczowe return. Instrukcja return powoduje natychmiastowe wyjście z funkcji i zwrócenie wyniku (o ile funkcja zwraca jakiś wynik). Jeżeli funkcja zwraca wynik, instrukcja return wraz z podaniem wyniku jest obowiązkowa. Zwracaną wartością może być również wartość zmiennej, tablica, wskaźnik czy też inne jakiekolwiek wyrażenie.
Jeżeli za po instrukcji return znajdą się jakieś inne instrukcje, nie zostaną one wykonane. W procedurach słowo return ma zastosowanie, kiedy w pewnej sytuacji procedura musi zakończyć się w odpowiednim miejscu:
void podzielIWypiszWynik(double a, double b) {
if (b == 0) { // jeżeli b jest równe zero, to nie wykonujemy dzielenia,
// tylko informujemy użytkownika i opuszczamy procedurę.
writefln("Dzielenie przez zero!");
return;
}
writefln("Wynik: %f", a/b);
}
Zauważ, że istnieje pewne podobieństwo instrukcji return do instrukcji break w pętlach z poprzedniego rozdziału.
W wersji 2 języka D, zamiast typu zwracanego można napisać auto w celu poinstruowania kompilatora aby sam wywnioskował typ zwracany:
auto phi(double x, double y) {
return x + cos(y);
}
będzie funkcją zwracającą double, ponieważ x + cos(y) jest typu double, ponieważ x jest double, i cos(y) jest double (bo y i cos(y) jest double). Oprócz ogólnej wygody, jest to szczególnie przydtane w tak zwanych funkcjach szablonowych. Prosty przykład
auto dodaj(A, B)(A x, B y) {
return x+y;
}
Na przykład: dodaj(2, 5) będzie miało typ int, formalniej dodaj!(int, int)(2,5) będzie miało typ int, dodaj(2, 3.14) czy dodaj!(int, double)(2, 3.14), będzie miało typu double (ponieważ 2+3.14 ma taki typ). Często część za ! (tzw. instancja szablonu), nie musi być jawnie wyspecjalizowane ponieważ kompilator potrafi się domyśleć typów na podstawie typów przekazanych argumentów.
Argumenty funkcji
edytujArgumenty funkcji to jej dane wejściowe. Przykładowo pisząc: writefln("Hello World"); przekazujemy funkcji writefln jeden argument - ciąg znaków "Hello World".
// funkcja
void wyswietl(int a) {
writefln("a=%d", a);
}
// wywołanie
int x = 5;
wyswietl(x);
wyswietl(31);
Należy zaznaczyć, że nazwa/identyfikator a pierwszego argumentu funkcji wyswietl, nie ma większego znaczenia z punktu kodu wykonującego. Mówimy, że a jest argumentem formalnym, i ma jedynie znaczenie dla samej funkcji, tj. definiuje zmienną w tej funkcji. Natomiast zmienna czy wartość przekazywana jako ten argument, może się nazywać całkiem inaczej. W szczególności, możemy zmienić w funkcji wyswietl na następującą
// nowa funkcja wyswietl
void wyswietl(int z) {
writefln("a=%d", z);
}
i działanie programu będzie absolutnie identyczne.
Sposoby przekazywania parametrów
edytujJęzyk D umożliwia przekazywanie parametrów funkcjom na kilka różnych sposobów.
Standardowo zmienne są przekazywane przez tak zwaną wartość. Można to rozumieć, że w chwili wejścia do funkcji, do argumentu formalnego jest jest kopiowana wartość wyrażenia (np. zmiennej) z jaką została wywołana dana funkcja. Ponieważ operujemy na kopii, to modyfikacje takiego argumentu, powodują operacje na oddzielnej wersji, i nie powodują modyfikacji zmiennych które zostały przekazane. W szczególności, przekazane parametry nie muszą być zmiennymi.
void f(int x) {
x = 2 * x;
writefln("2x=%d", x);
}
int a = 5;
f(a); // wyświetli 10
writefln(a); // wyświetli 5
f(7); // wyświetli 14
Jest to tak zwane typ in (z ang. input, wejście). Mogli byśmy napisać:
void f(in int x) {
x = 2 * x;
writefln("2x=%d", x);
}
Ten sposób przekazywania parametrów funkcjom jest domyślny, więc słowo kluczowe in nie jest wymagane. Można przyjąć że jest to oddzielna zmienna, o wartości takiej jaką miał argument wykonania. Modyfikowanie tego argumentu, spowoduje jedynie zmiany wewnątrz funkcji.
Poza tym istnieją dwa inne sposoby przekazywania argumentów.
inout (z ang. input and output, wejście i wyjście) - tak oznaczony argument, w rzeczywistości będzie wskazywał na przekazany argument wykonania. Jej zmiana spowoduje również zmianę argumentu który został przekazany.
void f(inout int x) {
x = 2 * x;
writefln("2x=%d", x);
}
int a = 5;
f(a); // wyświetli 10
writefln(a); // wyświetli 10, ponieważ f, w rzeczywistości operowało na a
f(7); // BŁĄD. nie da się przekazać liczby jako argumentu inout
f(a+2); // BŁĄD. nie da się przekazać wyrażenia jako argumentu inout
W rozdziale o funkcjach wczytywaniu liczb, spotkaliśmy się z funkcją toInt, która modyfikowała przekazany napis, właśnie w podobny sposób.
Ostatni sposób, to out - tak oznaczony argument musi zostać jedynie zapisywany wewnątrz funkcji, jest on użyteczny jeśli chcemy zwrócić z funkcji wiele wartości, co nie jest prosto do wykonania poprzez instrukcje return, która zwraca tylko jedną wartość.
void f(int x, out int y, out int z) {
y = 5 * x / 9;
z = x - y;
}
int a = 36;
int b, c;
f(a, b, c);
writefln(a, b, c); // wyświetli 36 20 16
f(36, b, c);
writefln(a, b, c); // tak samo
f(36, 5, 6); // BŁĄD. parametry out, nie mogą być liczbami
W szczególności, funkcja o postaci
void f(int x, out int y, out int z, out int h) {
z = x + y;
}
zostanie odrzucona przez kompilator, ponieważ czytamy zmienną y, czego nie możemy zrobić, zwłaszcza jeśli jej uprzednio nie zapisaliśmy jeszcze. Dodatkowo zmienna h nie jest zapisywana, a powinna.
Parametry domyślne
edytujParametry funkcji mogą mieć też ustawione wartości domyślne. Zostaną one wykorzystane, jeżeli nie podamy parametru w wywołaniu. Ustawiamy je po prostu przypisując wartości parametrów formalnych w definicji funkcji.
void funkcja(int a = 1) {
writefln("%d", a);
}
Jeżeli w wywołaniu funkcji podamy wartość argumentu a, zostanie ona podstawiona. W przeciwnym wypadku zostanie wykorzystana wartość domyślna parametru, czyli - w tym wypadku 1.
f(5); // wyświetli 5 f(); // wyświetli 1
Zagnieżdżanie funkcji
edytujW języku D istnieje możliwość zagnieżdżania funkcji, to znaczy umieszczania jednej funkcji wewnątrz drugiej. Przydaje się to na przykład do tworzenia funkcji pomocniczych, które wykorzystywane są tylko wewnątrz jednej funkcji.
Przykład zagnieżdżania funkcji:
import std.math;
double distance(double x1, double y1, double x2, double y2) {
double f(double a, double b) {
double d = a-b;
return d*d;
}
return sqrt(f(x1, y1), f(x2, y2));
}
Funkcje zagnieżdżone, poza własnymi parametrami, mają dostęp również do parametrów i zmiennych funkcji w których zostały zagnieżdżone.
import std.math;
void f(int a, int b) {
double g(int c) {
writefln("%d %d %d", a, b, c);
}
g(5);
g(43);
}
f(1, 2); // wyświetli 1 2 5 oraz 1 2 43
Wykonanie w czasie kompilacji
edytujFunkcje czasu kompilacji, to mechanizm pozwalający wykonać pewną klasę funkcji, już trakcie kompilacji, zanim program się wykona. Jest to pomocne do obliczenia wartości stałych, i tworzenia programów metodami metaprogramowania. Zobacz więcej w rozdziale dla wtajemniczonych: Funkcje czasu kompilacji oraz Metaprogramowanie.
Rekurencja
edytujRekurencja (w przeciwieństwie do iteracji) jest sytuacją, w której funkcja wywołuje samą siebie. Każdą funkcję iteracyjną można zmienić w rekurencyjną i na odwrót, funkcje iteracyjne cechuje jednak często niższe zużycie pamięci i są one lepsze w większości sytuacji. Istnieją jednak też problemy które wygodniej i (czasami) wydajniej da się rozwiązać rekurencyjnie, zalicza się do nich m.in. silnie i szukanie n-tego elementu ciągu Fibonacciego. Właśnie te dwa problemy rozwiążą poniższe przykłady:
// silnia.d
ulong silnia(uint n)
{
// 0! = 1
if(n == 0)
return 1;
/* n! = n * (n - 1)! */
return n * silnia(n - 1);
}
// fibonacci.d
ulong fib(uint n)
{
// Fib(0) = 1 i Fib(1) = 1
if(n == 0 || n == 1)
return 1;
/* Fib(n) = Fib(n - 1) + Fib(n - 2) */
return fib(n - 1) + fib(n - 2);
}
Wskaźniki
edytujWskaźniki na funkcje deklarujemy trochę inaczej niż zwykłe, uwzględniając że na tym etapie możesz nie znać ich jeszcze w ogóle, omówimy sprawę bardzo pobieżnie. Pierwsza, prostsza metoda to użycie „typu” auto, dobierze on już właściwy typ dedukując go z reszty kodu. Nie jest niestety uniwersalny, ponieważ taka dedukcja nie zawsze jest możliwa.
double add(double a, double b) { return a + b; }
/* ... */
auto sth = &add;
writeln(typeid(sth), "\n", sth(2, 3));
Funkcja add pobiera dwa argumenty typu double i zwraca ich sumę.
auto sth = &add;
Tutaj tworzymy zmienną sth i przypisujemy do niej wskaźnik na funkcję add. Kompilator mógł się łatwo domyślić jaki typ musi mieć zmienna sth, więc kompilacja przebiegła prawidłowo a po uruchomieniu programu otrzymamy to:
double()* 5
Pierwsza linia to typ zmiennej (widać, że jest to wskaźnik) a druga to wynik jej wykonania, 2 + 2 = 5, więc wszystko jest ok. Nie zawsze można sobie jednak pozwolić na luksus jakim jest typ auto, dlatego trzeba też wiedzieć jak ręcznie utworzyć wskaźnik na funkcję. Jest to jednak dużo prostsze niż w C, więc nie ma się czego obawiać. Sprawa wygląda mniej–więcej tak:
typ_zwracany function(typ_parametru1, typ_parametru2, …) nazwa_wskaznika;
W takim razie zmienna wskaźnikowa sth, wskazująca na wcześniejszą funkcję add, zgodnie z powyższym szablonem wyglądałaby tak:
double function(double, double) sth = &add;
Program kompiluje się i uruchamia z takim samym efektem, więc jak widać wszystko działa.
Delegaty
edytujDelegaty w przypadku niestatycznych metod w klasach i strukturach, lub funkcji w innych funkcjach są bardzo podobne do wskaźników, zawierają jednak jeszcze kopię wskaźnika this, pozwalającą na poprawne wykonanie metody. Na początek interesująca nas struktura, do której metody chcielibyśmy utworzyć delegat:
struct Adder
{
double a;
double b;
double bar()
{
return a + b;
}
}
Niezbyt finezyjnie. Tworzymy instancję, ustawiamy wartości zmiennych a i b, wywołujemy funkcję bar() i mamy wynik dodawania. Skąd jednak program ma wiedzieć, które a i b wykorzystać? Jak wiadomo instancji jednej klasy mogą być tysiące, po to jest wskaźnik this. Zwykłe wskaźniki nie są w stanie go przechowywać, dlatego musiały powstać delegaty. Pomijając typ auto, działający dokładnie tak samo jak w przypadku wskaźników, przejdziemy od razu do właściwych delegatów. Składnia jest bardzo podobna do wskaźników na funkcje:
typ_zwracany delegate(typ_parametru1, typ_parametru2, …) nazwa_delegatu = &nazwa_instancji.nazwa_metody;
Prosty przykład:
Adder adder;
adder.a = 3.2;
adder.b = 1.3;
double delegate() delegat = &adder.bar;
writeln(typeid(delegat), "\n", delegat());
Po kompilacji naszym oczom powinno się ukazać to:
double delegate() 4.5
W wypadku funkcji w funkcjach wszystko działa bardzo podobnie:
void main()
{
double a = 2.2;
double b = 1.3;
double bar()
{
return a + b;
}
double delegate() delegat = &bar;
writeln(typeid(delegat), "\n", delegat());
}
Wskaźniki na funkcje w funkcjach nie mogą być zwykłymi wskaźnikami na funkcję (próba zastąpienia delegate przez function zaowocuje błędem kompilacji), ale wynika to z pewnych zawiłości języka D, więc tak jak inne możliwości delegatów, zostanie to opisane w dalszych częściach tego podręcznika.