programowanieProgramista Rust (infrastruktura/biblioteki rdzeniowe)

Jak działa operator match w Rust: cechy exhaustiveness checking, pattern guardy i zagnieżdżone dopasowywanie wzorca?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania

Operator match w Rust to potężne narzędzie do dopasowywania wzorców, zaczerpnięte z języków funkcyjnych. W przeciwieństwie do odpowiedników w C/C++, w Rust przeprowadzana jest ścisła kontrola pełności (exhaustiveness checking), co oznacza, że dla każdej możliwej opcji muszą być zdefiniowane odpowiednie gałęzie.

Problem

Błędy podczas używania match najczęściej wynikają z niewłaściwego uwzględnienia wszystkich wariantów typu (np. enum), błędnego działania z guardami (warunkami dla gałęzi) lub złożonej, zagnieżdżonej struktury. Niewłaściwe przetwarzanie prowadzi do błędów na etapie kompilacji lub do ukrytej, błędnej logiki.

Rozwiązanie

  • Exhaustiveness checking nie pozwala pominąć żadnej opcji, kompilator zmusi do dodania gałęzi dla każdej z nich (lub catch-all _).
  • Pattern guard uzupełniają gałęzie match dodatkowymi warunkami (if po wzorcu).
  • Zagnieżdżone dopasowywanie umożliwia rozwiązywanie złożonych, zagnieżdżonych enum lub krotek w jednej konstrukcji match.

Przykład kodu:

enum Shape { Circle(f64), Rectangle { width: f64, height: f64 }, } fn print_area(s: Shape) { match s { Shape::Circle(r) if r > 0.0 => println!("area = {}", 3.14 * r * r), Shape::Rectangle { width, height } if width > 0.0 && height > 0.0 => println!("area = {}", width * height), _ => println!("invalid shape"), } }

Kluczowe cechy:

  • Kontrola pełności dopasowania wzorca
  • Możliwość używania wyrażeń guard do filtrowania
  • Dopasowywanie do zagnieżdżonych struktur i praca z destrukturyzacją

Pytania podchwytliwe.

Czy kolejność gałęzi w match ma wpływ na wykonanie?

Tak, kolejność gałęzi jest ważna: gdy tylko znajdzie się pierwsze dopasowanie, kolejne nie są sprawdzane. To jest szczególnie krytyczne w przypadku pattern guard – jeśli wcześniej znajduje się gałąź z guard, przechwyci ona wartość przed catch-all.

Czy catch-all (_) jest wymagany przy match na enum?

Nie, jeśli jawnie rozpatrzono wszystkie przypadki (a same opcje deklaracji typu nie zmienią się w czasie). Jednak catch-all jest potrzebny przy pracy z typami, które mogą zyskać dodatkowe wartości lub gdy nie chcemy jawnie przetwarzać wszystkich gałęzi.

Czy w jednej gałęzi match można używać kilku wzorców (alternatives)?

Tak. Poprzez pionową kreskę (|) można łączyć wzorce:

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

Typowe błędy i antywzorce

  • Zapomnienie o obsłużeniu wszystkich wariantów enum bez catch-all
  • Użycie catch-all _ zbyt wcześnie (przed specyficznymi gałęziami)
  • Ignorowanie kolejności gałęzi z guardami

Przykład z życia

Negatywny przypadek

Programista napisał match z catch-all na początku i nie zdołał poprawnie obsłużyć specyficznych przypadków.

Zalety:

Program kompiluje się.

Wady:

Specyficzna logika nigdy nie działa, część kodu nie jest pokrywana.

Pozytywny przypadek

Jawna obsługa wszystkich wariantów enum, catch-all tylko jako ostatnia gałąź, oddzielne guardy dla nietypowych przypadków.

Zalety:

Przewidywalność, kompilator pomaga nie zapomnieć o wariancie, łatwe rozszerzenie typów.

Wady:

Dodatkowy szablonowy kod przy dużej liczbie wariantów.