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

Składnia Wzorców

W tej sekcji zbieramy całą składnię, która jest prawidłowa we wzorcach, i omawiamy, dlaczego i kiedy warto używać każdej z nich.

Dopasowywanie Literałów

Jak widziałeś w Rozdziale 6, możesz dopasowywać wzorce bezpośrednio do literałów. Poniższy kod przedstawia kilka przykładów:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Ten kod wypisuje one, ponieważ wartość w x wynosi 1. Ta składnia jest użyteczna, gdy chcesz, aby Twój kod podjął działanie, jeśli otrzyma konkretną wartość.

Dopasowywanie Nazwanych Zmiennych

Nazwane zmienne to nieodrzucalne wzorce, które pasują do dowolnej wartości, i używaliśmy ich wiele razy w tej książce. Jednakże, pojawia się komplikacja, gdy używasz nazwanych zmiennych w wyrażeniach match, if let lub while let. Ponieważ każdy z tych rodzajów wyrażeń rozpoczyna nowy zakres, zmienne zadeklarowane jako część wzorca wewnątrz tych wyrażeń będą zasłaniać te o tej samej nazwie poza konstrukcjami, tak jak to ma miejsce w przypadku wszystkich zmiennych. Na Liście 19-11 deklarujemy zmienną x o wartości Some(5) i zmienną y o wartości 10. Następnie tworzymy wyrażenie match na wartości x. Spójrz na wzorce w ramionach match i println! na końcu, a spróbuj odgadnąć, co kod wydrukuje, zanim uruchomisz ten kod lub przeczytasz dalej.

Nazwa pliku: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Lista 19-11: Wyrażenie match z ramieniem, które wprowadza nową zmienną, która zasłania istniejącą zmienną y

Przeanalizujmy, co dzieje się, gdy uruchamia się wyrażenie match. Wzorzec w pierwszym ramieniu match nie pasuje do zdefiniowanej wartości x, więc kod kontynuuje działanie.

Wzorzec w drugim ramieniu match wprowadza nową zmienną o nazwie y, która będzie pasować do dowolnej wartości wewnątrz wartości Some. Ponieważ jesteśmy w nowym zakresie wewnątrz wyrażenia match, jest to nowa zmienna y, a nie y, którą zadeklarowaliśmy na początku z wartością 10. To nowe powiązanie y będzie pasować do wewnętrznej wartości Some w x. Ta wartość to 5, więc wyrażenie dla tego ramienia wykonuje się i wypisuje Matched, y = 5.

Gdyby x było wartością None zamiast Some(5), wzorce w dwóch pierwszych ramionach nie pasowałyby, więc wartość pasowałaby do podkreślenia. Nie wprowadziliśmy zmiennej x do wzorca ramienia podkreślenia, więc x w wyrażeniu jest nadal zewnętrznym x, które nie zostało zacienione. W tym hipotetycznym przypadku match wypisałby Default case, x = None.

Po zakończeniu wyrażenia match, jego zakres się kończy, podobnie jak zakres wewnętrznego y. Ostatnie println! wypisuje at the end: x = Some(5), y = 10.

Aby stworzyć wyrażenie match, które porównuje wartości zewnętrznych x i y, zamiast wprowadzać nową zmienną, która zasłania istniejącą zmienną y, musielibyśmy użyć warunkowego ograniczenia dopasowania. O match guardach porozmawiamy później w sekcji „Dodawanie warunków za pomocą match guardów”.

Dopasowywanie Wielu Wzorców

W wyrażeniach match możesz dopasowywać wiele wzorców za pomocą składni |, która jest operatorem lub wzorca. Na przykład, w poniższym kodzie dopasowujemy wartość x do ramion match, z których pierwsze ma opcję lub, co oznacza, że jeśli wartość x pasuje do którejkolwiek z wartości w tym ramieniu, kod tego ramienia zostanie uruchomiony:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Ten kod wypisuje one or two.

Dopasowywanie Zakresów Wartości za pomocą ..=

Składnia ..= pozwala nam dopasowywać do włącznie zakresu wartości. W poniższym kodzie, gdy wzorzec pasuje do którejkolwiek z wartości w danym zakresie, to ramię zostanie wykonane:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

Jeśli x wynosi 1, 2, 3, 4 lub 5, pierwsze ramię zostanie dopasowane. Ta składnia jest wygodniejsza dla wielu wartości dopasowania niż używanie operatora | do wyrażenia tej samej idei; gdybyśmy mieli użyć |, musielibyśmy określić 1 | 2 | 3 | 4 | 5. Określanie zakresu jest znacznie krótsze, zwłaszcza jeśli chcemy dopasować, powiedzmy, dowolną liczbę od 1 do 1000!

Kompilator sprawdza w czasie kompilacji, czy zakres nie jest pusty, a ponieważ jedynymi typami, dla których Rust może stwierdzić, czy zakres jest pusty, są char i wartości liczbowe, zakresy są dozwolone tylko z wartościami liczbowymi lub char.

Oto przykład użycia zakresów wartości char:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Rust może stwierdzić, że 'c' znajduje się w zakresie pierwszego wzorca i wypisuje early ASCII letter.

Dekonstrukcja w Celu Rozbicia Wartości

Możemy również używać wzorców do dekonstrukcji struktur, wyliczeń i krotek, aby używać różnych części tych wartości. Przejdźmy przez każdą wartość.

Struktury

Lista 19-12 pokazuje strukturę Point z dwoma polami, x i y, które możemy rozdzielić za pomocą wzorca z instrukcją let.

Nazwa pliku: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}
Lista 19-12: Dekonstrukcja pól struktury na osobne zmienne

Ten kod tworzy zmienne a i b, które odpowiadają wartościom pól x i y struktury p. Ten przykład pokazuje, że nazwy zmiennych we wzorcu nie muszą odpowiadać nazwom pól struktury. Jednak często dopasowuje się nazwy zmiennych do nazw pól, aby łatwiej było zapamiętać, które zmienne pochodzą z których pól. Z powodu tego powszechnego użycia i ponieważ pisanie let Point { x: x, y: y } = p; zawiera wiele powtórzeń, Rust ma skrót dla wzorców, które dopasowują pola struktury: wystarczy wymienić nazwę pola struktury, a zmienne utworzone na podstawie wzorca będą miały te same nazwy. Lista 19-13 działa tak samo jak kod z Listy 19-12, ale zmienne utworzone we wzorcu let to x i y zamiast a i b.

Nazwa pliku: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}
Lista 19-13: Dekonstrukcja pól struktury za pomocą skrótu pól struktury

Ten kod tworzy zmienne x i y, które pasują do pól x i y zmiennej p. Wynikiem jest to, że zmienne x i y zawierają wartości ze struktury p.

Możemy również dokonywać dekonstrukcji z użyciem wartości literałowych jako części wzorca struktury, zamiast tworzyć zmienne dla wszystkich pól. Pozwala nam to testować niektóre pola pod kątem określonych wartości, jednocześnie tworząc zmienne do dekonstrukcji pozostałych pól.

Na Liście 19-14 mamy wyrażenie match, które dzieli wartości Point na trzy przypadki: punkty leżące bezpośrednio na osi x (co jest prawdą, gdy y = 0), na osi y (x = 0) lub na żadnej z osi.

Nazwa pliku: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}
Lista 19-14: Dekonstrukcja i dopasowywanie wartości literałowych w jednym wzorcu

Pierwsze ramię dopasuje każdy punkt leżący na osi x, określając, że pole y pasuje, jeśli jego wartość odpowiada literałowi 0. Wzorzec nadal tworzy zmienną x, której możemy użyć w kodzie dla tego ramienia.

Podobnie, drugie ramię pasuje do każdego punktu na osi y, określając, że pole x pasuje, jeśli jego wartość wynosi 0, i tworzy zmienną y dla wartości pola y. Trzecie ramię nie określa żadnych literałów, więc pasuje do każdego innego Point i tworzy zmienne dla pól x i y.

W tym przykładzie wartość p pasuje do drugiego ramienia dzięki temu, że x zawiera 0, więc ten kod wypisze On the y axis at 7.

Pamiętaj, że wyrażenie match przestaje sprawdzać ramiona, gdy tylko znajdzie pierwszy pasujący wzorzec, więc nawet jeśli Point { x: 0, y: 0 } znajduje się na osi x i osi y, ten kod wydrukowałby tylko On the x axis at 0.

Wyliczenia (Enums)

Dekonstruowaliśmy wyliczenia w tej książce (na przykład Lista 6-5 w Rozdziale 6), ale nie omówiliśmy jeszcze wyraźnie, że wzorzec do dekonstrukcji wyliczenia odpowiada sposobowi definiowania danych przechowywanych w wyliczeniu. Jako przykład, na Liście 19-15 używamy wyliczenia Message z Listy 6-2 i piszemy match z wzorcami, które dekonstruują każdą wewnętrzną wartość.

Nazwa pliku: src/main.rs
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}
Lista 19-15: Dekonstrukcja wariantów wyliczenia, które zawierają różne rodzaje wartości

Ten kod wypisze Change color to red 0, green 160, and blue 255. Spróbuj zmienić wartość msg, aby zobaczyć, jak działa kod z innych ramion.

Dla wariantów wyliczeniowych bez żadnych danych, takich jak Message::Quit, nie możemy dalej dekonstruować wartości. Możemy dopasować tylko literał Message::Quit, a w tym wzorcu nie ma żadnych zmiennych.

Dla wariantów wyliczeń podobnych do struktur, takich jak Message::Move, możemy użyć wzorca podobnego do wzorca, który określamy w celu dopasowania struktur. Po nazwie wariantu umieszczamy nawiasy klamrowe, a następnie wymieniamy pola ze zmiennymi, tak abyśmy rozdzielili elementy do użycia w kodzie dla tego ramienia. Tutaj używamy skróconej formy, tak jak na Liście 19-13.

Dla wariantów wyliczeniowych typu krotka, takich jak Message::Write, które przechowuje krotkę z jednym elementem, oraz Message::ChangeColor, które przechowuje krotkę z trzema elementami, wzorzec jest podobny do wzorca, który określamy, aby dopasować krotki. Liczba zmiennych we wzorcu musi odpowiadać liczbie elementów w wariancie, który dopasowujemy.

Zagnieżdżone Struktury i Wyliczenia

Do tej pory wszystkie nasze przykłady dotyczyły dopasowywania struktur lub wyliczeń na jednym poziomie, ale dopasowywanie może działać również na zagnieżdżonych elementach! Na przykład, możemy refaktoryzować kod z Listy 19-15, aby obsługiwał kolory RGB i HSV w wiadomości ChangeColor, jak pokazano na Liście 19-16.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}
Lista 19-16: Dopasowywanie zagnieżdżonych wyliczeń

Wzorzec pierwszego ramienia w wyrażeniu match pasuje do wariantu wyliczeniowego Message::ChangeColor, który zawiera wariant Color::Rgb; następnie wzorzec wiąże się z trzema wewnętrznymi wartościami i32. Wzorzec drugiego ramienia również pasuje do wariantu wyliczeniowego Message::ChangeColor, ale wewnętrzne wyliczenie pasuje zamiast tego do Color::Hsv. Możemy określić te złożone warunki w jednym wyrażeniu match, mimo że biorą w nim udział dwa wyliczenia.

Struktury i Krotki

Możemy mieszać, dopasowywać i zagnieżdżać wzorce dekonstrukcji na jeszcze bardziej złożone sposoby. Poniższy przykład pokazuje skomplikowaną dekonstrukcję, w której zagnieżdżamy struktury i krotki wewnątrz krotki i dekonstruujemy wszystkie wartości pierwotne:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

Ten kod pozwala nam rozbić złożone typy na ich części składowe, abyśmy mogli osobno używać wartości, które nas interesują.

Dekonstrukcja za pomocą wzorców to wygodny sposób na używanie części wartości, takich jak wartość z każdego pola w strukturze, oddzielnie od siebie.

Ignorowanie Wartości we Wzorcu

Widziałeś, że czasami przydatne jest ignorowanie wartości we wzorcu, na przykład w ostatnim ramieniu match, aby uzyskać catch-all, który faktycznie nic nie robi, ale uwzględnia wszystkie pozostałe możliwe wartości. Istnieje kilka sposobów ignorowania całych wartości lub części wartości we wzorcu: użycie wzorca _ (który już widziałeś), użycie wzorca _ w innym wzorcu, użycie nazwy zaczynającej się od podkreślenia lub użycie .., aby zignorować pozostałe części wartości. Przyjrzyjmy się, jak i dlaczego używać każdego z tych wzorców.

Cała Wartość za pomocą _

Używaliśmy podkreślenia jako wzorca wieloznacznego, który będzie pasował do dowolnej wartości, ale nie będzie wiązał się z wartością. Jest to szczególnie przydatne jako ostatnie ramię w wyrażeniu match, ale możemy go również używać w dowolnym wzorcu, w tym w parametrach funkcji, jak pokazano na Liście 19-17.

Nazwa pliku: src/main.rs
fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}
Lista 19-17: Użycie _ w sygnaturze funkcji

Ten kod całkowicie zignoruje wartość 3 przekazaną jako pierwszy argument i wydrukuje This code only uses the y parameter: 4.

W większości przypadków, gdy nie potrzebujesz już konkretnego parametru funkcji, zmieniłbyś sygnaturę tak, aby nie zawierała nieużywanego parametru. Ignorowanie parametru funkcji może być szczególnie przydatne w przypadkach, gdy na przykład implementujesz cechę, gdy potrzebujesz określonej sygnatury typu, ale ciało funkcji w Twojej implementacji nie potrzebuje jednego z parametrów. Wtedy unikasz ostrzeżenia kompilatora o nieużywanych parametrach funkcji, tak jakbyś użył nazwy zamiast.

Fragmenty Wartości z Zagnieżdżonym _

Możemy również używać _ wewnątrz innego wzorca, aby zignorować tylko część wartości, na przykład, gdy chcemy przetestować tylko część wartości, ale nie mamy zastosowania dla pozostałych części w odpowiadającym kodzie, który chcemy uruchomić. Lista 19-18 pokazuje kod odpowiedzialny za zarządzanie wartością ustawienia. Wymagania biznesowe są takie, że użytkownikowi nie wolno nadpisywać istniejącej dostosowanej wartości ustawienia, ale może anulować ustawienie i nadać mu wartość, jeśli jest ono obecnie niezdefiniowane.

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}
Lista 19-18: Użycie podkreślenia w wzorcach pasujących do wariantów Some, gdy nie musimy używać wartości wewnątrz Some

Ten kod wypisze Can't overwrite an existing customized value, a następnie setting is Some(5). W pierwszym ramieniu match nie musimy dopasowywać ani używać wartości wewnątrz żadnego z wariantów Some, ale musimy sprawdzić przypadek, gdy setting_value i new_setting_value są wariantem Some. W takim przypadku wypisujemy powód, dla którego setting_value nie zostanie zmienione, i nie zostanie ono zmienione.

We wszystkich innych przypadkach (jeśli setting_value lub new_setting_value jest None), wyrażonych wzorcem _ w drugim ramieniu, chcemy, aby new_setting_value stało się setting_value.

Możemy również używać podkreśleń w wielu miejscach w jednym wzorcu, aby ignorować określone wartości. Lista 19-19 pokazuje przykład ignorowania drugiej i czwartej wartości w krotce pięciu elementów.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}
Lista 19-19: Ignorowanie wielu części krotki

Ten kod wypisze Some numbers: 2, 8, 32, a wartości 4 i 16 zostaną zignorowane.

Nieużywana Zmienna, Zaczynająca Się od _

Jeśli utworzysz zmienną, ale nie użyjesz jej nigdzie, Rust zazwyczaj wyświetli ostrzeżenie, ponieważ nieużywana zmienna może być błędem. Czasami jednak przydatne jest stworzenie zmiennej, której jeszcze nie użyjesz, na przykład podczas prototypowania lub rozpoczynania projektu. W tej sytuacji możesz powiedzieć Rust, aby nie ostrzegał Cię o nieużywanej zmiennej, zaczynając nazwę zmiennej od podkreślenia. Na Liście 19-20 tworzymy dwie nieużywane zmienne, ale po skompilowaniu tego kodu powinniśmy otrzymać ostrzeżenie tylko o jednej z nich.

Nazwa pliku: src/main.rs
fn main() {
    let _x = 5;
    let y = 10;
}
Lista 19-20: Rozpoczynanie nazwy zmiennej od podkreślenia w celu uniknięcia ostrzeżeń o nieużywanych zmiennych

Tutaj otrzymujemy ostrzeżenie o nieużywaniu zmiennej y, ale nie otrzymujemy ostrzeżenia o nieużywaniu _x.

Zauważ, że istnieje subtelna różnica między używaniem samego _ a używaniem nazwy zaczynającej się od podkreślenia. Składnia _x nadal wiąże wartość ze zmienną, podczas gdy _ w ogóle nie wiąże. Aby pokazać przypadek, w którym ta różnica ma znaczenie, Lista 19-21 dostarczy nam błędu.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Lista 19-21: Nieużywana zmienna zaczynająca się od podkreślenia nadal wiąże wartość, co może przejąć własność wartości.

Otrzymamy błąd, ponieważ wartość s zostanie nadal przeniesiona do _s, co uniemożliwi nam ponowne użycie s. Jednak użycie samego podkreślenia nigdy nie wiąże się z wartością. Lista 19-22 skompiluje się bez błędów, ponieważ s nie zostanie przeniesione do _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Lista 19-22: Użycie podkreślenia nie wiąże wartości.

Ten kod działa bez zarzutu, ponieważ nigdy nie wiążemy s z niczym; nie zostaje przeniesione.

Pozostałe Części Wartości za pomocą ..

W przypadku wartości, które mają wiele części, możemy użyć składni .., aby użyć konkretnych części i zignorować resztę, unikając konieczności wymieniania podkreśleń dla każdej ignorowanej wartości. Wzorzec .. ignoruje wszystkie części wartości, których nie dopasowaliśmy jawnie w pozostałej części wzorca. Na Liście 19-23 mamy strukturę Point, która przechowuje współrzędną w trójwymiarowej przestrzeni. W wyrażeniu match chcemy operować tylko na współrzędnej x i ignorować wartości w polach y i z.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {x}"),
    }
}
Lista 19-23: Ignorowanie wszystkich pól Point oprócz x za pomocą ..

Wypisujemy wartość x, a następnie po prostu dodajemy wzorzec ... Jest to szybsze niż konieczność wypisywania y: _ i z: _, szczególnie gdy pracujemy ze strukturami, które mają wiele pól w sytuacjach, gdy tylko jedno lub dwa pola są istotne.

Składnia .. rozszerzy się do tylu wartości, ile potrzebuje. Lista 19-24 pokazuje, jak używać .. z krotką.

Nazwa pliku: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}
Lista 19-24: Dopasowywanie tylko pierwszej i ostatniej wartości w krotce i ignorowanie wszystkich pozostałych wartości

W tym kodzie pierwsza i ostatnia wartość są dopasowywane do first i last. .. dopasuje i zignoruje wszystko pośrodku.

Jednakże, używanie .. musi być jednoznaczne. Jeśli nie jest jasne, które wartości są przeznaczone do dopasowania, a które powinny zostać zignorowane, Rust zgłosi błąd. Lista 19-25 pokazuje przykład niejednoznacznego użycia .., dlatego nie skompiluje się.

Nazwa pliku: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}
Lista 19-25: Próba użycia .. w sposób niejednoznaczny

Kiedy skompilujemy ten przykład, otrzymamy ten błąd:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

Rust nie jest w stanie określić, ile wartości w krotce należy zignorować przed dopasowaniem wartości do second, a następnie ile dalszych wartości należy zignorować. Ten kod mógłby oznaczać, że chcemy zignorować 2, powiązać second z 4, a następnie zignorować 8, 16 i 32; albo że chcemy zignorować 2 i 4, powiązać second z 8, a następnie zignorować 16 i 32; i tak dalej. Nazwa zmiennej second nie oznacza niczego specjalnego dla Rust, więc otrzymujemy błąd kompilatora, ponieważ użycie .. w dwóch miejscach w ten sposób jest niejednoznaczne.

Dodawanie Warunków za pomocą Match Guardów

Match guard to dodatkowy warunek if, określony po wzorcu w ramieniu match, który również musi zostać spełniony, aby to ramię zostało wybrane. Match guardy są przydatne do wyrażania bardziej złożonych idei niż sam wzorzec. Zauważ jednak, że są one dostępne tylko w wyrażeniach match, a nie w wyrażeniach if let ani while let.

Warunek może używać zmiennych utworzonych we wzorcu. Lista 19-26 pokazuje match, gdzie pierwsze ramię ma wzorzec Some(x) i dodatkowo match guard if x % 2 == 0 (który będzie true, jeśli liczba jest parzysta).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}
Lista 19-26: Dodawanie match guarda do wzorca

Ten przykład wypisze The number 4 is even. Gdy num jest porównywane z wzorcem w pierwszym ramieniu, pasuje, ponieważ Some(4) pasuje do Some(x). Następnie match guard sprawdza, czy reszta z dzielenia x przez 2 jest równa 0, a ponieważ tak jest, wybrane zostaje pierwsze ramię.

Gdyby num było Some(5) zamiast tego, match guard w pierwszym ramieniu byłby false, ponieważ reszta z dzielenia 5 przez 2 wynosi 1, co nie jest równe 0. Rust następnie przeszedłby do drugiego ramienia, które by pasowało, ponieważ drugie ramię nie ma match guarda i dlatego pasuje do dowolnego wariantu Some.

Nie ma sposobu, aby wyrazić warunek if x % 2 == 0 w ramach wzorca, więc match guard daje nam możliwość wyrażenia tej logiki. Wadą tej dodatkowej ekspresywności jest to, że kompilator nie próbuje sprawdzać kompletności, gdy w grę wchodzą wyrażenia match guard.

Podczas omawiania Listy 19-11, wspomnieliśmy, że moglibyśmy użyć match guardów do rozwiązania naszego problemu z zasłanianiem wzorców. Przypomnijmy, że stworzyliśmy nową zmienną wewnątrz wzorca w wyrażeniu match zamiast używać zmiennej poza match. Ta nowa zmienna oznaczała, że nie mogliśmy testować wartości zmiennej zewnętrznej. Lista 19-27 pokazuje, jak możemy użyć match guarda, aby naprawić ten problem.

Nazwa pliku: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Lista 19-27: Użycie match guarda do testowania równości z zewnętrzną zmienną

Ten kod wydrukuje teraz Default case, x = Some(5). Wzorzec w drugim ramieniu match nie wprowadza nowej zmiennej y, która zasłaniałaby zewnętrzną y, co oznacza, że możemy użyć zewnętrznej y w match guardzie. Zamiast określać wzorzec jako Some(y), co zasłoniłoby zewnętrzną y, określamy Some(n). Tworzy to nową zmienną n, która niczego nie zasłania, ponieważ poza match nie ma zmiennej n.

Match guard if n == y nie jest wzorcem i dlatego nie wprowadza nowych zmiennych. To y jest zewnętrznym y, a nie nowym y je zasłaniającym, i możemy szukać wartości, która ma taką samą wartość jak zewnętrzne y, porównując n z y.

Możesz również użyć operatora lub | w match guardzie, aby określić wiele wzorców; warunek match guarda będzie miał zastosowanie do wszystkich wzorców. Lista 19-28 pokazuje pierwszeństwo przy łączeniu wzorca używającego | z match guardem. Ważną częścią tego przykładu jest to, że match guard if y ma zastosowanie do 4, 5 i 6, mimo że może wydawać się, że if y ma zastosowanie tylko do 6.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}
Lista 19-28: Łączenie wielu wzorców z match guardem

Warunek dopasowania stwierdza, że ramię pasuje tylko wtedy, gdy wartość x jest równa 4, 5 lub 6 i jeśli y jest true. Kiedy ten kod się uruchamia, wzorzec pierwszego ramienia pasuje, ponieważ x wynosi 4, ale match guard if y jest false, więc pierwsze ramię nie zostaje wybrane. Kod przechodzi do drugiego ramienia, które pasuje, a program wypisuje no. Powodem jest to, że warunek if ma zastosowanie do całego wzorca 4 | 5 | 6, a nie tylko do ostatniej wartości 6. Innymi słowy, pierwszeństwo match guarda w stosunku do wzorca zachowuje się w ten sposób:

(4 | 5 | 6) if y => ...

zamiast tego:

4 | 5 | (6 if y) => ...

Po uruchomieniu kodu zachowanie pierwszeństwa jest oczywiste: gdyby match guard był stosowany tylko do ostatniej wartości na liście wartości określonych za pomocą operatora |, ramię pasowałoby, a program wydrukowałby yes.

Używanie Wiązań @

Operator at @ pozwala nam utworzyć zmienną, która przechowuje wartość w tym samym czasie, gdy testujemy tę wartość pod kątem dopasowania wzorca. Na Liście 19-29 chcemy sprawdzić, czy pole id w Message::Hello mieści się w zakresie 3..=7. Chcemy również powiązać wartość ze zmienną id, aby móc jej użyć w kodzie skojarzonym z ramieniem.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id @ 3..=7 } => {
            println!("Found an id in range: {id}")
        }
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}
Lista 19-29: Użycie @ do związania się z wartością we wzorcu, jednocześnie ją testując

Ten przykład wydrukuje Found an id in range: 5. Określając id @ przed zakresem 3..=7, przechwytujemy dowolną wartość, która pasuje do zakresu, w zmiennej nazwanej id, jednocześnie testując, czy wartość pasuje do wzorca zakresu.

W drugim ramieniu, gdzie we wzorcu mamy określony tylko zakres, kod skojarzony z ramieniem nie ma zmiennej zawierającej faktyczną wartość pola id. Wartość pola id mogła wynosić 10, 11 lub 12, ale kod, który towarzyszy temu wzorcowi, nie wie, która to jest. Kod wzorca nie jest w stanie użyć wartości z pola id, ponieważ nie zapisaliśmy wartości id w zmiennej.

W ostatnim ramieniu, gdzie określiliśmy zmienną bez zakresu, mamy dostępną wartość do użycia w kodzie ramienia w zmiennej o nazwie id. Powodem jest to, że użyliśmy skróconej składni pól struktury. Ale w tym ramieniu nie zastosowaliśmy żadnego testu do wartości w polu id, tak jak zrobiliśmy to w dwóch pierwszych ramionach: dowolna wartość pasowałaby do tego wzorca.

Używanie @ pozwala nam testować wartość i zapisywać ją w zmiennej w ramach jednego wzorca.

Podsumowanie

Wzorce Rust są bardzo przydatne w rozróżnianiu różnych rodzajów danych. Używane w wyrażeniach match, Rust zapewnia, że Twoje wzorce obejmują każdą możliwą wartość, w przeciwnym razie Twój program się nie skompiluje. Wzorce w instrukcjach let i parametrach funkcji czynią te konstrukcje bardziej użytecznymi, umożliwiając dekonstrukcję wartości na mniejsze części i przypisywanie tych części do zmiennych. Możemy tworzyć proste lub złożone wzorce, aby sprostać naszym potrzebom.

Następnie, w przedostatnim rozdziale książki, przyjrzymy się niektórym zaawansowanym aspektom różnych funkcji Rust.