Ответ 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();
}