Как в популярных крейтах реализованы обработчики событий с неизвестным количеством параметров?

Я все чаще замечаю, что в разных крейтах ржавчины используется регистрация обработчиков событий с любым набором параметров, который хочет пользователь, а не разработчик библиотеки. Я видел такое в следующих ящиках: axum, bevy engine, teloxyd.

У меня вопрос, как именно это делается под капотом, откуда библиотека знает, какие параметры нужны обработчику событий, которые могут иметь переменное количество параметров, как они передаются в функцию? Хочу почитать что-нибудь на эту тему. Я не могу понять, как библиотека принимает такие методы-обработчики в аргументах других методов и не использует макросы.

Как черта, которая реализуется для X параметров. Почему бы вам не взглянуть на их реализацию?

Chayim Friedman 22.07.2024 19:50

Я пробовал, но там много всего намешано. Я хотел бы увидеть где-нибудь только основной код.

John Darkman 22.07.2024 20:05
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
2
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как именно это работает, зависит от специфики библиотеки. Но общий шаблон состоит в том, чтобы иметь признак с методом, вызываемым с некоторым состоянием, а затем реализовать его для каждой функции с параметрами до X (с помощью макроса), когда параметры реализуют какой-либо другой признак, что означает возможность извлечь параметр из состояние.

Например:

pub struct State {
    // ...
}

pub trait Extractor: Sized {
    fn extract(state: &mut State) -> Self;
}

pub trait Handler<Args> {
    fn call(&mut self, state: &mut State);
}

// The below is usually done with a macro.
impl<F: FnMut()> Handler<()> for F {
    fn call(&mut self, _state: &mut State) {
        self()
    }
}
impl<Arg1: Extractor, F: FnMut(Arg1)> Handler<(Arg1,)> for F {
    fn call(&mut self, state: &mut State) {
        self(Arg1::extract(state))
    }
}
impl<Arg1: Extractor, Arg2: Extractor, F: FnMut(Arg1, Arg2)> Handler<(Arg1, Arg2)> for F {
    fn call(&mut self, state: &mut State) {
        self(Arg1::extract(state), Arg2::extract(state))
    }
}

pub fn call_handler<Args, H: Handler<Args>>(handler: &mut H, state: &mut State) {
    handler.call(state);
}

Удаление типа обработчика (например, чтобы сохранить все обработчики вместе) можно выполнить следующим образом:

pub fn handler_to_dyn<Args, H: Handler<Args> + 'static>(
    mut handler: H,
) -> Box<dyn FnMut(&mut State)> {
    Box::new(move |state| handler.call(state))
}

Хотя это часто делается с помощью дополнительной черты.

Теперь меня смущают возвращаемые значения от обработчиков. Я добавил признак, чтобы пользователи библиотеки также могли выбирать возвращаемые значения, но я не знаю, как использовать этот новый признак в векторе обработчиков. Я добавил универсальный код R ко всему коду: rust pub trait Handler<Args, R> { fn call(&self, state: &mut State) -> R; }

John Darkman 23.07.2024 15:11

@JohnDarkman Возвращаемое значение также должно обрабатываться единообразно. Обычно он реализует некоторый признак (например, IntoResponse), который позволяет превратить его в некоторый общий тип (Response). В impl Handler for Fn мы преобразуем возвращаемый тип в Response, и call() возвращает Response.

Chayim Friedman 23.07.2024 15:14

Все понял, добавил еще одну структуру для хранения результата и черту, которая преобразуется в эту структуру, вроде работает. Спасибо.

John Darkman 23.07.2024 15:36

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