绕过"移出借用自身"检查器的首选模式是什么?

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,因为selfhandle_event中被借用,但我不确定这是否是最好的方法。

以下是我能想到的方法及其缺点:

  1. 像我所做的那样将其放入Option中:这感觉很鬼畜,每次都需要检查不变量,即Option始终是Some,否则就会panic!或者使用if let Some() =并忽略else子句,但这会导致代码膨胀。使用unwrap或用if let Some()让人感到有些不舒服。
  2. 将其放入共享所有权的Rc<RefCell<>>中:需要堆分配所有这样的变量或构造另一个名为Inner的结构体,它具有所有这些非可克隆类型,并将其放入Rc<RefCell<>>中。
  3. 将东西传回Dispatcher,表示它基本上要将我们从地图中删除,然后将东西移出我们到下一个State,这也将通过我们的返回值指示:耦合过多,破坏面向对象编程,不适用于规模较大的项目,因为Dispatcher需要了解所有的State并需要频繁更新。我认为这不是一种好的编程范式,但可能是错的。
  4. 为上述MOF实现Default:现在我们可以使用默认值mem::replace替换它,同时移出旧值。抛出异常、返回错误或执行NOP的负担现在隐藏在MOF的实现中。问题在于,我们并不总是有访问MOF类型的权限,对于那些我们有访问权限的类型,它又将代码膨胀从用户代码转移到MOF的代码中。
  5. 让函数handle_event通过移动self来获取self,如fn handle_event(mut self, ...) -> Option<Self>:现在,你需要使用Box<State>,每次在调度程序中移动它,如果返回值是Some,则将其放回。这几乎感觉像一个大锤,使许多其他惯用语不可能,例如,如果我想在某个注册的闭包/回调中进一步共享self,我通常会先放置一个Weak<RefCell<>>,但现在在回调等中共享self是不可能的。

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


“Dispatcher” 到底是做什么的?我能理解的唯一方式是,你有多个具有不同 Uid 的状态机,并且调度程序选择其中一个来处理每个事件。为什么要在 HashMap 中保留所有“已使用”的状态 - 状态是否会再次进入?如何进入? - trent
@Shepmaster,我没有完全理解你的意思 - 哪个 dtor 将访问不应访问的内存?此外,这里没有 unsafe 代码,那么哪些安全性已经被破坏了?另外,您建议如何解决这个问题? - ustulation
@Shepmaster,现在我明白你的意思了,本来还以为你是说这段代码本身不安全呢。好吧——我认为使用Rc<RefCell>实现状态转换模式在单线程事件驱动机制中很常见。那么你更喜欢哪种方法(或者如果没有的话,你会用什么来替代这种做法)? - ustulation
你没有任何代码可以在转换时删除旧状态;我没有意识到它应该在那里。 - trent
当然会 - 它不是一个multimap,只是一个普通的rust std HashMap。重新阅读代码中的插入逻辑 - 这将替换以前的内容(这就是插入通常要做的)。如果您错过了这个逻辑,else分支还明确调用remove_state,这被认为是与已编码的insert_state互补的。 - ustulation
显示剩余4条评论
1个回答

1
让函数handle_event采用fn handle_event(mut self, ...) -> Option<Self>的方式进行移动:现在,您需要使用Box<State>并在调度程序中每次移出它,如果返回值为Some,则将其放回。但是,如果只有一个强引用,则不需要从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。不过,看起来UidCopy,所以几乎没有什么区别。)
现在不清楚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
        }
    }
}

现在你不必处理一个奇怪的,本应该不可能出现的选项为空的情况。
这几乎感觉像是一把大锤,使许多其他惯用语变得不可能,例如如果我想在某个注册的闭包/回调中进一步共享self,我通常会先放置一个Weak,但现在在回调等中共享self是不可能的。
因为如果你拥有唯一的强引用,你可以从Rc中移出它,所以你不必牺牲这种技术。
“感觉像是一把大锤子”可能是主观的,但对我来说,像 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 而不是完全从映射中删除它。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接