Kurze Antwort: Für maximale Flexibilität können Sie den Rückruf als Box- FnMut
Objekt speichern , wobei der Rückruf-Setter für den Rückruftyp generisch ist. Der Code hierfür wird im letzten Beispiel in der Antwort gezeigt. Eine ausführlichere Erklärung finden Sie weiter.
"Funktionszeiger": Rückrufe als fn
Das nächste Äquivalent des C ++ - Codes in der Frage wäre das Deklarieren des Rückrufs als fn
Typ. fn
kapselt Funktionen, die durch das fn
Schlüsselwort definiert sind , ähnlich wie die Funktionszeiger von 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(); // hello world!
}
Dieser Code könnte erweitert werden, um Option<Box<Any>>
die der Benutzer zugeordneten "Benutzerdaten" aufzunehmen. Trotzdem wäre es kein idiomatischer Rust. Die Rust-Methode zum Verknüpfen von Daten mit einer Funktion besteht darin, sie in einem anonymen Abschluss zu erfassen , genau wie in modernem C ++. Da Verschlüsse nicht vorhanden sind fn
, set_callback
müssen andere Arten von Funktionsobjekten akzeptiert werden.
Rückrufe als generische Funktionsobjekte
Sowohl in Rust- als auch in C ++ - Abschlüssen mit derselben Anrufsignatur gibt es unterschiedliche Größen, um den unterschiedlichen Werten Rechnung zu tragen, die sie möglicherweise erfassen. Darüber hinaus generiert jede Abschlussdefinition einen eindeutigen anonymen Typ für den Wert des Abschlusses. Aufgrund dieser Einschränkungen kann die Struktur weder den Typ ihres callback
Felds benennen noch einen Alias verwenden.
Eine Möglichkeit, einen Abschluss in das Strukturfeld einzubetten, ohne sich auf einen konkreten Typ zu beziehen, besteht darin, die Struktur generisch zu gestalten . Die Struktur passt ihre Größe und die Art des Rückrufs automatisch an die konkrete Funktion oder den Abschluss an, den Sie an sie übergeben:
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();
}
Nach wie vor kann die neue Definition des Rückrufs Funktionen der obersten Ebene akzeptieren, die mit definiert fn
wurden. Diese Definition akzeptiert jedoch auch Schließungen || println!("hello world!")
als sowie Schließungen, die Werte erfassen, wie z || println!("{}", somevar)
. Aus diesem Grund muss der Prozessor userdata
den Rückruf nicht begleiten. Der vom Aufrufer von bereitgestellte Abschluss set_callback
erfasst automatisch die benötigten Daten aus seiner Umgebung und stellt sie beim Aufrufen zur Verfügung.
Aber was ist mit dem FnMut
, warum nicht einfach Fn
? Da Closures erfasste Werte enthalten, müssen beim Aufrufen des Closures die üblichen Mutationsregeln von Rust gelten. Abhängig davon, was die Schließungen mit den Werten tun, die sie enthalten, werden sie in drei Familien eingeteilt, die jeweils mit einem Merkmal gekennzeichnet sind:
Fn
sind Abschlüsse, die nur Daten lesen und sicher mehrmals aufgerufen werden können, möglicherweise von mehreren Threads. Beide oben genannten Verschlüsse sind Fn
.
FnMut
sind Abschlüsse, die Daten ändern, z. B. durch Schreiben in eine erfasste mut
Variable. Sie können auch mehrfach aufgerufen werden, jedoch nicht parallel. (Das Aufrufen eines FnMut
Abschlusses von mehreren Threads würde zu einem Datenrennen führen, daher kann dies nur unter dem Schutz eines Mutex erfolgen.) Das Abschlussobjekt muss vom Aufrufer als veränderbar deklariert werden.
FnOnce
sind Abschlüsse, die die von ihnen erfassten Daten verbrauchen , z. B. indem sie in eine Funktion verschoben werden, deren Eigentümer sie sind. Wie der Name schon sagt, dürfen diese nur einmal aufgerufen werden, und der Anrufer muss sie besitzen.
Etwas kontraintuitiv, wenn ein Merkmal angegeben wird, das für den Typ eines Objekts gebunden ist, das einen Abschluss akzeptiert, FnOnce
ist tatsächlich das freizügigste. Die Erklärung, dass ein generischer Rückruftyp das FnOnce
Merkmal erfüllen muss, bedeutet, dass er buchstäblich jede Schließung akzeptiert. Das ist aber mit einem Preis verbunden: Der Inhaber darf ihn nur einmal anrufen. Da process_events()
der Rückruf möglicherweise mehrmals aufgerufen wird und die Methode selbst mehrmals aufgerufen werden kann, ist die nächsthöhere Grenze FnMut
. Beachten Sie, dass wir process_events
als mutierend markieren mussten self
.
Nicht generische Rückrufe: Objekte mit Funktionsmerkmalen
Obwohl die generische Implementierung des Rückrufs äußerst effizient ist, weist sie schwerwiegende Schnittstellenbeschränkungen auf. Jede Processor
Instanz muss mit einem konkreten Rückruftyp parametrisiert werden. Dies bedeutet, dass eine einzelne Instanz Processor
nur mit einem einzelnen Rückruftyp umgehen kann. Da jeder Verschluss einen eigenen Typ hat, kann das Generikum Processor
nicht proc.set_callback(|| println!("hello"))
gefolgt von verarbeiten proc.set_callback(|| println!("world"))
. Um die Struktur auf zwei Rückruffelder zu erweitern, müsste die gesamte Struktur auf zwei Typen parametrisiert werden, was mit zunehmender Anzahl von Rückrufen schnell unhandlich werden würde. Das Hinzufügen weiterer Typparameter würde nicht funktionieren, wenn die Anzahl der Rückrufe dynamisch sein müsste, z. B. um eine add_callback
Funktion zu implementieren , die einen Vektor verschiedener Rückrufe verwaltet.
Um den Typparameter zu entfernen, können wir Merkmalsobjekte nutzen , die Funktion von Rust, die die automatische Erstellung dynamischer Schnittstellen basierend auf Merkmalen ermöglicht. Dies wird manchmal als Typlöschung bezeichnet und ist eine beliebte Technik in C ++ [1] [2] , nicht zu verwechseln mit der etwas anderen Verwendung des Begriffs durch Java- und FP-Sprachen. Mit C ++ vertraute Leser erkennen die Unterscheidung zwischen einem implementierten Abschluss Fn
und einem Fn
Merkmalsobjekt als äquivalent zur Unterscheidung zwischen allgemeinen Funktionsobjekten und std::function
Werten in C ++.
Ein Merkmalsobjekt wird erstellt, indem ein Objekt beim &
Bediener ausgeliehen und auf einen Verweis auf das jeweilige Merkmal geworfen oder gezwungen wird. In diesem Fall Processor
können wir , da wir das Rückrufobjekt besitzen müssen, keine Ausleihe verwenden, sondern müssen den Rückruf in einem Heap-zugewiesenen Box<dyn Trait>
(dem Rust-Äquivalent von std::unique_ptr
) speichern , das funktional einem Merkmalsobjekt entspricht.
Wenn Processor
gespeichert wird Box<dyn FnMut()>
, muss es nicht mehr generisch sein, aber die set_callback
Methode akzeptiert jetzt ein generisches c
über ein impl Trait
Argument . Als solches kann es jede Art von aufrufbar akzeptieren, einschließlich Schließungen mit Status, und es ordnungsgemäß verpacken, bevor es in der gespeichert wird Processor
. Das generische Argument, set_callback
die Art des Rückrufs, den der Prozessor akzeptiert, nicht einzuschränken, da der Typ des akzeptierten Rückrufs von dem in der Processor
Struktur gespeicherten Typ entkoppelt ist .
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();
}
Lebensdauer von Referenzen in Boxverschlüssen
Die 'static
Lebensdauer, die an den Typ des von c
akzeptierten Arguments gebunden set_callback
ist, ist eine einfache Möglichkeit, den Compiler davon zu überzeugen, dass die darin enthaltenen Verweisec
, bei denen es sich möglicherweise um einen Abschluss handelt, der sich auf seine Umgebung bezieht, nur auf globale Werte verweisen und daher während der gesamten Verwendung von gültig bleiben zurückrufen. Die statische Bindung ist aber auch sehr hartnäckig: Während sie Verschlüsse akzeptiert, die Objekte besitzen, die in Ordnung sind (was wir oben durch das Schließen sichergestellt haben move
), lehnt sie Verschlüsse ab, die sich auf die lokale Umgebung beziehen, selbst wenn sie sich nur auf Werte beziehen, die dies tun überleben den Prozessor und wäre in der Tat sicher.
Da wir die Rückrufe nur so lange benötigen, wie der Prozessor aktiv ist, sollten wir versuchen, ihre Lebensdauer an die des Prozessors zu binden, was weniger streng ist als 'static
. Wenn wir jedoch nur die 'static
gebundene Lebensdauer entfernen set_callback
, wird sie nicht mehr kompiliert. Dies liegt daran, dass set_callback
ein neues Feld erstellt und dem callback
als definierten Feld zugewiesen wird Box<dyn FnMut()>
. Da die Definition keine Lebensdauer für das Boxed-Trait-Objekt angibt, 'static
ist dies impliziert, und die Zuweisung würde die Lebensdauer (von einer unbenannten willkürlichen Lebensdauer des Rückrufs auf 'static
) effektiv verlängern , was nicht zulässig ist. Der Fix besteht darin, eine explizite Lebensdauer für den Prozessor bereitzustellen und diese Lebensdauer sowohl mit den Referenzen in der Box als auch mit den Referenzen im Rückruf zu verknüpfen, die empfangen wurden von 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);
}
// ...
}
Da diese Lebensdauern explizit angegeben werden, ist eine Verwendung nicht mehr erforderlich 'static
. Der Abschluss kann sich nun auf das lokale s
Objekt beziehen , muss es also nicht mehr sein move
, vorausgesetzt, die Definition von s
wird vor die Definition von gesetzt, p
um sicherzustellen, dass die Zeichenfolge den Prozessor überlebt.
CB
es'static
im letzten Beispiel sein?