Risposta breve: per la massima flessibilità, è possibile memorizzare il callback come FnMut
oggetto boxed , con il setter callback generico sul tipo di callback. Il codice per questo è mostrato nell'ultimo esempio nella risposta. Per una spiegazione più dettagliata, continua a leggere.
"Puntatori a funzione": richiama come fn
L'equivalente più vicino del codice C ++ nella domanda sarebbe dichiarare callback come fn
tipo. fn
incapsula le funzioni definite dalla fn
parola chiave, proprio come i puntatori a funzione di 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();
}
Questo codice potrebbe essere esteso per includere un Option<Box<Any>>
per contenere i "dati utente" associati alla funzione. Anche così, non sarebbe Rust idiomatico. Il modo Rust per associare i dati a una funzione è catturarli in una chiusura anonima , proprio come nel moderno C ++. Poiché le chiusure non lo sono fn
, set_callback
sarà necessario accettare altri tipi di oggetti funzione.
Callback come oggetti funzione generici
Sia in Rust che in C ++ le chiusure con la stessa firma di chiamata sono disponibili in dimensioni diverse per adattarsi ai diversi valori che potrebbero acquisire. Inoltre, ogni definizione di chiusura genera un tipo anonimo univoco per il valore della chiusura. A causa di questi vincoli, la struttura non può nominare il tipo del suo callback
campo, né può utilizzare un alias.
Un modo per incorporare una chiusura nel campo struct senza fare riferimento a un tipo concreto è rendere generico lo struct . La struttura adatterà automaticamente la sua dimensione e il tipo di callback per la funzione concreta o la chiusura che gli passi:
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();
}
Come prima, la nuova definizione di callback sarà in grado di accettare funzioni di primo livello definite con fn
, ma questa accetterà anche chiusure come || println!("hello world!")
, così come chiusure che catturano valori, come || println!("{}", somevar)
. Per questo motivo il processore non ha bisogno userdata
di accompagnare la richiamata; la chiusura fornita dal chiamante di set_callback
catturerà automaticamente i dati di cui ha bisogno dal suo ambiente e li avrà a disposizione quando invocato.
Ma qual è il problema con il FnMut
, perché non solo Fn
? Poiché le chiusure contengono valori acquisiti, le normali regole di mutazione di Rust devono essere applicate quando si chiama la chiusura. A seconda di cosa fanno le chiusure con i valori che detengono, sono raggruppate in tre famiglie, ciascuna contrassegnata da un tratto:
Fn
sono chiusure che leggono solo i dati e possono essere chiamate in modo sicuro più volte, possibilmente da più thread. Entrambe le chiusure di cui sopra sono Fn
.
FnMut
sono chiusure che modificano i dati, ad esempio scrivendo in una mut
variabile catturata . Possono anche essere chiamati più volte, ma non in parallelo. (Chiamare una FnMut
chiusura da più thread porterebbe a una corsa di dati, quindi può essere eseguita solo con la protezione di un mutex.) L'oggetto di chiusura deve essere dichiarato mutabile dal chiamante.
FnOnce
sono chiusure che consumano alcuni dei dati che acquisiscono, ad esempio spostando un valore acquisito in una funzione che ne assume la proprietà. Come suggerisce il nome, questi possono essere chiamati solo una volta e il chiamante deve possederli.
Un po 'controintuitivamente, quando si specifica un tratto legato al tipo di un oggetto che accetta una chiusura, FnOnce
è in realtà il più permissivo. Dichiarare che un tipo di callback generico deve soddisfare il FnOnce
tratto significa che accetterà letteralmente qualsiasi chiusura. Ma questo ha un prezzo: significa che il titolare può chiamarlo solo una volta. Poiché process_events()
può scegliere di invocare la richiamata più volte e poiché il metodo stesso può essere chiamato più di una volta, il successivo limite più permissivo è FnMut
. Nota che abbiamo dovuto contrassegnare process_events
come mutante self
.
Callback non generici: oggetti tratto di funzione
Anche se l'implementazione generica del callback è estremamente efficiente, presenta gravi limitazioni di interfaccia. Richiede che ogni Processor
istanza sia parametrizzata con un tipo di callback concreto, il che significa che una singola istanza Processor
può gestire solo un singolo tipo di callback. Dato che ogni chiusura ha un tipo distinto, il generico Processor
non può gestire proc.set_callback(|| println!("hello"))
seguito da proc.set_callback(|| println!("world"))
. L'estensione della struttura per supportare due campi di callback richiederebbe la parametrizzazione dell'intera struttura su due tipi, che diventerebbero rapidamente ingombranti man mano che il numero di callback cresce. L'aggiunta di più parametri di tipo non funzionerebbe se il numero di callback dovesse essere dinamico, ad esempio per implementare una add_callback
funzione che mantiene un vettore di callback differenti.
Per rimuovere il parametro type, possiamo sfruttare gli oggetti trait , la funzionalità di Rust che consente la creazione automatica di interfacce dinamiche basate sui tratti. Questo a volte è indicato come cancellazione del tipo ed è una tecnica popolare in C ++ [1] [2] , da non confondere con l'uso un po 'diverso del termine nei linguaggi Java e FP. I lettori che hanno familiarità con C ++ riconosceranno la distinzione tra una chiusura che implementa Fn
e un Fn
oggetto tratto come equivalente alla distinzione tra oggetti funzione generali e std::function
valori in C ++.
Un oggetto tratto viene creato prendendo in prestito un oggetto con l' &
operatore e proiettandolo o costringendolo a un riferimento al tratto specifico. In questo caso, poiché è Processor
necessario possedere l'oggetto callback, non possiamo usare il prestito, ma dobbiamo memorizzare il callback in un heap allocato Box<dyn Trait>
(l'equivalente Rust di std::unique_ptr
), che è funzionalmente equivalente a un oggetto trait.
Se Processor
memorizza Box<dyn FnMut()>
, non deve più essere generico, ma il set_callback
metodo ora accetta un generico c
tramite un impl Trait
argomento . In quanto tale, può accettare qualsiasi tipo di chiamata, comprese le chiusure con stato, e inscatolarlo adeguatamente prima di memorizzarlo nel file Processor
. L'argomento generico a set_callback
non limita il tipo di callback che il processore accetta, poiché il tipo di callback accettato è disaccoppiato dal tipo memorizzato nella Processor
struttura.
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();
}
Durata delle referenze all'interno delle chiusure in scatola
La 'static
durata legata al tipo di c
argomento accettato da set_callback
è un modo semplice per convincere il compilatore che i riferimenti contenuti in c
, che potrebbe essere una chiusura che si riferisce al suo ambiente, si riferiscono solo a valori globali e rimarranno quindi validi durante l'uso del richiama. Ma il limite statico è anche molto pesante: mentre accetta bene le chiusure che possiedono oggetti (cosa che abbiamo assicurato sopra effettuando la chiusura move
), rifiuta le chiusure che si riferiscono all'ambiente locale, anche quando si riferiscono solo a valori che sopravvivono al processore e sarebbero infatti al sicuro.
Poiché abbiamo solo bisogno dei callback attivi finché il processore è attivo, dovremmo cercare di legare la loro durata a quella del processore, che è un limite meno stretto di 'static
. Ma se rimuoviamo solo il 'static
limite di durata da set_callback
, non viene più compilato. Questo perché set_callback
crea una nuova casella e la assegna al callback
campo definito come Box<dyn FnMut()>
. Poiché la definizione non specifica una durata per l'oggetto tratto in scatola, 'static
è implicita e l'assegnazione amplierebbe effettivamente la durata (da una durata arbitraria senza nome del callback a 'static
), che non è consentita. La soluzione consiste nel fornire una durata esplicita per il processore e legare tale durata sia ai riferimenti nella casella che ai riferimenti nella richiamata ricevuta da 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);
}
}
Con queste vite rese esplicite, non è più necessario utilizzare 'static
. La chiusura può ora riferirsi s
all'oggetto locale , cioè non deve più essere move
, a condizione che la definizione di s
sia posta prima della definizione di p
per garantire che la stringa sopravviva al processore.
CB
deve essere'static
l'ultimo esempio?