【发布时间】: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);
宏可以在任何时候添加新状态时自动编写这个样板,但是当我编写的模式应该涵盖我正在寻找的确切情况时,这感觉就像是不必要的工作。
- 为什么不按书面规定工作?
- 什么是我想做的最干净的方法?
我决定第二种形式的宏(即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