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

Przepływ sterowania

Możliwość uruchomienia części kodu w zależności od tego, czy warunek jest true, oraz możliwość wielokrotnego uruchamiania części kodu, dopóki warunek jest true, to podstawowe bloki konstrukcyjne w większości języków programowania. Najczęstsze konstrukcje, które pozwalają kontrolować przepływ wykonania kodu Rusta, to wyrażenia if i pętle.

Wyrażenia if

Wyrażenie if pozwala rozgałęziać kod w zależności od warunków. Podajesz warunek, a następnie stwierdzasz: „Jeśli ten warunek jest spełniony, uruchom ten blok kodu. Jeśli warunek nie jest spełniony, nie uruchamiaj tego bloku kodu.”

Utwórz nowy projekt o nazwie branches w katalogu projects, aby zbadać wyrażenie if. W pliku src/main.rs wprowadź następujący kod:

Nazwa pliku: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("warunek był prawdziwy");
    } else {
        println!("warunek był fałszywy");
    }
}

Wszystkie wyrażenia if zaczynają się od słowa kluczowego if, po którym następuje warunek. W tym przypadku warunek sprawdza, czy zmienna number ma wartość mniejszą niż 5. Blok kodu do wykonania, jeśli warunek jest true, umieszczamy bezpośrednio po warunku w nawiasach klamrowych. Bloki kodu powiązane z warunkami w wyrażeniach if są czasami nazywane ramionami, podobnie jak ramiona w wyrażeniach match, które omówiliśmy w sekcji „Porównywanie zgadywanej liczby z tajną liczbą” w Rozdziale 2.

Opcjonalnie możemy również dołączyć wyrażenie else, co tutaj zrobiliśmy, aby podać programowi alternatywny blok kodu do wykonania, jeśli warunek oceni się na false. Jeśli nie podasz wyrażenia else, a warunek jest false, program po prostu pominie blok if i przejdzie do następnej części kodu.

Spróbuj uruchomić ten kod; powinieneś zobaczyć następujący wynik:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
warunek był prawdziwy

Spróbujmy zmienić wartość number na taką, która sprawi, że warunek będzie false, aby zobaczyć, co się stanie:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Uruchom program ponownie i spójrz na wynik:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
warunek był fałszywy

Warto również zauważyć, że warunek w tym kodzie musi być typu bool. Jeśli warunek nie jest typu bool, otrzymamy błąd. Na przykład, spróbuj uruchomić następujący kod:

Nazwa pliku: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

Warunek if tym razem oblicza się do wartości 3, a Rust zwraca błąd:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Błąd wskazuje, że Rust oczekiwał bool, ale otrzymał liczbę całkowitą. W przeciwieństwie do języków takich jak Ruby i JavaScript, Rust nie będzie automatycznie próbował konwertować typów niebędących bool na bool. Musisz być jawny i zawsze dostarczać if z wartością bool jako warunkiem. Jeśli chcemy, aby blok kodu if był uruchamiany tylko wtedy, gdy liczba nie jest równa 0, na przykład, możemy zmienić wyrażenie if na następujące:

Nazwa pliku: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number było czymś innym niż zero");
    }
}

Uruchomienie tego kodu wypisze number było czymś innym niż zero.

Obsługa wielu warunków za pomocą else if

Możesz użyć wielu warunków, łącząc if i else w wyrażeniu else if. Na przykład:

Nazwa pliku: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("liczba jest podzielna przez 4");
    } else if number % 3 == 0 {
        println!("liczba jest podzielna przez 3");
    } else if number % 2 == 0 {
        println!("liczba jest podzielna przez 2");
    } else {
        println!("liczba nie jest podzielna przez 4, 3 ani 2");
    }
}

Ten program może podążyć czterema możliwymi ścieżkami. Po jego uruchomieniu powinieneś zobaczyć następujący wynik:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
liczba jest podzielna przez 3

Kiedy ten program się wykonuje, sprawdza kolejno każde wyrażenie if i wykonuje pierwszy blok, dla którego warunek ocenia się na true. Zauważ, że mimo że 6 jest podzielne przez 2, nie widzimy wyjścia liczba jest podzielna przez 2, ani tekstu liczba nie jest podzielna przez 4, 3 ani 2 z bloku else. Dzieje się tak, ponieważ Rust wykonuje blok tylko dla pierwszego warunku true, a gdy tylko znajdzie taki, nie sprawdza już reszty.

Zbyt wiele wyrażeń else if może zaśmiecać kod, więc jeśli masz ich więcej niż jeden, możesz chcieć refaktoryzować swój kod. Rozdział 6 opisuje potężną konstrukcję rozgałęziającą Rusta o nazwie match dla takich przypadków.

Używanie if w instrukcji let

Ponieważ if jest wyrażeniem, możemy go użyć po prawej stronie instrukcji let, aby przypisać wynik do zmiennej, jak w Listingu 3-2.

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("Wartość liczby to: {number}");
}

Zmienna number zostanie związana z wartością na podstawie wyniku wyrażenia if. Uruchom ten kod, aby zobaczyć, co się stanie:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
Wartość liczby to: 5

Pamiętaj, że bloki kodu oceniają się do ostatniego w nich wyrażenia, a same liczby są również wyrażeniami. W tym przypadku wartość całego wyrażenia if zależy od tego, który blok kodu zostanie wykonany. Oznacza to, że wartości, które potencjalnie mogą być wynikami z każdego ramienia if, muszą być tego samego typu; w Listingu 3-2 wyniki zarówno ramienia if, jak i ramienia else były liczbami całkowitymi i32. Jeśli typy nie pasują do siebie, jak w następującym przykładzie, otrzymamy błąd:

Nazwa pliku: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "sześć" };

    println!("Wartość liczby to: {number}");
}

Kiedy spróbujemy skompilować ten kod, otrzymamy błąd. Ramiona if i else mają niezgodne typy wartości, a Rust dokładnie wskazuje, gdzie znaleźć problem w programie:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Wyrażenie w bloku if oblicza się do liczby całkowitej, a wyrażenie w bloku else oblicza się do ciągu znaków. To nie zadziała, ponieważ zmienne muszą mieć pojedynczy typ, a Rust musi wiedzieć jednoznacznie w czasie kompilacji, jakiego typu jest zmienna number. Znajomość typu number pozwala kompilatorowi sprawdzić, czy typ jest prawidłowy wszędzie tam, gdzie używamy number. Rust nie byłby w stanie tego zrobić, gdyby typ number był określany tylko w czasie działania; kompilator byłby bardziej złożony i dawałby mniej gwarancji co do kodu, gdyby musiał śledzić wiele hipotetycznych typów dla dowolnej zmiennej.

Powtarzanie z pętlami

Często przydatne jest wielokrotne wykonanie bloku kodu. Do tego zadania Rust udostępnia kilka pętli, które będą wykonywać kod wewnątrz ciała pętli do końca, a następnie natychmiast wrócą na początek. Aby poeksperymentować z pętlami, utwórzmy nowy projekt o nazwie loops.

Rust ma trzy rodzaje pętli: loop, while i for. Spróbujmy każdej z nich.

Powtarzanie kodu za pomocą loop

Słowo kluczowe loop mówi Rustowi, aby wykonywał blok kodu w kółko, albo w nieskończoność, albo dopóki jawnie nie powiesz mu, aby się zatrzymał.

Jako przykład, zmień plik src/main.rs w katalogu loops tak, aby wyglądał tak:

Nazwa pliku: src/main.rs

fn main() {
    loop {
        println!("znowu!");
    }
}

Kiedy uruchomimy ten program, będziemy widzieć znowu! wypisywane w kółko nieprzerwanie, dopóki nie zatrzymamy programu ręcznie. Większość terminali obsługuje skrót klawiaturowy ctrl-C do przerwania programu, który utknął w nieustannej pętli. Spróbuj:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
znowu!
znowu!
znowu!
znowu!
^Cznowu!

Symbol ^C reprezentuje miejsce, w którym naciśnąłeś ctrl-C.

Możesz, ale nie musisz, zobaczyć słowo znowu! wypisane po ^C, w zależności od tego, w którym miejscu pętli znajdował się kod, gdy otrzymał sygnał przerwania.

Na szczęście Rust udostępnia również sposób na wyjście z pętli za pomocą kodu. Możesz umieścić słowo kluczowe break w pętli, aby powiedzieć programowi, kiedy ma zakończyć wykonywanie pętli. Przypomnij sobie, że zrobiliśmy to w grze w zgadywanie w sekcji „Zakończenie po poprawnym odgadnięciu” w Rozdziale 2, aby zakończyć program, gdy użytkownik wygrał grę, zgadując poprawną liczbę.

Używaliśmy również continue w grze w zgadywanie, co w pętli mówi programowi, aby pominął pozostały kod w tej iteracji pętli i przeszedł do następnej iteracji.

Zwracanie wartości z pętli

Jednym z zastosowań pętli loop jest ponowne wykonanie operacji, o której wiesz, że może się nie powieść, na przykład sprawdzenie, czy wątek zakończył swoje zadanie. Może być również konieczne przekazanie wyniku tej operacji poza pętlę do reszty kodu. Aby to zrobić, możesz dodać wartość, którą chcesz zwrócić, po wyrażeniu break, którego używasz do zatrzymania pętli; ta wartość zostanie zwrócona z pętli, abyś mógł jej użyć, jak pokazano tutaj:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("Wynik to {result}");
}

Przed pętlą deklarujemy zmienną counter i inicjalizujemy ją na 0. Następnie deklarujemy zmienną result, aby przechowywała wartość zwróconą z pętli. W każdej iteracji pętli dodajemy 1 do zmiennej counter, a następnie sprawdzamy, czy counter jest równe 10. Gdy tak jest, używamy słowa kluczowego break z wartością counter * 2. Po pętli używamy średnika, aby zakończyć instrukcję, która przypisuje wartość do result. Na koniec wypisujemy wartość w result, która w tym przypadku wynosi 20.

Możesz również return z wnętrza pętli. Podczas gdy break tylko wychodzi z bieżącej pętli, return zawsze wychodzi z bieżącej funkcji.

Rozróżnianie wielu pętli za pomocą etykiet pętli

Jeśli masz pętle zagnieżdżone w innych pętlach, break i continue dotyczą aktualnie najbardziej wewnętrznej pętli. Możesz opcjonalnie określić etykietę pętli dla pętli, którą następnie możesz użyć z break lub continue, aby określić, że te słowa kluczowe dotyczą oznaczonej pętli, a nie najbardziej wewnętrznej. Etykiety pętli muszą zaczynać się od pojedynczego cudzysłowu. Oto przykład z dwoma zagnieżdżonymi pętlami:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("licznik = {count}");
        let mut remaining = 10;

        loop {
            println!("pozostało = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("Końcowy licznik = {count}");
}

Zewnętrzna pętla ma etykietę 'counting_up i będzie liczyć od 0 do 2. Wewnętrzna pętla bez etykiety liczy od 10 do 9. Pierwszy break, który nie określa etykiety, zakończy tylko wewnętrzną pętlę. Instrukcja break 'counting_up; zakończy zewnętrzną pętlę. Ten kod wypisuje:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
licznik = 0
pozostało = 10
pozostało = 9
licznik = 1
pozostało = 10
pozostało = 9
licznik = 2
pozostało = 10
Końcowy licznik = 2

Upraszczanie pętli warunkowych za pomocą while

Program często musi oceniać warunek wewnątrz pętli. Dopóki warunek jest true, pętla działa. Gdy warunek przestaje być true, program wywołuje break, zatrzymując pętlę. Możliwe jest zaimplementowanie takiego zachowania za pomocą połączenia loop, if, else i break; możesz spróbować tego teraz w programie, jeśli chcesz. Jednak ten wzorzec jest tak powszechny, że Rust ma wbudowaną konstrukcję językową dla niego, zwaną pętlą while. W Listingu 3-3 używamy while, aby zapętlić program trzy razy, odliczając za każdym razem, a następnie, po pętli, wypisać wiadomość i zakończyć działanie.

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("START!!!");
}

Ta konstrukcja eliminuje wiele zagnieżdżeń, które byłyby konieczne, gdybyś używał loop, if, else i break, i jest bardziej przejrzysta. Dopóki warunek ocenia się na true, kod działa; w przeciwnym razie wychodzi z pętli.

Przeglądanie kolekcji za pomocą for

Możesz użyć konstrukcji while do iteracji po elementach kolekcji, takiej jak tablica. Na przykład, pętla w Listingu 3-4 wypisuje każdy element w tablicy a.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("wartość to: {}", a[index]);

        index += 1;
    }
}

Tutaj kod liczy elementy w tablicy. Zaczyna od indeksu 0, a następnie zapętla się, aż osiągnie ostatni indeks w tablicy (czyli, gdy index < 5 przestaje być true). Uruchomienie tego kodu wypisze każdy element w tablicy:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
wartość to: 10
wartość to: 20
wartość to: 30
wartość to: 40
wartość to: 50

Wszystkie pięć wartości tablicy pojawia się w terminalu, zgodnie z oczekiwaniami. Mimo że index osiągnie wartość 5 w pewnym momencie, pętla przestaje się wykonywać przed próbą pobrania szóstej wartości z tablicy.

Jednak to podejście jest podatne na błędy; moglibyśmy spowodować panikę programu, jeśli wartość indeksu lub warunek testowy są niepoprawne. Na przykład, jeśli zmieniłeś definicję tablicy a na cztery elementy, ale zapomniałeś zaktualizować warunek na while index < 4, kod uległby panice. Jest to również wolne, ponieważ kompilator dodaje kod wykonawczy do wykonywania kontroli warunkowej, czy indeks znajduje się w granicach tablicy w każdej iteracji pętli.

Jako bardziej zwięzłą alternatywę, możesz użyć pętli for i wykonać kod dla każdego elementu w kolekcji. Pętla for wygląda jak kod w Listingu 3-5.

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("wartość to: {element}");
    }
}

Kiedy uruchomimy ten kod, zobaczymy ten sam wynik co w Listingu 3-4. Co ważniejsze, zwiększyliśmy teraz bezpieczeństwo kodu i wyeliminowaliśmy możliwość błędów, które mogłyby wyniknąć z wyjścia poza koniec tablicy lub niewystarczającego przeszukania i pominięcia niektórych elementów. Kod maszynowy generowany z pętli for może być również bardziej wydajny, ponieważ indeks nie musi być porównywany z długością tablicy w każdej iteracji.

Używając pętli for, nie musiałbyś pamiętać o zmienianiu żadnego innego kodu, gdybyś zmienił liczbę wartości w tablicy, tak jak to miało miejsce w metodzie użytej w Listingu 3-4.

Bezpieczeństwo i zwięzłość pętli for sprawiają, że są one najczęściej używaną konstrukcją pętli w Ruście. Nawet w sytuacjach, gdy chcesz uruchomić jakiś kod określoną liczbę razy, jak w przykładzie odliczania, który używał pętli while w Listingu 3-3, większość Rustaceanów użyłaby pętli for. Sposób na to polegałby na użyciu Range, dostarczanego przez bibliotekę standardową, który generuje wszystkie liczby w sekwencji, zaczynając od jednej liczby i kończąc przed inną liczbą.

Oto jak wyglądałoby odliczanie za pomocą pętli for i innej metody, o której jeszcze nie mówiliśmy, rev, do odwracania zakresu:

Nazwa pliku: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("START!!!");
}

Ten kod jest trochę ładniejszy, prawda?

Podsumowanie

Udało się! Dotarłeś do końca obszernego rozdziału: nauczyłeś się o zmiennych, skalarnych i złożonych typach danych, funkcjach, komentarzach, wyrażeniach if i pętlach! Aby poćwiczyć koncepcje omówione w tym rozdziale, spróbuj zbudować programy, aby wykonać następujące zadania:

  • Konwertowanie temperatur między stopniami Fahrenheita i Celsjusza.
  • Generowanie n-tej liczby Fibonacciego.
  • Wypisywanie tekstu kolędy „The Twelve Days of Christmas”, wykorzystując powtórzenia w piosence.

Kiedy będziesz gotowy, przejdziemy do koncepcji w Ruście, która nie występuje powszechnie w innych językach programowania: własności.