
In diesem kurzen Artikel werde ich über ein Muster in Rust sprechen, mit dem Sie einen Typ, der eine generische Methode durchläuft, für die spätere Verwendung "speichern" können. Dieses Muster befindet sich in den Quellbibliotheken von Rust-Bibliotheken und wird manchmal auch in meinen Projekten verwendet. Ich konnte im Netzwerk keine Veröffentlichungen über ihn finden und gab ihm meinen Namen: "Generalized Type Closure". In diesem Artikel möchte ich Ihnen sagen, was es ist, warum und wie es verwendet werden kann.
Das Problem
In Rust reichen ein entwickeltes statisches Typsystem und seine statischen Fähigkeiten für wahrscheinlich 80% der Fälle aus. Es kommt jedoch vor, dass eine dynamische Eingabe erforderlich ist, wenn Sie Objekte unterschiedlichen Typs an derselben Stelle speichern möchten. Zeichentyp-Objekte helfen hier: Sie löschen die realen Objekttypen, reduzieren sie auf eine bestimmte allgemeine Schnittstelle, die vom Typ definiert wird, und dann können Sie diese Objekte als dieselben Typ-Typ-Objekte bearbeiten.
Dies funktioniert in der Hälfte der verbleibenden Fälle gut. Was aber, wenn wir die gelöschten Objekttypen bei ihrer Verwendung noch wiederherstellen müssen? Zum Beispiel, wenn das Verhalten unserer Objekte durch einen Typ bestimmt wird, der nicht als Typobjekt verwendet werden kann . Dies ist eine häufige Situation für Merkmale mit zugehörigen Typen. Was ist in diesem Fall zu tun?
Lösung
Für alle 'static
Typen (dh Typen, die keine nicht statischen Links enthalten) implementiert Rust den Any
Typ, der die Konvertierung des dyn Any
Typ-Objekts in einen Verweis auf den ursprünglichen Objekttyp ermöglicht:
let value = "test".to_string(); let value_any = &value as &dyn Any;
Ausführen
Box
hat zu diesem Zweck auch eine downcast
Methode.
Diese Lösung eignet sich für Fälle, in denen der Quellentyp am Arbeitsort bekannt ist. Aber was ist, wenn es nicht so ist? Was ist, wenn der aufrufende Code den Quelltyp des Objekts anstelle seiner Verwendung einfach nicht kennt? Dann müssen wir uns irgendwie an den Originaltyp erinnern, ihn dort ablegen, wo er definiert ist, und ihn zusammen mit dem dyn Any
Typ-Objekt speichern, damit dieser später an der richtigen Stelle in den Originaltyp konvertiert wird.
Verallgemeinerte Typen in Rust können als Typvariablen behandelt werden, an die der eine oder andere Typwert beim Aufruf übergeben werden kann. In Rust gibt es jedoch keine Möglichkeit, sich an diesen Typ für die weitere Verwendung an anderer Stelle zu erinnern. Es gibt jedoch eine Möglichkeit, sich alle Funktionen dieses Typs zusammen mit diesem Typ zu merken. Dies ist die Idee des Musters "Schließen eines verallgemeinerten Typs": Code, der einen Typ verwendet, wird in Form eines Verschlusses ausgeführt, der als normale Funktion gespeichert wird, da außer verallgemeinerten Typen keine Objekte der Umgebung verwendet werden.
Implementierung
Schauen wir uns ein Implementierungsbeispiel an. Angenommen, wir möchten einen rekursiven Baum erstellen, der eine Hierarchie von Grafikobjekten darstellt, in der jeder Knoten entweder ein Grafikprimitiv mit untergeordneten Knoten oder eine Komponente sein kann - ein separater Baum von Grafikobjekten:
enum Node { Prim(Primitive), Comp(Component), } struct Primitive { shape: Shape, children: Vec<Node>, } struct Component { node: Box<Node>, } enum Shape { Rectangle, Circle, }
Node
in der Component
ist erforderlich, da die Component
selbst im Node
.
Nehmen wir nun an, unser Baum ist nur eine Darstellung eines Modells, mit dem er verknüpft werden soll. Darüber hinaus hat jede Komponente ein eigenes Modell:
struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model,
Wir könnten schreiben:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), }
Dieser Code funktioniert jedoch nicht wie erforderlich. Weil die Komponente ein eigenes Modell haben muss und nicht das Modell des übergeordneten Elements, das die Komponente enthält. Das heißt, wir brauchen:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>,
Ausführen
Wir haben die Angabe eines bestimmten Modelltyps auf die new
Methode verschoben und in der Komponente selbst das Modell und den Teilbaum bereits mit gelöschten Typen gespeichert.
use_model
nun die use_model
Methode hinzu, die das Modell verwendet, jedoch nicht nach Typ parametrisiert wird:
struct Component { node: Box<dyn Any>, model: Box<dyn Any>, use_model_closure: fn(&Component), } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { let use_model_closure = |comp: &Component| { comp.model.downcast_ref::<Model>().unwrap(); }; Self { node: Box::new(node), model: Box::new(model), use_model_closure, } } fn use_model(&self) { (self.use_model_closure)(self); } }
Ausführen
Beachten Sie, dass wir in der Komponente einen Zeiger auf eine Funktion speichern, die in der new
Methode mithilfe der Syntax zum Definieren eines Abschlusses erstellt wird. Alles, was von außen erfasst werden sollte, ist der Model
. Daher müssen wir über ein Argument einen Link zur Komponente selbst an diese Funktion übergeben.
Es scheint, dass wir anstelle des Abschlusses eine interne Funktion verwenden können, aber ein solcher Code wird nicht kompiliert. Weil die interne Funktion in Rust keine verallgemeinerten Typen von der externen erfassen kann , da sie sich nur in der Sichtbarkeit von der üblichen Funktion der obersten Ebene unterscheidet.
Die use_model
Methode kann use_model
in einem Kontext verwendet werden, in dem der tatsächliche use_model
unbekannt ist. Zum Beispiel in einer rekursiven Baumdurchquerung, die aus vielen verschiedenen Komponenten mit verschiedenen Modellen besteht.
Alternative
Wenn es möglich ist, die Schnittstelle der Komponente auf einen Typ zu übertragen, mit dem ein Typobjekt erstellt werden kann, ist es besser, dies zu tun und stattdessen die Komponente selbst zu verwenden, um das Typobjekt zu bearbeiten:
enum Node<Model> { Prim(Primitive<Model>), Comp(Box<dyn ComponentApi>), } struct Component<Model> { node: Node<Model>, model: Model, } impl<Model> Component<Model> { fn new(node: Node<Model>, model: Model) -> Self { Self { node, model, } } } trait ComponentApi { fn use_model(&self); } impl<Model> ComponentApi for Component<Model> { fn use_model(&self) { &self.model; } }
Ausführen
Fazit
Es stellt sich heraus, dass Schließungen in Rust nicht nur Umgebungsobjekte, sondern auch Typen erfassen können. Sie können jedoch als gewöhnliche Funktionen interpretiert werden. Diese Eigenschaft ist nützlich, wenn Sie einheitlich mit verschiedenen Typen arbeiten müssen, ohne Informationen darüber zu verlieren, wenn Zeichentypen nicht anwendbar sind.
Ich hoffe, dieser Artikel hilft Ihnen bei der Verwendung von Rust. Teilen Sie Ihre Gedanken in den Kommentaren.