Funkcje
Funkcje są powszechne w kodzie Rusta. Widziałeś już jedną z
najważniejszych funkcji w języku: funkcję main, która jest punktem wejścia
wielu programów. Widziałeś również słowo kluczowe fn, które pozwala
deklarować nowe funkcje.
Kod Rusta używa snake case jako konwencjonalnego stylu dla nazw funkcji i zmiennych, w którym wszystkie litery są małe, a podkreślenia oddzielają słowa. Oto program, który zawiera przykładową definicję funkcji:
Nazwa pliku: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Inna funkcja.");
}
Definiujemy funkcję w Ruście, wpisując fn po której następuje nazwa funkcji i
zestaw nawiasów. Nawiasy klamrowe informują kompilator, gdzie zaczyna się i
kończy ciało funkcji.
Możemy wywołać dowolną zdefiniowaną przez nas funkcję, wpisując jej nazwę,
a następnie zestaw nawiasów. Ponieważ another_function jest zdefiniowana w
programie, może być wywołana z wewnątrz funkcji main. Zauważ, że zdefiniowaliśmy
another_function po funkcji main w kodzie źródłowym; mogliśmy ją również
zdefiniować wcześniej. Rust nie dba o to, gdzie definiujesz swoje funkcje,
a jedynie o to, czy są zdefiniowane w zasięgu, który może być widoczny dla
wywołującego.
Utwórzmy nowy projekt binarny o nazwie functions, aby dokładniej zbadać
funkcje. Umieść przykład another_function w src/main.rs i uruchom go.
Ppowinien pojawić się następujący wynik:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Inna funkcja.
Wiersze wykonują się w kolejności, w jakiej pojawiają się w funkcji main.
Najpierw wypisuje się komunikat „Witaj, świecie!”, a następnie wywoływana jest
another_function i wypisuje się jej komunikat.
Parametry
Możemy definiować funkcje tak, aby miały parametry, które są specjalnymi zmiennymi będącymi częścią sygnatury funkcji. Gdy funkcja ma parametry, można podać jej konkretne wartości dla tych parametrów. Technicznie, konkretne wartości nazywane są argumentami, ale w potocznej rozmowie ludzie mają skłonność do używania słów parametr i argument zamiennie dla zmiennych w definicji funkcji lub konkretnych wartości przekazywanych podczas wywoływania funkcji.
W tej wersji another_function dodajemy parametr:
Nazwa pliku: src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("Wartość x to: {x}");
}
Spróbuj uruchomić ten program; powinieneś uzyskać następujący wynik:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
Wartość x to: 5
Deklaracja another_function ma jeden parametr o nazwie x. Typ x jest
określony jako i32. Kiedy przekazujemy 5 do another_function, makro
println! umieszcza 5 tam, gdzie w ciągu formatującym znajdowały się nawiasy
klemrowe zawierające x.
W sygnaturach funkcji musisz zadeklarować typ każdego parametru. Jest to świadoma decyzja w projekcie Rusta: Wymaganie adnotacji typów w definicjach funkcji oznacza, że kompilator prawie nigdy nie potrzebuje, abyś używał ich w innym miejscu w kodzie, aby zrozumieć, jaki typ masz na myśli. Kompilator jest również w stanie dostarczyć bardziej pomocne komunikaty o błędach, jeśli wie, jakich typów oczekuje funkcja.
Podczas definiowania wielu parametrów, oddziel deklaracje parametrów przecinkami, w ten sposób:
Nazwa pliku: src/main.rs
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("Pomiar to: {value}{unit_label}");
}
Ten przykład tworzy funkcję o nazwie print_labeled_measurement z dwoma
parametrami. Pierwszy parametr ma nazwę value i jest typu i32. Drugi ma
nazwę unit_label i jest typu char. Funkcja następnie wypisuje tekst
zawierający zarówno value, jak i unit_label.
Spróbuj uruchomić ten kod. Zastąp program znajdujący się obecnie w pliku
src/main.rs projektu functions poprzednim przykładem i uruchom go za
pomocą cargo run:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
Pomiar to: 5h
Ponieważ wywołaliśmy funkcję z 5 jako wartością dla value i 'h' jako
wartością dla unit_label, wyjście programu zawiera te wartości.
Instrukcje i wyrażenia
Ciała funkcji składają się z serii instrukcji, opcjonalnie zakończonych wyrażeniem. Do tej pory omówione przez nas funkcje nie zawierały końcowego wyrażenia, ale widziałeś wyrażenie jako część instrukcji. Ponieważ Rust jest językiem opartym na wyrażeniach, jest to ważne rozróżnienie do zrozumienia. Inne języki nie mają tych samych rozróżnień, więc przyjrzyjmy się, czym są instrukcje i wyrażenia oraz jak ich różnice wpływają na ciała funkcji.
- Instrukcje to polecenia, które wykonują jakąś akcję i nie zwracają wartości.
- Wyrażenia obliczają się do wartości wynikowej.
Przyjrzyjmy się kilku przykładom.
W rzeczywistości już używaliśmy instrukcji i wyrażeń. Tworzenie zmiennej i
przypisywanie jej wartości za pomocą słowa kluczowego let jest instrukcją.
W Listingu 3-1, let y = 6; to instrukcja.
fn main() {
let y = 6;
}
Definicje funkcji to również instrukcje; cały poprzedni przykład sam w sobie jest instrukcją. (Jak wkrótce zobaczymy, wywoływanie funkcji nie jest instrukcją.)
Instrukcje nie zwracają wartości. Dlatego nie możesz przypisać instrukcji
let do innej zmiennej, tak jak próbuje to zrobić poniższy kod; otrzymasz błąd:
Nazwa pliku: src/main.rs
fn main() {
let x = (let y = 6);
}
Po uruchomieniu tego programu otrzymasz następujący błąd:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
Instrukcja let y = 6 nie zwraca wartości, więc nie ma nic, do czego x
mogłoby się związać. Różni się to od tego, co dzieje się w innych językach,
takich jak C i Ruby, gdzie przypisanie zwraca wartość przypisania. W tych
językach można napisać x = y = 6 i sprawić, że zarówno x, jak i y będą
miały wartość 6; tak nie jest w Ruście.
Wyrażenia obliczają się do wartości i stanowią większość pozostałego kodu,
który będziesz pisać w Ruście. Rozważ operację matematyczną, taką jak 5 + 6,
która jest wyrażeniem, które oblicza się do wartości 11. Wyrażenia mogą być
częścią instrukcji: W Listingu 3-1, 6 w instrukcji let y = 6; jest
wyrażeniem, które oblicza się do wartości 6. Wywoływanie funkcji jest
wyrażeniem. Wywoływanie makra jest wyrażeniem. Nowy blok zasięgu utworzony za
pomocą nawiasów klamrowych jest wyrażeniem, na przykład:
Nazwa pliku: src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("Wartość y to: {y}");
}
To wyrażenie:
{
let x = 3;
x + 1
}
jest blokiem, który w tym przypadku oblicza się do 4. Ta wartość zostaje
związana z y jako część instrukcji let. Zauważ wiersz x + 1 bez średnika
na końcu, co różni się od większości dotychczas widzianych wierszy. Wyrażenia
nie zawierają końcowych średników. Jeśli dodasz średnik na końcu wyrażenia,
zmienisz je w instrukcję, a wtedy nie zwróci ono wartości. Miej to na uwadze,
gdy będziesz dalej poznawać wartości zwracane przez funkcje i wyrażenia.
Funkcje zwracające wartości
Funkcje mogą zwracać wartości do wywołującego je kodu. Nie nazywamy wartości
zwracanych, ale musimy zadeklarować ich typ po strzałce (->). W Ruście,
wartość zwracana przez funkcję jest synonimem wartości ostatniego wyrażenia w
bloku ciała funkcji. Możesz zwrócić wartość wcześniej z funkcji, używając
słowa kluczowego return i określając wartość, ale większość funkcji zwraca
ostatnie wyrażenie niejawnie. Oto przykład funkcji, która zwraca wartość:
Nazwa pliku: src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("Wartość x to: {x}");
}
Nie ma wywołań funkcji, makr, ani nawet instrukcji let w funkcji five —
tylko sama liczba 5. To jest całkowicie poprawna funkcja w Ruście.
Zauważ, że typ zwracany funkcji jest również określony jako -> i32.
Spróbuj uruchomić ten kod; wyjście powinno wyglądać tak:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
Wartość x to: 5
5 w five jest wartością zwracaną przez funkcję, dlatego typ zwracany to
i32. Zbadajmy to bardziej szczegółowo. Są tu dwa ważne aspekty:
Po pierwsze, wiersz let x = five(); pokazuje, że używamy wartości
zwracanej przez funkcję do inicjalizacji zmiennej. Ponieważ funkcja five
zwraca 5, ten wiersz jest równoważny z następującym:
#![allow(unused)]
fn main() {
let x = 5;
}
Po drugie, funkcja five nie ma parametrów i definiuje typ wartości
zwracanej, ale ciało funkcji to samotne 5 bez średnika, ponieważ jest to
wyrażenie, którego wartość chcemy zwrócić.
Przyjrzyjmy się innemu przykładowi:
Nazwa pliku: src/main.rs
fn main() {
let x = plus_one(5);
println!("Wartość x to: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
Uruchomienie tego kodu wypisze Wartość x to: 6. Ale co się stanie, jeśli
umieścimy średnik na końcu wiersza zawierającego x + 1, zmieniając go z
wyrażenia w instrukcję?
Nazwa pliku: src/main.rs
fn main() {
let x = plus_one(5);
println!("Wartość x to: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
Skompilowanie tego kodu spowoduje błąd, jak następuje:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
Główny komunikat o błędzie, mismatched types (niezgodne typy), ujawnia
sedno problemu z tym kodem. Definicja funkcji plus_one mówi, że zwróci i32,
ale instrukcje nie obliczają się do wartości, co jest wyrażone przez (),
typ jednostkowy. Dlatego nic nie jest zwracane, co jest sprzeczne z definicją
funkcji i skutkuje błędem. W tym wyjściu Rust dostarcza komunikat, który być
może pomoże rozwiązać ten problem: sugeruje usunięcie średnika, co naprawiłoby
błąd.