Dopasowanie wzorca

Konstrukcja match

  1. Dopisz do enumeracji z poprzedniego ćwiczenia (typ UIEvent) funkcję obsługującą dane zdarzenia:
    fn call(event : UIEvent) {
        match event {
            ButtonClicked => println!("Button clicked"), // simple match
            Scroll(x) => println!("Scroll {:?}", x), // attribute extraction
            KeyPressed(ch) => { // whole block
                let up_ch = ch.to_uppercase();
                println!("Key pressed: {}", up_ch);
            }
        }
    }
    fn main() {
        let clicked = ButtonClicked;
        let scroll = Scroll(Direction::Down);
        let key_pressed = KeyPressed('b');
        call(clicked);
        call(scroll);
        call(key_pressed);
    }

Zasady działania konstrukcji match?

  • przetwarzania od góry do dołu,
  • brak zachowania fall-through - dopasowanie do jednego wzorca, kończy dalsze przetwarzanie,
  • konieczność dopasowania co najmniej jednego warunku; reguła ta sprawdzana jest na etapie kompilacji,
  • match jest wyrażeniem, a więc może zwracać wartość; typ zwracanej wartości musi być wspólny dla wszystkich wariantów.

Dopasowanie wartości

  1. Konstrukcja match może zostać wykorzystana również do dopasowania zwykłych wartości (nie tylko typów wyliczeniowych). Przykładem może być tutaj dopasowanie liczby. Należy jednak pamiętać, że musimy zapewnić dopasowanie wszystkich możliwych wartości. W tym celu, aby nie musieć pisać wszystkich wartości z osobna, można wykorzystać symbol _:

    fn main() {
        let x = 2u8;
     
        match x {
            1 => println!("one"),
            2 => println!("two"),
            3 => println!("three"),
            _ => println!("other number")
        }
    }
  2. Wzorce wielokrotne pozwalają na dopasowanie do jednej operacji wielu wartości:

     fn main() {
        let x = 2u8;
     
        match x {
            1 | 2 => println!("one or two"),
            3 | 4 => println!("three or four"),
            _ => println!("anything else"),
        }
    }  
  3. Dopasowanie zakresów daje jeszcze większe możliwości dopasowania wartości

     fn main() {
        let x = 2u8;
     
        match x {
            1..=9 => println!("easy"),
            10..=99 => println!("medium"),
            100..=999 => println!("large"),
            _ => println!("anything else"),
        }
    }  

Ekstrakcja / dekonstrukcja wartości

  1. Dopasowanie wzorców może zostać również wykorzystane do dekonstrukcji struktury i zdefiniowanie warunków dotyczących wartości jej atrybutów. Dekonstrukcja pozwala na wykorzystanie atrybutu bezpośrednio w bloku określającym operację wykonywana po dopasowaniu wzorca. Przykładowo, na poniższym fragmencie kodu zdefiniowano warunki dla struktury Point
     
    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})");
            }
        }
    }
  2. Możliwe jest również pominięcie atrybutów, które nie są istotne z punktu widzenia dopasowania wzorca
    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),
            _ => ()
        }
    }

Instrukcja warunkowa w dopasowaniu wzorca

  1. Reguły dopasowujace mogą dodatkowy warunek z wykorzystaniem instrukcji if (tzw. strażnik, guard)
    fn main() {
        let num = Some(4);
     
        match num {
            Some(x) if x % 2 == 0 => println!("The number {} is even", x),
            Some(x) => println!("The number {} is odd", x),
            None => (),
        }
    }
  2. Kombinacja strażnika z dopasowaniem wielokrotnym
    fn main() {
        let x = 4;
        let y = false;
     
        match x {
            4 | 5 | 6 if y => println!("yes"),
            _ => println!("no"),
        }
    }

Konstrukcja if let

  1. Konstrukcja if let jest wykorzystywana wtedy gdy chcemy dopasować tylko jeden wzorzec, a całą resztę pominać. Na przykład mowa o takiej sytuacji:
    fn main() {
        let x = Some(2);
     
        match x {
            Some(x) => println!("{}"),
            None => ()
        }
    }
  2. Powyższa konstrukcję można zastąpić w następujący sposób:
    fn main() {
        let x = Some(2);
     
        if let Some(x) = x {
            println!("{}", x);
        }
    }
  3. Zarówno w konstrukcji match oraz let if wzorzec może zawierać konkretne wartości:
    fn main() {
        let x = Some(2);
     
        if let Some(2) = x {
            println!("{}", x);
        }
    }

Więcej informacji o dopasowaniu wzorców znajdziesz na stronie: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html

Ćwiczenie

Zdefiniuj typ wyliczeniowy Message oraz funkcję process na bazie ich użycia w teście test_match_message_call, przy użyciu zdefiniowanych metod.

enum Message {
    // TODO: implement the message variant types based on their usage below
}
 
struct Point {
    x: u8,
    y: u8,
}
 
struct State {
    color: (u8, u8, u8),
    position: Point,
    quit: bool,
    message: String,
}
 
impl State {
    fn change_color(&mut self, color: (u8, u8, u8)) {
        self.color = color;
    }
 
    fn quit(&mut self) {
        self.quit = true;
    }
 
    fn echo(&mut self, s: String) {
        self.message = s
    }
 
    fn move_position(&mut self, p: Point) {
        self.position = p;
    }
 
    fn process(&mut self, message: Message) {
        // TODO: create a match expression to process the different message variants
        // Remember: When passing a tuple as a function argument, you'll need extra parentheses:
        // fn function((t, u, p, l, e))
    }
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn test_match_message_call() {
        let mut state = State {
            quit: false,
            position: Point { x: 0, y: 0 },
            color: (0, 0, 0),
            message: "hello world".to_string(),
        };
        state.process(Message::ChangeColor(255, 0, 255));
        state.process(Message::Echo(String::from("Hello world!")));
        state.process(Message::Move(Point { x: 10, y: 15 }));
        state.process(Message::Quit);
 
        assert_eq!(state.color, (255, 0, 255));
        assert_eq!(state.position.x, 10);
        assert_eq!(state.position.y, 15);
        assert_eq!(state.quit, true);
        assert_eq!(state.message, "Hello world!");
    }
}