Идиоматичные колбэки в Rust: как правильно передавать функции и замыкания
Основные типы колбэков в Rust
Rust использует три трейта для представления вызываемых объектов:
Fn()— неизменяемый доступ к окружению (можно вызывать многократно)FnMut()— изменяемый доступ к окружению (можно вызывать многократно)FnOnce()— потребляет окружение (можно вызвать только один раз)
Выбор между обобщениями (generics) и типажами-объектами (dyn) — ключевое решение.
Вариант 1: Обобщения (рекомендуется для владельцев)
Используйте обобщения, когда структура владеет колбэком и тип известен на этапе компиляции:
struct Button<F: FnMut()> {
callback: F,
}
impl<F: FnMut()> Button<F> {
fn new(callback: F) -> Self {
Button { callback }
}
fn press(&mut self) {
(self.callback)();
}
}
// Использование:
let mut count = 0;
let mut button = Button::new(|| {
count += 1;
println!("Нажатие #{}", count);
});
button.press(); // Нажатие #1
button.press(); // Нажатие #2
Плюсы: нулевые накладные расходы, мономорфизация, высокая производительность.
Минусы: каждый экземпляр Button<F> имеет свой уникальный тип; нельзя хранить разные колбэки в одной коллекции.
Вариант 2: Типаж-объект (Box<dyn FnMut()>)
Когда нужна гибкость (разные колбэки в одном контейнере) или время жизни неизвестно:
struct Button {
callback: Box<dyn FnMut()>,
}
impl Button {
fn new<F: FnMut() + 'static>(callback: F) -> Self {
Button {
callback: Box::new(callback),
}
}
fn press(&mut self) {
(self.callback)();
}
}
// Разные колбэки:
let mut buttons: Vec<Button> = vec![
Button::new(|| println!("Кнопка 1")),
Button::new(|| println!("Кнопка 2")),
];
Важно: 'static требуется, если колбэк может пережить захваченные переменные. Если колбэк живёт только внутри функции, можно указать конкретное время жизни:
struct Button<'a> {
callback: Box<dyn FnMut() + 'a>,
}
Вариант 3: Ссылка на колбэк (для сервисов/наблюдателей)
Когда структура не владеет колбэком, а только хранит ссылку:
struct Button<'a> {
callback: &'a mut dyn FnMut(),
}
impl<'a> Button<'a> {
fn press(&mut self) {
(self.callback)();
}
}
Рекомендации по выбору:
- Для большинства случаев — используйте
FnMutс обобщением. Это наиболее идиоматично. - Если нужна коллекция колбэков —
Box<dyn FnMut()>. - Если колбэк вызывается один раз (например, обработчик завершения) —
FnOnce. - Для колбэков без захвата можно использовать
fn(), но это редкость.
Продвинутый пример: события с удалением
struct EventHandler<F: FnMut(&str)> {
handlers: Vec<F>, // не скомпилируется! F — один тип
}
// Правильно:
struct EventHandler {
handlers: Vec<Box<dyn FnMut(&str)>>,
}
impl EventHandler {
fn add<F: FnMut(&str) + 'static>(&mut self, handler: F) {
self.handlers.push(Box::new(handler));
}
fn trigger(&mut self, msg: &str) {
for handler in &mut self.handlers {
handler(msg);
}
}
}
Итог: начните с FnMut + обобщения. Если упрётесь в ограничения — переходите на Box<dyn FnMut()>.
Альтернатива: каналы и mpsc для асинхронных колбэков
Если ваша архитектура подразумевает асинхронную обработку событий (например, GUI или сетевые запросы), вместо прямых колбэков часто удобнее использовать каналы из std::sync::mpsc.
use std::sync::mpsc;
use std::thread;
enum Event {
Click,
KeyPress(char),
}
let (tx, rx) = mpsc::channel::<Event>();
// "Колбэк" — это отправитель
let click_tx = tx.clone();
thread::spawn(move || {
// имитация нажатия
click_tx.send(Event::Click).unwrap();
});
// Обработчик
for event in rx {
match event {
Event::Click => println!("Клик!"),
Event::KeyPress(c) => println!("Клавиша {}", c),
}
}
Преимущества:
- Нет проблем с временем жизни — канал живёт своей жизнью.
- Легко делать многопоточные системы.
- Можно передавать сложные данные, а не только вызов функции.
- Естественная поддержка отмены (drop канала).
Недостатки:
- Избыточно для простых синхронных колбэков.
- Требуется владение (move) для передачи в другой поток.
Этот подход особенно популярен в связке с tokio или async-std для построения event-driven систем. Если вы пишете сетевой сервис или GUI на Druid/Iced — присмотритесь к каналам вместо обычных колбэков.