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

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

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中借用的,但我不确定这是否是接近它的最佳方式.

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

>像我一样把它放入一个选项中:每次我需要的时候都会感觉很烦
检查选项总是不变的不变量
恐慌!如果让Some()=并忽略,则使其成为NOP
else子句,但这会导致代码膨胀.打开包装
或者让代码膨胀,如果让Some()感觉有点偏.
>将其转换为共享所有权Rc< RefCell<>>:需要堆分配
所有这些变量或构造另一个名为Inner或的结构
具有所有这些不可克隆类型的东西并将其放入
RC< RefCell<>取代.
>将东西传回Dispatcher,表明它基本上删除了我们
从地图上然后把我们的东西搬到下一个州
也将通过我们的返回值表示:耦合太多,
打破OOP,不会扩展,因为Dispatcher需要知道所有的
国家和需要经常更新.我认为这不是一件好事
范式,但可能是错的.
>为MOF实现默认值:现在我们可以用mem ::替换它
移出旧值时的默认值.恐慌的负担OR
返回错误或执行NOP现在隐藏在执行中
MOF.这里的问题是我们并不总是能够访问MOF
类型和我们做的那些,它再次臃肿
从用户代码到MOF代码.
>让函数handle_event通过move移动为fn handle_event(mut self,…) – >选项< Self>:现在而不是Rc< RefCell<>>你需要Box< State>并且每次在调度员中移出它,如果返回是某些你把它放回去.这几乎感觉像一个大锤,并使许多其他成语不可能,例如,如果我想在一些注册的闭包/回调中进一步分享自我,我通常会把一个弱的< RefCell<>>以前,但现在在回调等共享自我是不可能的.

还有其他选择吗?在Rust中有没有被认为是“最惯用”的方式?

最佳答案

  1. Let the function handle_event take self by move as fn handle_event(mut self, ...) -> Option<Self>: Now instead of Rc<RefCell<>> you will need to have Box<State> and move it out each time in the dispatcher and if the return is Some you put it back.

这就是我要做的.但是,如果只有一个强引用,则无需从Rc切换到Box:Rc::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是复制,所以它没什么区别.)

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

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.

This almost feels like a sledgehammer and makes many other idioms impossible, for instance if I wanted to share self further in some registered closure/callback I would normally put a Weak<RefCell<>> previously but now sharing self in callbacks etc is impossible.

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

“感觉像一把大锤”可能是主观的,但对我来说,像fn handle_event(mut self,…)这样的签名 – >选项<自>是编码不变量.对于原始版本,每个impl状态为…必须知道何时插入和从调度程序中删除自己,以及它是否取消是不可检查的.例如,如果您忘记调用dispatcher.insert(state_id,next_state)的逻辑深处,状态机将不会转换,可能会卡住或更糟.当handle_event采用self-value时,这是不可能的 – 你必须返回下一个状态,否则代码就不会编译.

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

点赞