首页 文章

绕过“搬出借来的自我”检查员的优选模式

提问于
浏览
4

考虑一种模式,其中有几个状态向调度程序注册,并且每个状态知道在收到适当事件时要转换到的状态 . 这是一种简单的状态转换模式 .

struct Dispatcher {
    states: HashMap<Uid, Rc<RefCell<State>>>,
}
impl Dispatcher {
    pub fn insert_state(&mut self, state_id: Uid, state: Rc<RefCell<State>>) -> Option<Rc<RefCell<State>>> {
        self.states.insert(state_id, state)
    }
    fn dispatch(&mut self, state_id: Uid, event: Event) {
        if let Some(mut state) = states.get_mut(&state_id).cloned() {
            state.handle_event(self, event);
        }
    }
}

trait State {
    fn handle_event(&mut self, &mut Dispatcher, Event);
}

struct S0 {
    state_id: Uid,
    move_only_field: Option<MOF>,
    // This is pattern that concerns me.
}
impl State for S0 {
    fn handle_event(&mut self, dispatcher: &mut Dispatcher, event: Event) {
        if event == Event::SomeEvent {
            // Do some work
            if let Some(mof) = self.mof.take() {
                let next_state = Rc::new(RefCell::new(S0 {
                    state_id: self.state_id,
                    move_only_field: mof,
                }));
                let _ = dispatcher.insert(self.state_id, next_state);
            } else {
                // log an error: BUGGY Logic somewhere
                let _ = dispatcher.remove_state(&self.state_id);
            }
        } else {
            // Do some other work, maybe transition to State S2 etc.
        }
    }
}

struct S1 {
    state_id: Uid,
    move_only_field: MOF,
}
impl State for S1 {
    fn handle_event(&mut self, dispatcher: &mut Dispatcher, event: Event) {
        // Do some work, maybe transition to State S2/S3/S4 etc.
    }
}

参考上面的内联评论说:

// This is pattern that concerns me.

S0::move_only_field 在此模式中需要是 Option ,因为 self 是在 handle_event 中借用的,但我不确定这是接近它的最佳方法 .

以下是我可以想到的每种方法的缺点:

  • 把它放进 Option 就像我做的那样:这感觉很乱,每次我需要检查不变量 Option 总是 Some 否则 panic! 或者用 if let Some() = 做一个NOP并忽略else子句,但这会导致代码膨胀 . 使用 if let Some() 执行 unwrap 或膨胀代码感觉有点偏 .

  • 将其置于共享所有权 Rc<RefCell<>> :需要堆分配所有此类变量或构造另一个名为 Inner 的结构或具有所有这些不可克隆类型的结构并将其放入 Rc<RefCell<>> .

  • 将东西传回 Dispatcher ,表示它基本上将我们从 Map 中删除,然后将我们的东西移出到下一个 State ,这也将通过我们的返回值表示:太多耦合,打破OOP,不会缩放为 Dispatcher 需要了解所有 State 并需要经常更新 . 我认为这不是一个好的范例,但可能是错的 .

  • 为上面的MOF实现 Default :现在我们可以在移出旧值时使用默认值 mem::replace . 恐慌或返回错误或执行NOP的负担现在隐藏在 MOF 的实现中 . 这里的问题是我们并不总是能够访问MOF类型,对于我们所做的那些,它再次从用户代码到MOF代码的膨胀点 .

  • 让函数 handle_eventself 移动为 fn handle_event(mut self, ...) -> Option<Self> :现在取代 Rc<RefCell<>> ,你需要有 Box<State> 并在调度程序中每次移出它,如果返回 Some ,你就把它放回去 . 这几乎感觉像一个大锤,并使许多其他成语不可能,例如,如果我想在一些注册的闭包/回调中进一步分享自我,我通常会先放一个 Weak<RefCell<>> 但现在在回调中分享自我等是不可能的 .

还有其他选择吗?在Rust中有没有被认为是“ most idiomatic ”这样做的方式?

1 回答

  • 1

    让函数handle_event通过move移动为fn handle_event(mut self,...) - > Option <Self>:现在代替Rc <RefCell <>>你将需要Box <State>并将其移出在调度员的时间,如果返回是一些你把它放回去 .

    这就是我要做的 . 但是,如果只有一个强引用,则无需从 Rc 切换到 BoxRc::try_unwrap可以移出 Rc .

    以下是如何重写 Dispatcher 的部分内容:

    struct Dispatcher {
        states: HashMap<Uid, Rc<State>>,
    }
    impl Dispatcher {
        fn dispatch(&mut self, state_id: Uid, event: Event) {
            if let Some(state_ref) = self.states.remove(&state_id) {
                let state = state_ref.try_unwrap()
                    .expect("Unique strong reference required");
                if let Some(next_state) = state.handle_event(event) {
                    self.states.insert(state_id, next_state);
                }
            } else {
                // handle state_id not found
            }
        }
    }
    

    (注意: dispatch 按值获取 state_id . 在原始版本中,这不是必需的 - 它可能已被更改为通过引用传递 . 在此版本中,有必要,因为 state_id 传递给 HashMap::insert . 它看起来像 Uid 虽然是 Copy ,所以它没什么区别 . )

    目前尚不清楚 state_id 是否真的需要成为实现 State 的结构的成员,因为你在 handle_event 中不需要它 - 所有插入和删除都发生在 impl Dispatcher 内部,这是有意义的并且减少了 StateDispatcher 之间的耦合 .

    impl State for S0 {
        fn handle_event(self, event: Event) -> Option<Rc<State>> {
            if event == Event::SomeEvent {
                // Do some work
                let next_state = Rc::new(S0 {
                    state_id: self.state_id,
                    move_only_field: self.mof,
                });
                Some(next_state)
            } else {
                // Do some other work
            }
        }
    }
    

    现在你不必处理一个奇怪的,应该是不可能的角落情况,其中Option是None .

    这几乎感觉像一个大锤,并使许多其他成语不可能,例如,如果我想在一些注册的闭包/回调中进一步分享自我我通常会先放一个弱的<RefCell <>>但现在在回调中分享自我等是不可能的 .

    因为如果你有唯一的强引用,你可以移出 Rc ,你不必牺牲这种技术 .

    "Feels like a sledgehammer"可能是主观的,但对我来说,像 fn handle_event(mut self, ...) -> Option<Self> 这样的签名就是编码一个不变量 . 对于原始版本,每个 impl State for ... 必须知道何时从调度程序中插入和删除自己,以及是否取消它是不可检查的 . 例如,如果您忘记调用 dispatcher.insert(state_id, next_state) 逻辑深处的某个地方,状态机将不会转换,并且可能会卡住或更糟 . 当 handle_event 取值 self 时,这是不可能的 - 你必须返回下一个状态,否则代码就不会编译 .

    (旁白:每次调用 dispatch 时,原始版本和我的至少两个散列表查找:一次获取当前状态,再次插入新状态 . 如果你想摆脱第二次查找,你可以组合方法:在 HashMap 中存储 Option<Rc<State>> ,在 Option 中存储 take 而不是从 Map 中删除它完全 . )

相关问题