【问题标题】:Why is a match pattern with a guard clause not exhaustive?为什么带有保护子句的匹配模式并不详尽?
【发布时间】:2018-11-06 05:31:50
【问题描述】:

考虑以下代码示例 (playground)。

#[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);)实际上是首选解决方案。

很容易想象未来我确实希望某些州在“无过渡”情况下做某事。当添加新的States 时,使用此宏仍然具有强制重新访问状态机的预期效果,并且如果不需要执行任何操作,只需将新状态添加到宏 arglist。一个合理的妥协 IMO。

我认为这个问题仍然有用,因为作为一个相对较新的 Rustacean,这种行为让我感到惊讶。

【问题讨论】:

  • 虽然在这个简单的情况下,编译器理论上可以找出保护所涵盖的情况,但这通常是不可能的。守卫可以包含任意表达式、函数调用和对外部状态的引用,因此在一般情况下编译器无法知道守卫条件是否成立。出于这个原因,Rust 总是假设它可能成立,即使守卫是if true,这使得语义简单且可预测。
  • @Stargateur,不,我自己没有说,是的,我确实清楚地解释了我想要做什么。我想捕捉所有 start_state 和 end_state 相等的情况。
  • @Sven 谢谢你说得有道理,我想我希望它能用尽这个简单的案例。
  • @MichaelLeonard 那么(State::Initial, State::Initial) | (State::One, State::One) | (State::Two, State::Two) => {} 有什么问题呢?那就是我会做的。解决方案已经在您问题的末尾。使用宏或手写。
  • @Stargateur,它可以工作,但很乏味(我只是好奇为什么这不起作用)。我希望这个状态机能够增长到包含 100 个状态。话虽如此,出于其他原因,我决定我建议的第二个宏实际上是更好的解决方案。

标签: rust pattern-matching


【解决方案1】:

为什么这不能像写的那样工作?

因为 Rust 编译器在确定 match 是否详尽时无法考虑防护表达式。只要你有一个守卫,它就假定守卫可能会失败。

请注意,这与refutable and irrefutable patterns 的区别无关。 if 守卫不是模式的一部分,它们是match 语法的一部分。

什么是做我想做的最干净的方法?

列出所有可能的组合。宏可以稍微缩短编写模式的时间,但您不能使用宏来替换整个match arm。

【讨论】:

  • 如果您确信自己做得对,请添加_ => unreachable!()
  • @rodrigo,不,我想避免包罗万象的模式,以便在以后添加新状态时强制更新匹配语句。我相信我现在做得正确,但要努力防止未来的变化。而且,模式不是'unreachable!()',在两个状态相等的情况下调用这个函数是有效的,我只是不想让它做任何事情。
  • @MichaelLeonard 作为一般评论,当然最好编译器可以静态检查您的不变量。如果不能,您的下一个最佳选择是单元测试。您需要使用来自strum 板条箱的EnumIter 派生宏才能遍历枚举的所有变体,但如果输入了catch-all 分支,您还需要_ => unreachable!() 恐慌。
  • @SvenMarnach,这是个好建议。这在这个示例案例中显然可以工作,但实际用例是一个需要 no_std crates 的 rust-embedded 项目,我不确定cargo test 是否在这种情况下工作(还没有尝试过)。
  • @DK。请更新您的答案以引用可反驳与不可反驳的模式,我会接受。 doc.rust-lang.org/book/second-edition/ch18-02-refutability.html
猜你喜欢
  • 2020-11-28
  • 1970-01-01
  • 2011-04-30
  • 2019-04-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-28
相关资源
最近更新 更多