Почему образец совпадения с пунктом охраны не является исчерпывающим? - PullRequest
0 голосов
/ 06 ноября 2018

Рассмотрим следующий пример кода ( детская площадка ).

#[derive(PartialEq, Clone, Debug)]
enum State {
    Initial,
    One,
    Two,
}

enum Event {
    ButtonOne,
    ButtonTwo,
}

struct StateMachine {
    state: State,
}

impl StateMachine {
    fn new() -> StateMachine {
        StateMachine {
            state: State::Initial,
        }
    }

    fn advance_for_event(&mut self, event: Event) {
        // grab a local copy of the current state
        let start_state = self.state.clone();

        // determine the next state required
        let end_state = match (start_state, event) {
            // starting with initial
            (State::Initial, Event::ButtonOne) => State::One,
            (State::Initial, Event::ButtonTwo) => State::Two,

            // starting with one
            (State::One, Event::ButtonOne) => State::Initial,
            (State::One, Event::ButtonTwo) => State::Two,

            // starting with two
            (State::Two, Event::ButtonOne) => State::One,
            (State::Two, Event::ButtonTwo) => State::Initial,
        };

        self.transition(end_state);
    }

    fn transition(&mut self, end_state: State) {
        // update the state machine
        let start_state = self.state.clone();
        self.state = end_state.clone();

        // handle actions on entry (or exit) of states
        match (start_state, end_state) {
            // transitions out of initial state
            (State::Initial, State::One) => {}
            (State::Initial, State::Two) => {}

            // transitions out of one state
            (State::One, State::Initial) => {}
            (State::One, State::Two) => {}

            // transitions out of two state
            (State::Two, State::Initial) => {}
            (State::Two, State::One) => {}

            // identity states (no transition)
            (ref x, ref y) if x == y => {}

            // ^^^ above branch doesn't match, so this is required
            // _ => {},
        }
    }
}

fn main() {
    let mut sm = StateMachine::new();

    sm.advance_for_event(Event::ButtonOne);
    assert_eq!(sm.state, State::One);

    sm.advance_for_event(Event::ButtonOne);
    assert_eq!(sm.state, State::Initial);

    sm.advance_for_event(Event::ButtonTwo);
    assert_eq!(sm.state, State::Two);

    sm.advance_for_event(Event::ButtonTwo);
    assert_eq!(sm.state, State::Initial);
}

В методе StateMachine::transition приведенный код не компилируется:

error[E0004]: non-exhaustive patterns: `(Initial, Initial)` not covered
  --> src/main.rs:52:15
   |
52 |         match (start_state, end_state) {
   |               ^^^^^^^^^^^^^^^^^^^^^^^^ pattern `(Initial, Initial)` not covered

Но это именно тот шаблон, которому я пытаюсь соответствовать! Вместе с ребром (One, One) и ребром (Two, Two). Важно, что я особенно хочу этот случай, потому что я хочу использовать компилятор, чтобы гарантировать, что каждый возможный переход состояния обрабатывается (особенно, когда новые состояния добавляются позже), и я знаю, что переходы идентичности всегда будут запрещены.

Я могу устранить ошибку компилятора, раскомментировав строку ниже этой (_ => {}), но тогда я потеряю преимущество проверки компилятором допустимых переходов, потому что это будет соответствовать любым состояниям, добавленным в будущем.

Я также мог бы решить эту проблему, набрав вручную каждый переход идентификации, например:

(State::Initial, State::Initial) => {}

Это утомительно, и в этот момент я просто сражаюсь с компилятором. Это, вероятно, можно превратить в макрос, поэтому я мог бы сделать что-то вроде:

identity_is_no_op!(State);

Или худший случай:

identity_is_no_op!(State::Initial, State::One, State::Two);

Макрос может автоматически писать этот шаблон в любое время, когда добавляется новое состояние, но это выглядит как ненужная работа, когда шаблон, который я написал, должен охватывать именно тот случай, который я ищу.

  1. Почему это не работает как написано?
  2. Какой самый чистый способ сделать то, что я пытаюсь сделать?

Я решил, что макрос второй формы (т.е. identity_is_no_op!(State::Initial, State::One, State::Two);) на самом деле является предпочтительным решением.

Легко представить себе будущее, в котором я хочу, чтобы некоторые государства что-то делали в случае «без перехода». Использование этого макроса все равно будет иметь желательный эффект от принудительного пересмотра конечного автомата при добавлении новых State s и просто потребует добавления нового состояния в арглист макроса, если ничего не нужно будет делать. Разумный компромисс ИМО.

Я думаю, что вопрос все еще полезен, потому что поведение меня удивляло как сравнительно нового Рустаха.

1 Ответ

0 голосов
/ 06 ноября 2018

Почему это не работает как написано?

Поскольку компилятор Rust не может учитывать защитные выражения при определении, является ли match исчерпывающим. Как только у вас появляется охранник, он предполагает, что охранник может потерпеть неудачу.

Обратите внимание, что это не имеет ничего общего с различием между опровержимыми и неопровержимыми образцами . if охранники не часть шаблона, они являются частью синтаксиса match.

Какой самый чистый способ сделать то, что я пытаюсь сделать?

Список всех возможных комбинаций. Макрос может немного сократить написание шаблонов, но вы не можете использовать макросы для замены целых match плеч.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...