为什么带有保护子句的匹配模式不是详尽无遗的? [英] Why is a match pattern with a guard clause not exhaustive?

查看:52
本文介绍了为什么带有保护子句的匹配模式不是详尽无遗的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下代码示例(游乐场).

#[derive(PartialEq, Clone, Debug)]枚举状态{最初的,一,二,}枚举事件{按钮一,按钮二,}结构状态机{状态:状态,}impl 状态机 {fn new() ->状态机{状态机{状态:状态::初始,}}fn Advance_for_event(&mut self, event: Event) {//获取当前状态的本地副本让 start_state = self.state.clone();//确定所需的下一个状态让 end_state = 匹配(开始状态,事件){//从初始(State::Initial, Event::ButtonOne) =>状态::一,(State::Initial, Event::ButtonTwo) =>状态::二,//从一开始(State::One, Event::ButtonOne) =>状态::初始,(State::One, Event::ButtonTwo) =>状态::二,//从两个开始(State::Two, Event::ButtonOne) =>状态::一,(State::Two, Event::ButtonTwo) =>状态::初始,};self.transition(end_state);}fn transition(&mut self, end_state: State) {//更新状态机让 start_state = self.state.clone();self.state = end_state.clone();//处理进入(或退出)状态的动作匹配(开始状态,结束状态){//跳出初始状态(State::Initial, State::One) =>{}(State::Initial, State::Two) =>{}//跳出一种状态(State::One, State::Initial) =>{}(State::One, State::Two) =>{}//两个状态的转换(State::Two, State::Initial) =>{}(状态::二,状态::一) =>{}//身份状态(无转换)(ref x, ref y) 如果 x == y =>{}//^^^ 上面的分支不匹配,所以这是必需的//_ =>{},}}}fn 主(){让 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]: 非穷举模式:`(Initial, Initial)` 未涵盖-->src/main.rs:52:15|52 |匹配(开始状态,结束状态){|^^^^^^^^^^^^^^^^^^^^^^^ 模式`(Initial, Initial)`未覆盖

但这正是我想要匹配的模式!连同 (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 时强制重新访问状态机,并且如果不需要做任何事情,只需要将新状态添加到宏 arglist.一个合理的妥协 IMO.

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

解决方案

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

因为 Rust 编译器在确定 match 是否详尽时无法考虑保护表达式.一旦有了守卫,它就会假设守卫可能会失败.

请注意,这与 无可辩驳和无可辩驳的模式.if 守卫不是模式的一部分,它们是match语法的一部分.

<块引用>

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

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

Consider the following code sample (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);
}

In the StateMachine::transition method, the code as presented does not compile:

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

But that is exactly the pattern I'm trying to match! Along with the (One, One) edge and (Two, Two) edge. Importantly I specifically want this case because I want to leverage the compiler to ensure that every possible state transition is handled (esp. when new states are added later) and I know that identity transitions will always be a no-op.

I can resolve the compiler error by uncommenting the line below that one (_ => {}) but then I lose the advantage of having the compiler check for valid transitions because this will match on any states added in the future.

I could also resolve this by manually typing each identity transition such as:

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

That is tedious, and at that point I'm just fighting the compiler. This could probably be turned into a macro, so I could possibly do something like:

identity_is_no_op!(State);

Or worst case:

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

The macro can automatically write this boilerplate any time a new state is added, but that feels like unnecessary work when the pattern I have written should be covering the exact case that I'm looking for.

  1. Why doesn't this work as written?
  2. What is the cleanest way to do what I am trying to do?


I have decided that a macro of the second form (i.e. identity_is_no_op!(State::Initial, State::One, State::Two);) is actually the preferred solution.

It's easy to imagine a future in which I do want some of the states to do something in the 'no transition' case. Using this macro would still have the desired effect of forcing the state machine to be revisited when new States are added and would just require adding the new state to the macro arglist if nothing needs to be done. A reasonable compromise IMO.

I think the question is still useful because the behavior was surprising to me as a relatively new Rustacean.

解决方案

Why doesn't this work as written?

Because the Rust compiler cannot take guard expressions into account when determining if a match is exhaustive. As soon as you have a guard, it assumes that guard could fail.

Note that this has nothing to do with the distinction between refutable and irrefutable patterns. if guards are not part of the pattern, they're part of the match syntax.

What is the cleanest way to do what I am trying to do?

List every possible combination. A macro could slightly shorten writing the patterns, but you can't use macros to replace entire match arms.

这篇关于为什么带有保护子句的匹配模式不是详尽无遗的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆