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

Konstrukcja kontroli przepływu match

Rust ma niezwykle potężną konstrukcję kontroli przepływu o nazwie match, która pozwala porównywać wartość z serią wzorców, a następnie wykonywać kod na podstawie pasującego wzorca. Wzorce mogą składać się z literałów, nazw zmiennych, symboli wieloznacznych i wielu innych rzeczy; Rozdział 19 obejmuje wszystkie różne rodzaje wzorców i to, co robią. Siła match pochodzi z ekspresywności wzorców i faktu, że kompilator potwierdza, że wszystkie możliwe przypadki są obsługiwane.

Pomyśl o wyrażeniu match jak o maszynie do sortowania monet: monety zjeżdżają po torze z otworami o różnej wielkości, a każda moneta wpada przez pierwszy otwór, do którego pasuje. W ten sam sposób wartości przechodzą przez każdy wzorzec w match, a przy pierwszym wzorcu, do którego wartość „pasuje”, wartość wpada do skojarzonego bloku kodu, aby zostać użyta podczas wykonania.

Skoro mowa o monetach, użyjmy ich jako przykładu z match! Możemy napisać funkcję, która przyjmuje nieznaną monetę amerykańską i, podobnie jak maszyna licząca, określa, jaka to moneta i zwraca jej wartość w centach, jak pokazano w Listingu 6-3.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Rozłóżmy match w funkcji value_in_cents. Najpierw wymieniamy słowo kluczowe match, po którym następuje wyrażenie, które w tym przypadku jest wartością coin. To wydaje się bardzo podobne do wyrażenia warunkowego używanego z if, ale jest duża różnica: z if warunek musi ewaluować do wartości boolowskiej, ale tutaj może to być dowolny typ. Typ coin w tym przykładzie to enum Coin, który zdefiniowaliśmy w pierwszej linii.

Następne są ramiona match. Ramka składa się z dwóch części: wzorca i pewnego kodu. Pierwsza ramka ma wzorzec, którym jest wartość Coin::Penny, a następnie operator =>, który oddziela wzorzec od kodu do uruchomienia. Kod w tym przypadku to po prostu wartość 1. Każda ramka jest oddzielona od następnej przecinkiem.

Kiedy wyrażenie match jest wykonywane, porównuje ono wynikową wartość z wzorcem każdej gałęzi, po kolei. Jeśli wzorzec pasuje do wartości, kod skojarzony z tym wzorcem jest wykonywany. Jeśli ten wzorzec nie pasuje do wartości, wykonanie przechodzi do następnej gałęzi, podobnie jak w maszynie do sortowania monet. Możemy mieć tyle gałęzi, ile potrzebujemy: w Listingu 6-3, nasz match ma cztery gałęzie.

Kod skojarzony z każdym ramieniem jest wyrażeniem, a wynikowa wartość wyrażenia w pasującym ramieniu jest wartością, która jest zwracana dla całego wyrażenia match.

Zazwyczaj nie używamy nawiasów klamrowych, jeśli kod gałęzi match jest krótki, jak w Listingu 6-3, gdzie każda gałąź po prostu zwraca wartość. Jeśli chcesz uruchomić wiele linii kodu w gałęzi match, musisz użyć nawiasów klamrowych, a przecinek po gałęzi jest wtedy opcjonalny. Na przykład, poniższy kod drukuje „Lucky penny!” za każdym razem, gdy metoda jest wywoływana z Coin::Penny, ale nadal zwraca ostatnią wartość bloku, 1:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Szczęśliwy grosz!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Wzorce, które wiążą się z wartościami

Inną przydatną cechą ramion match jest to, że mogą one wiązać się z częściami wartości, które pasują do wzorca. W ten sposób możemy wyodrębniać wartości z wariantów enum.

Na przykład, zmieńmy jeden z naszych wariantów enum, aby przechowywał w sobie dane. Od 1999 do 2008 roku Stany Zjednoczone biły ćwierćdolarówki z różnymi wzorami dla każdego z 50 stanów po jednej stronie. Żadne inne monety nie otrzymały wzorów stanów, więc tylko ćwierćdolarówki mają tę dodatkową wartość. Możemy dodać tę informację do naszego enum, zmieniając wariant Quarter tak, aby zawierał wartość UsState przechowywaną w środku, co zrobiliśmy w Listingu 6-4.

#[derive(Debug)] // abyśmy mogli za chwilę sprawdzić stan
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

Wyobraźmy sobie, że przyjaciel próbuje zebrać wszystkie 50 monet stanowych. Podczas gdy my sortujemy naszą drobną monetę według typu monety, będziemy również wywoływać nazwę stanu związanego z każdą monetą, tak aby jeśli jest to moneta, której nasz przyjaciel nie ma, mógł ją dodać do swojej kolekcji.

W wyrażeniu match dla tego kodu dodajemy zmienną o nazwie state do wzorca, który pasuje do wartości wariantu Coin::Quarter. Gdy pasuje Coin::Quarter, zmienna state zostanie powiązana z wartością stanu tej ćwiartki. Następnie możemy użyć state w kodzie dla tego ramienia, w ten sposób:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("Moneta stanowa z {state:?}!");
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

Gdybyśmy wywołali value_in_cents(Coin::Quarter(UsState::Alaska)), coin byłby Coin::Quarter(UsState::Alaska). Kiedy porównamy tę wartość z każdym z ramion match, żadne z nich nie pasuje, dopóki nie dotrzemy do Coin::Quarter(state). W tym momencie wiązanie dla state będzie wartością UsState::Alaska. Możemy następnie użyć tego wiązania w wyrażeniu println!, uzyskując w ten sposób wewnętrzną wartość stanu z wariantu enum Coin dla Quarter.

Wzorzec Option<T> match

W poprzedniej sekcji chcieliśmy wydobyć wewnętrzną wartość T z przypadku Some podczas używania Option<T>; możemy również obsługiwać Option<T> za pomocą match, tak jak zrobiliśmy to z enumem Coin! Zamiast porównywać monety, będziemy porównywać warianty Option<T>, ale sposób działania wyrażenia match pozostaje taki sam.

Powiedzmy, że chcemy napisać funkcję, która przyjmuje Option<i32> i, jeśli w środku jest wartość, dodaje do niej 1. Jeśli w środku nie ma wartości, funkcja powinna zwrócić wartość None i nie próbować wykonywać żadnych operacji.

Tę funkcję bardzo łatwo napisać, dzięki match, i będzie ona wyglądać jak Listing 6-5.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Przyjrzyjmy się dokładniej pierwszemu wykonaniu plus_one. Kiedy wywołujemy plus_one(five), zmienna x w ciele plus_one będzie miała wartość Some(5). Następnie porównujemy ją z każdym ramieniem match:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Wartość Some(5) nie pasuje do wzorca None, więc przechodzimy do następnego ramienia:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Czy Some(5) pasuje do Some(i)? Tak! Mamy ten sam wariant. i wiąże się z wartością zawartą w Some, więc i przyjmuje wartość 5. Następnie kod w ramieniu match jest wykonywany, więc dodajemy 1 do wartości i i tworzymy nową wartość Some z naszym łącznym 6 w środku.

Rozważmy teraz drugie wywołanie plus_one w Listingu 6-5, gdzie x jest None. Wchodzimy do match i porównujemy z pierwszym ramieniem:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Pasuje! Nie ma wartości do dodania, więc program zatrzymuje się i zwraca wartość None po prawej stronie =>. Ponieważ pierwsze ramię pasowało, żadne inne ramiona nie są porównywane.

Łączenie match i enumów jest przydatne w wielu sytuacjach. Będziesz często widział ten wzorzec w kodzie Rust: match z enumem, powiązanie zmiennej z danymi wewnątrz, a następnie wykonanie kodu na tej podstawie. Na początku jest to trochę trudne, ale gdy się do tego przyzwyczaisz, będziesz żałował, że nie miałeś tego we wszystkich językach. Jest to konsekwentnie ulubiona funkcja użytkowników.

Dopasowania są wyczerpujące

Jest jeszcze jeden aspekt match, który musimy omówić: wzorce ramion muszą obejmować wszystkie możliwości. Rozważmy tę wersję naszej funkcji plus_one, która zawiera błąd i nie skompiluje się:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Nie obsłużyliśmy przypadku None, więc ten kod spowoduje błąd. Na szczęście jest to błąd, który Rust potrafi wyłapać. Jeśli spróbujemy skompilować ten kod, otrzymamy taki błąd:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` nie pokryto
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ wzorzec `None` nie pokryto
  |
note: `Option<i32>` zdefiniowano tutaj
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: nie pokryto
  = note: dopasowana wartość jest typu `Option<i32>`
help: upewnij się, że wszystkie możliwe przypadki są obsługiwane, dodając ramię dopasowania z wzorcem wieloznacznym lub jawnym wzorcem, jak pokazano
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

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

Rust wie, że nie objęliśmy każdego możliwego przypadku i nawet wie, który wzorzec pominęliśmy! Dopasowania w Rust są wyczerpujące: Musimy wyczerpać każdą ostatnią możliwość, aby kod był poprawny. Zwłaszcza w przypadku Option<T>, gdy Rust uniemożliwia nam zapomnienie o jawnej obsłudze przypadku None, chroni nas przed zakładaniem, że mamy wartość, gdy możemy mieć null, co uniemożliwia popełnienie błędu miliarda dolarów, o którym mówiliśmy wcześniej.

Wzorce ogólne (Catch-All) i symbol zastępczy _

Używając typów wyliczeniowych, możemy również podejmować specjalne działania dla kilku konkretnych wartości, ale dla wszystkich innych wartości podjąć jedno domyślne działanie. Wyobraźmy sobie, że implementujemy grę, w której, jeśli na rzucie kostką wypadnie 3, gracz nie rusza się, ale zamiast tego dostaje fantazyjny nowy kapelusz. Jeśli wypadnie 7, gracz traci fantazyjny kapelusz. Dla wszystkich innych wartości, gracz przesuwa się o tę liczbę pól na planszy. Oto match, który implementuje tę logikę, z wynikiem rzutu kostką zakodowanym na stałe, a nie wartością losową, a cała inna logika jest reprezentowana przez funkcje bez ciał, ponieważ ich faktyczne zaimplementowanie wykracza poza zakres tego przykładu:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

Dla pierwszych dwóch gałęzi wzorcami są literały 3 i 7. Dla ostatniej gałęzi, która obejmuje wszystkie inne możliwe wartości, wzorcem jest zmienna, którą nazwaliśmy other. Kod uruchamiany dla gałęzi other używa zmiennej, przekazując ją do funkcji move_player.

Ten kod kompiluje się, chociaż nie wymieniliśmy wszystkich możliwych wartości, jakie może mieć u8, ponieważ ostatni wzorzec będzie pasował do wszystkich wartości, które nie zostały specjalnie wymienione. Ten wzorzec typu catch-all spełnia wymóg, że match musi być wyczerpujące. Zauważ, że musimy umieścić ramię typu catch-all na końcu, ponieważ wzorce są oceniane po kolei. Gdybyśmy umieścili ramię typu catch-all wcześniej, inne ramiona nigdy by się nie uruchomiły, więc Rust ostrzeże nas, jeśli dodamy ramiona po catch-all!

Rust posiada również wzorzec, którego możemy użyć, gdy chcemy zastosować wzorzec ogólny, ale nie chcemy używać wartości w tym wzorcu: _ to specjalny wzorzec, który pasuje do dowolnej wartości i nie wiąże się z tą wartością. To mówi Rustowi, że nie będziemy używać tej wartości, więc Rust nie ostrzeże nas o nieużywanej zmiennej.

Zmieńmy zasady gry: teraz, jeśli wyrzucisz coś innego niż 3 lub 7, musisz rzucić ponownie. Nie potrzebujemy już używać wartości typu catch-all, więc możemy zmienić nasz kod, aby używał _ zamiast zmiennej o nazwie other:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

Ten przykład również spełnia wymóg wyczerpującego dopasowania, ponieważ jawnie ignorujemy wszystkie inne wartości w ostatnim ramieniu; niczego nie pominęliśmy.

Na koniec zmienimy zasady gry jeszcze raz, tak aby nic innego nie działo się w Twojej turze, jeśli wyrzucisz coś innego niż 3 lub 7. Możemy to wyrazić, używając wartości jednostkowej (typu pustej krotki, o której wspominaliśmy w sekcji „Typ krotki”) jako kodu, który towarzyszy ramieniu _:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

Tutaj jawnie mówimy Rustowi, że nie zamierzamy używać żadnej innej wartości, która nie pasuje do wzorca w poprzednim ramieniu, i nie chcemy uruchamiać żadnego kodu w tym przypadku.

Więcej o wzorcach i dopasowywaniu omówimy w Rozdziale 19. Na razie przejdziemy do składni if let, która może być użyteczna w sytuacjach, gdy wyrażenie match jest nieco rozwlekłe.