Avatar ·

Идиоматичные колбэки в Rust: как правильно передавать функции и замыкания

Привет, коллеги! Я начал изучать Rust после C++ и не могу понять, как правильно делать колбэки. В C++ я просто передавал std::function или указатель на функцию. Здесь же всё строже с владением и временем жизни. Проблема: хочу написать простой обработчик событий. Есть структура Button, которая должна вызывать пользовательскую функцию при нажатии. В C++ я бы сделал так: ```cpp struct Button { std::function onClick; void press() { if(onClick) onClick(); } }; ``` Пытаюсь переписать на Rust: ```rust struct Button { callback: fn(), // пробую указатель на функцию } impl Button { fn press(&self) { (self.callback)(); } } ``` Это работает только для обычных функций, но не для замыканий, которые захватывают переменные. Если я пишу: ```rust let mut count = 0; let button = Button { callback: || { count += 1; } // ошибка! не совпадает тип }; ``` Компилятор ругается: "expected fn item, found closure". Я читал про Box и про обобщения с Fn/FnMut/FnOnce, но не понимаю, какой вариант считается идиоматичным. Когда использовать dyn, когда generics? Как быть с временем жизни, если колбэк должен жить долго? И что делать, если колбэк должен изменять внешние данные (нужен &mut self)? Помогите разобраться с идиоматичными подходами для колбэков в 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 — присмотритесь к каналам вместо обычных колбэков.