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.