Предпочтительный шаблон для обхода проверки на "выезд из заемного я"
Рассмотрим схему, в которой есть несколько состояний, зарегистрированных диспетчером, и каждое состояние знает, в какое состояние переходить, когда оно получает соответствующее событие. Это простой шаблон перехода состояний.
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!
или сделать это NOP сif let Some() =
и игнорируйте предложение else, но это вызывает раздувание кода. Делатьunwrap
или раздутый код сif let Some()
чувствует себя немного не в себе. - Получить его в общую собственность
Rc<RefCell<>>
: Нужно выделить кучу всех таких переменных или создать другую структуру с именемInner
или что-то, что имеет все эти не клонируемые типы и положить это вRc<RefCell<>>
, - Передать материал обратно
Dispatcher
указывая на это, чтобы в основном удалить нас с карты, а затем переместить вещи из нас к следующемуState
который также будет указан через наше возвращаемое значение: Слишком сильная связь, разрыв ООП, не масштабируется какDispatcher
должен знать обо всехState
и нуждается в частом обновлении. Я не думаю, что это хорошая парадигма, но могу ошибаться. - Воплощать в жизнь
Default
для MOF выше: теперь мы можемmem::replace
это по умолчанию при удалении старого значения. Бремя паники ИЛИ возврата ошибки ИЛИ выполнения NOP теперь скрыто в реализацииMOF
, Проблема здесь в том, что у нас не всегда есть доступ к типу MOF, и для тех, кто у нас это делает, снова возникает точка раздувания от пользовательского кода к коду MOF. - Пусть функция
handle_event
приниматьself
по ходу какfn handle_event(mut self, ...) -> Option<Self>
: Теперь вместоRc<RefCell<>>
вам нужно будет иметьBox<State>
и перемещать его каждый раз в диспетчере, и если возвратSome
ты положил его обратно. Это почти похоже на кувалду и делает невозможным многие другие идиомы, например, если бы я хотел поделиться с самим собой в каком-то зарегистрированном закрытии / обратном вызове, я обычно ставил быWeak<RefCell<>>
Раньше, но сейчас делить себя в обратных вызовах и т. д. невозможно.
Есть ли другие варианты? Есть ли какой-нибудь способ, который считается "самым идиоматическим" в Rust?
1 ответ
- Позвольте функции handle_event взять себя, двигаясь как
fn handle_event(mut self, ...) -> Option<Self>
: Теперь вместоRc<RefCell<>>
вам нужно будет иметь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
, Это выглядит как Uid
является Copy
хотя, так что это мало что меняет.)
Не понятно 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.
Это почти похоже на кувалду и делает невозможным многие другие идиомы, например, если бы я хотел поделиться с самим собой в каком-то зарегистрированном закрытии / обратном вызове, я обычно ставил бы
Weak<RefCell<>>
Раньше, но сейчас делить себя в обратных вызовах и т. д. невозможно.
Потому что вы можете выйти из Rc
Если у вас есть единственная надежная ссылка, вам не нужно жертвовать этой техникой.
"Чувствуется как кувалда" может быть субъективным, но для меня какая подпись fn handle_event(mut self, ...) -> Option<Self>
делает кодировать инвариант. С оригинальной версией каждый impl State for ...
должен был знать, когда вставлять и удалять себя из диспетчера, и было ли это невозможно или не подлежало проверке. Например, если где-то глубоко в логике ты забыл позвонить dispatcher.insert(state_id, next_state)
конечный автомат не будет переходить, а может застрять или еще хуже. когда handle_event
принимает self
по значению, это больше невозможно - вам нужно вернуть следующее состояние, иначе код просто не скомпилируется.
(Помимо: и оригинальная, и моя версии каждый раз выполняют как минимум два хеш-таблицы dispatch
называется: один раз, чтобы получить текущее состояние, и снова, чтобы вставить новое состояние. Если вы хотите избавиться от второго поиска, вы можете комбинировать подходы: хранить Option<Rc<State>>
в HashMap
, а также take
от Option
вместо того, чтобы полностью удалить его с карты.)