Краткий ответ: для максимальной гибкости вы можете сохранить обратный вызов как упакованный 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 p = Processor {
callback: simple_callback,
};
p.process_events();
}
Этот код можно расширить, включив Option<Box<Any>>
в него «пользовательские данные», связанные с функцией. Даже в этом случае это не был бы идиоматический Rust. В Rust способ связать данные с функцией заключается в их анонимном закрытии , как в современном C ++. Поскольку замыканий нет fn
, set_callback
необходимо будет принимать другие типы объектов функций.
Обратные вызовы как универсальные функциональные объекты
И в Rust, и в C ++ замыкания с одной и той же сигнатурой вызова имеют разные размеры, чтобы соответствовать различным значениям, которые они могут захватывать. Кроме того, каждое определение замыкания генерирует уникальный анонимный тип для значения замыкания. Из-за этих ограничений структура не может назвать тип своего callback
поля или использовать псевдоним.
Один из способов встроить замыкание в поле структуры без ссылки на конкретный тип - сделать структуру универсальной . Структура автоматически адаптирует свой размер и тип обратного вызова для конкретной функции или замыкания, которые вы ей передаете:
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
, но это также будет принимать замыкания as || println!("hello world!")
, а также замыкания, которые фиксируют значения, такие как || println!("{}", somevar)
. Из-за этого процессору не нужно userdata
сопровождать обратный вызов; закрытие, предоставленное вызывающим set_callback
, автоматически захватит необходимые данные из своей среды и сделает их доступными при вызове.
Но в чем дело 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<dyn Trait>
(эквивалент Rust std::unique_ptr
), что функционально эквивалентно объекту черты.
Если Processor
хранит Box<dyn FnMut()>
, он больше не должен быть универсальным, но теперь set_callback
метод принимает универсальный c
через impl Trait
аргумент . Таким образом, он может принимать любые вызываемые объекты, включая замыкания с состоянием, и правильно упаковывать их перед сохранением в Processor
. Общий аргумент set_callback
не ограничивает тип обратного вызова, который принимает процессор, поскольку тип принятого обратного вызова отделен от типа, хранящегося в Processor
структуре.
struct Processor {
callback: Box<dyn FnMut()>,
}
impl Processor {
fn set_callback(&mut self, c: impl FnMut() + 'static) {
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();
}
Срок службы ссылок внутри закрытых коробок
'static
Срок службы связан с типом c
аргумента , принятого set_callback
простой способ убедить компилятор , что ссылки , содержащиеся в c
, что может быть замыкание , которое относится к окружающей среде, относятся только к глобальным ценностям и , следовательно , будет оставаться в силе в течение использования из перезвонить. Но статическая граница также является очень жесткой: хотя она принимает замыкания, которые владеют объектами в полном порядке (что мы обеспечили выше, сделав замыкание move
), она отклоняет замыкания, которые относятся к локальной среде, даже если они относятся только к значениям, которые пережить процессор и на самом деле будет в безопасности.
Поскольку нам нужны только обратные вызовы, пока жив процессор, мы должны попытаться связать их время жизни со временем жизни процессора, что является менее строгим ограничением, чем 'static
. Но если мы просто удалим 'static
ограничение времени жизни set_callback
, он больше не будет компилироваться. Это связано с тем, что set_callback
создается новый блок и назначается его callback
полю, определенному как Box<dyn FnMut()>
. Поскольку в определении не указано время жизни для упакованного в штучный объект признака, 'static
подразумевается, и присвоение эффективно расширит время жизни (от неназванного произвольного времени жизни обратного вызова до 'static
), что запрещено. Исправление состоит в том, чтобы указать явное время жизни процессора и связать это время жизни как со ссылками в поле, так и со ссылками в обратном вызове, полученными set_callback
:
struct Processor<'a> {
callback: Box<dyn FnMut() + 'a>,
}
impl<'a> Processor<'a> {
fn set_callback(&mut self, c: impl FnMut() + 'a) {
self.callback = Box::new(c);
}
}
Поскольку эти значения времени жизни указаны явно, в использовании больше нет необходимости 'static
. Замыкание теперь может относиться к локальному s
объекту, т. Е. Больше не должно быть move
, при условии, что определение объекта s
помещается перед определением, p
чтобы гарантировать, что строка переживет процессор.
CB
должно быть'static
в последнем примере?