Коротка відповідь: Для максимальної гнучкості ви можете зберігати зворотний дзвінок у коробці FnMut
об'єкт, що знаходиться , із загальним типом зворотного дзвінка для типу зворотного дзвінка. Код для цього наведено в останньому прикладі у відповіді. Для більш детального пояснення читайте далі.
"Покажчики функцій": зворотні виклики як fn
Найближчим еквівалентом коду С ++ у питанні було б оголошення зворотного виклику як 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-спосіб пов’язати дані з функцією - це захопити їх в анонімному закритті , як і в сучасному C ++. Оскільки закриття не є fn
,set_callback
потрібно буде приймати інші типи об’єктів функцій.
Зворотні виклики як загальні об’єкти функцій
І в Rust, і в C ++ закриття з однаковим підписом дзвінка бувають різних розмірів, щоб вмістити різні значення, які вони можуть захопити. Крім того, кожне визначення закриття генерує унікальний анонімний тип для значення закриття. Через ці обмеження структура не може назвати тип свого callback
поля, а також не може використовувати псевдонім.
Один із способів вставити закриття в поле struct без посилання на конкретний тип - це зробити структуру загальною . Структура автоматично адаптує свій розмір та тип зворотного виклику для конкретної функції або закриття, яке ви їй передаєте:
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!("{}", somevar)
. Через це процесору не потрібно userdata
супроводжувати зворотний виклик; закриття, надане абонентом set_callback
, автоматично захоплює потрібні йому дані із свого середовища та надає їх доступність при виклику.
Але в чому справа FnMut
, чому не просто Fn
? Оскільки закриття містять захоплені значення, при виклику закриття повинні застосовуватися звичайні правила мутації Руста. Залежно від того, що закриття робить із цінностями, які вони мають, вони згруповані у три сім'ї, кожна з яких позначена рисою:
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
функції, яка підтримує вектор різних зворотних викликів.
Щоб видалити параметр type, ми можемо скористатися перевагами об’єктів ознак , особливістю 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
в останньому прикладі?