Typgestützte Programmierung
Das Konzept der typestates beschreibt die Kodierung von Informationen über den aktuellen Zustand eines Objekts direkt in dessen Typ. Auch wenn dies zunächst etwas abstrakt oder kompliziert klingen mag: Wenn Sie in Rust bereits das Builder-Muster verwendet haben, sind Sie schon mit der Typestate-Programmierung in Berührung gekommen!
pub mod foo_module {
#[derive(Debug)]
pub struct Foo {
inner: u32,
}
pub struct FooBuilder {
a: u32,
b: u32,
}
impl FooBuilder {
pub fn new(starter: u32) -> Self {
Self {
a: starter,
b: starter,
}
}
pub fn double_a(self) -> Self {
Self {
a: self.a * 2,
b: self.b,
}
}
pub fn into_foo(self) -> Foo {
Foo {
inner: self.a + self.b,
}
}
}
}
fn main() {
let x = foo_module::FooBuilder::new(10)
.double_a()
.into_foo();
println!("{:#?}", x);
}In diesem Beispiel gibt es keine direkte Möglichkeit, ein Foo-Objekt zu erstellen. Wir müssen einen FooBuilder erstellen und ihn korrekt initialisieren, bevor wir das gewünschte Foo-Objekt erhalten können.
Dieses Minimalbeispiel kodiert zwei Zustände:
FooBuilder, das einen „unkonfigurierten“ Zustand oder einen Zustand der „laufenden Konfiguration” repräsentiertFoo, das einen „konfigurierten“ oder „einsatzbereiten” Zustand repräsentiert.
Starke Typen
Da Rust über ein starkes Typsystem verfügt, gibt es keine einfache Möglichkeit, auf magische Weise eine Instanz von Foo zu erzeugen oder einen FooBuilder in ein Foo umzuwandeln, ohne die Methode into_foo() aufzurufen. Zudem verbraucht der Aufruf der Methode into_foo() die ursprüngliche FooBuilder-Struktur; das bedeutet, dass sie nicht ohne die Erstellung einer neuen Instanz wiederverwendet werden kann.
Dies ermöglicht es uns, die Zustände unseres Systems als Typen darzustellen und die für Zustandsübergänge erforderlichen Aktionen in jene Methoden zu integrieren, die einen Typ gegen einen anderen austauschen. Indem wir einen FooBuilder erstellen und diesen gegen ein Foo-Objekt austauschen, haben wir die Schritte einer einfachen Zustandsmaschine durchlaufen.