Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.