Предпочтительный шаблон для обхода проверки на "выезд из заемного я"

Рассмотрим схему, в которой есть несколько состояний, зарегистрированных диспетчером, и каждое состояние знает, в какое состояние переходить, когда оно получает соответствующее событие. Это простой шаблон перехода состояний.

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, но я не уверен, что это лучший способ подойти к нему.

Вот способы, которыми я могу думать с недостатками каждого из них:

  1. Поместите это в Option как я уже делал: это кажется счастливым, и каждый раз, когда мне нужно проверить инвариант, что Option всегда Some иначеpanic! или сделать это NOP с if let Some() = и игнорируйте предложение else, но это вызывает раздувание кода. Делать unwrapили раздутый код с if let Some() чувствует себя немного не в себе.
  2. Получить его в общую собственность Rc<RefCell<>>: Нужно выделить кучу всех таких переменных или создать другую структуру с именем Inner или что-то, что имеет все эти не клонируемые типы и положить это вRc<RefCell<>>,
  3. Передать материал обратно Dispatcher указывая на это, чтобы в основном удалить нас с карты, а затем переместить вещи из нас к следующему State который также будет указан через наше возвращаемое значение: Слишком сильная связь, разрыв ООП, не масштабируется как Dispatcher должен знать обо всехStateи нуждается в частом обновлении. Я не думаю, что это хорошая парадигма, но могу ошибаться.
  4. Воплощать в жизнь Default для MOF выше: теперь мы можем mem::replace это по умолчанию при удалении старого значения. Бремя паники ИЛИ возврата ошибки ИЛИ выполнения NOP теперь скрыто в реализацииMOF, Проблема здесь в том, что у нас не всегда есть доступ к типу MOF, и для тех, кто у нас это делает, снова возникает точка раздувания от пользовательского кода к коду MOF.
  5. Пусть функция handle_event принимать self по ходу как fn handle_event(mut self, ...) -> Option<Self>: Теперь вместо Rc<RefCell<>> вам нужно будет иметь Box<State> и перемещать его каждый раз в диспетчере, и если возврат Some ты положил его обратно. Это почти похоже на кувалду и делает невозможным многие другие идиомы, например, если бы я хотел поделиться с самим собой в каком-то зарегистрированном закрытии / обратном вызове, я обычно ставил бы Weak<RefCell<>> Раньше, но сейчас делить себя в обратных вызовах и т. д. невозможно.

Есть ли другие варианты? Есть ли какой-нибудь способ, который считается "самым идиоматическим" в Rust?

1 ответ

  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 вместо того, чтобы полностью удалить его с карты.)

Другие вопросы по тегам