Подтвердить что ты не робот

Идиоматические обратные вызовы в Rust

В C/С++ я обычно делаю обратные вызовы с помощью простого указателя на функцию, возможно, передавая параметр void* userdata. Что-то вроде этого:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

Что такое идиоматический способ сделать это в Rust? В частности, какие типы должны использовать мои функции setCallback(), и какой тип должен быть mCallback? Должен ли он взять Fn? Может быть, FnMut? Сохранить его Boxed? Пример будет потрясающим.

4b9b3361

Ответ 1

Краткий ответ: для максимальной гибкости вы можете сохранить обратный вызов в виде объекта FnMut в штучной FnMut, с установщиком обратного вызова, общим для типа обратного вызова. Код для этого показан в последнем примере в ответе. Для более подробного объяснения читайте дальше.

"Указатели на функции": обратные вызовы как fn

Ближайшим эквивалентом кода C++ в вопросе будет объявление обратного вызова как тип fn. fn инкапсулирует функции, определенные ключевым словом fn, очень похоже на указатели функций C++:

type Callback = fn();

struct Processor {
    callback: Callback,
}

impl Processor {
    fn set_callback(&mut self, c: Callback) {
        self.callback = c;
    }

    fn process_events(&self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello world!");
}

fn main() {
    let mut p = Processor { callback: simple_callback };
    p.process_events();         // hello world!
}

Этот код может быть расширен, чтобы включить Option<Box<Any>> для хранения "пользовательских данных", связанных с функцией. Несмотря на это, это не был бы идиоматический Rust. Способ связать данные с функцией в Rust состоит в том, чтобы захватить их в анонимном закрытии, как в современном C++. Поскольку замыкания не являются fn, set_callback должен будет принимать другие виды функциональных объектов.

Обратные вызовы как объекты универсальных функций

И в Rust, и в C++ у замыканий с одной и той же сигнатурой вызова бывают разные размеры, чтобы приспособиться к разным размерам захваченных значений, которые они хранят в объекте замыкания. Кроме того, каждый сайт закрытия генерирует отдельный анонимный тип, который является типом объекта закрытия во время компиляции. Из-за этих ограничений структура не может ссылаться на тип обратного вызова по имени или псевдониму типа.

Один из способов владеть замыканием в структуре без ссылки на конкретный тип - сделать структуру универсальной. Структура автоматически адаптирует свой размер и тип обратного вызова для конкретной функции или замыкания, которое вы ей передаете:

struct Processor<CB> where CB: FnMut() {
    callback: CB,
}

impl<CB> Processor<CB> where CB: FnMut() {
    fn set_callback(&mut self, c: CB) {
        self.callback = c;
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn main() {
    let s = "world!".to_string();
    let callback = || println!("hello {}", s);
    let mut p = Processor { callback: callback };
    p.process_events();
}

Как и прежде, новое определение обратного вызова сможет принимать функции верхнего уровня, определенные с помощью fn, но это также будет принимать замыкания как || println!("hello world!") || println!("hello world!"), а также замыкания, которые записывают значения, такие как || println!("{}", somevar) || println!("{}", somevar). Из-за этого замыкание не нуждается в отдельном аргументе userdata; он может просто захватывать данные из своей среды и будет доступен при вызове.

Но что FnMut с FnMut, а не только с Fn? Поскольку замыкания содержат захваченные значения, Rust применяет к ним те же правила, что и к другим объектам-контейнерам. В зависимости от того, что замыкания делают со значениями, которые они содержат, они группируются в три семейства, каждое из которых помечено свойством:

  • Fn - это замыкания, которые только читают данные и могут безопасно вызываться несколько раз, возможно, из нескольких потоков. Оба вышеуказанных замыкания являются Fn.
  • FnMut - это замыкания, которые изменяют данные, например, путем записи в захваченную переменную mut. Они также могут быть вызваны несколько раз, но не параллельно. (Вызов замыкания FnMut из нескольких потоков приведет к гонке данных, поэтому это может быть сделано только с защитой мьютекса.) Объект замыкания должен быть объявлен изменяемым вызывающей стороной.
  • FnOnce - это замыкания, которые потребляют данные, которые они захватывают, например, перемещая их в функцию, которой они принадлежат. Как следует из названия, они могут быть вызваны только один раз, и вызывающий должен иметь их.

В некоторой степени нелогично, когда указывается черта, привязанная к типу объекта, который принимает замыкание, FnOnce на самом деле является наиболее допустимым. Объявление о том, что универсальный тип обратного вызова должен удовлетворять признаку FnOnce означает, что он будет принимать буквально любое замыкание. Но это связано с ценой: это означает, что владелец может позвонить только один раз. Поскольку process_events() может вызывать обратный вызов несколько раз, а сам метод может вызываться более одного раза, следующей наиболее допустимой границей является FnMut. Обратите внимание, что мы должны были пометить process_events как мутировавшего self.

Неуниверсальные обратные вызовы: объекты функций функций

Хотя общая реализация обратного вызова чрезвычайно эффективна, она имеет серьезные ограничения интерфейса. Это требует, чтобы каждый экземпляр Processor был параметризован с конкретным типом обратного вызова, что означает, что один Processor может иметь дело только с одним типом обратного вызова. Учитывая, что каждое замыкание имеет отдельный тип, универсальный Processor не может обрабатывать proc.set_callback(|| println!("hello")) за которым следует proc.set_callback(|| println!("world")). Расширение структуры для поддержки двух полей обратного вызова потребовало бы параметризации всей структуры двумя типами, что быстро становилось бы громоздким по мере роста числа обратных вызовов. Добавление большего количества параметров типа не будет работать, если число обратных вызовов должно быть динамическим, например, для реализации функции add_callback которая поддерживает вектор различных обратных вызовов.

Чтобы удалить параметр типа, мы можем воспользоваться объектами признаков, функцией Rust, которая позволяет автоматически создавать динамические интерфейсы на основе признаков. Это иногда упоминается как стирание типа и является популярным методом в C++ [1] [2], его не следует путать с несколько иным использованием этого термина в языках Java и FP. Читатели, знакомые с C++, распознают различие между замыканием, которое реализует Fn и объект признака Fn как эквивалентное различию между объектами общей функции и значениями std::function в C++.

Объект признака создается путем заимствования объекта оператором & и приведения или приведения его к ссылке на конкретную признак. В этом случае, поскольку Processor необходимо владеть объектом обратного вызова, мы не можем использовать заимствование, но должны хранить обратный вызов в выделенной куче Box<Trait> (эквивалент std::unique_ptr для std::unique_ptr), который функционально эквивалентен признаку объект.

Если Processor хранит Box<FnMut()>, он больше не должен быть универсальным, но метод set_callback теперь является универсальным, поэтому он может правильно set_callback что set_callback вызываете, перед сохранением блока в Processor. Обратный вызов может быть любым, если он не использует захваченные значения. set_callback универсальным, set_callback не имеет ограничений, описанных выше, так как он не влияет на интерфейс данных, хранящихся в структуре.

struct Processor {
    callback: Box<FnMut()>,
}

impl Processor {
    fn set_callback<CB: 'static + FnMut()>(&mut self, c: CB) {
        self.callback = Box::new(c);
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello");
}

fn main() {
    let mut p = Processor { callback: Box::new(simple_callback) };
    p.process_events();
    let s = "world!".to_string();
    let callback2 = move || println!("hello {}", s);
    p.set_callback(callback2);
    p.process_events();
}