Notizen zu Rust.
Ein Ziel von Rust sind „Zero-Overhead Abstractions”. Damit sind Abstraktionen gemeint, die ohne vermeidbare Laufzeitkosten auskommen:
Gelegentlich wird dies auch mit „Zero-Cost” bezeichnet, was gelegentlich aber zu dem Missverständnis führt, dass durch die Abstraktionen überhaupt keine Laufzeitkosten entstehen.
Beispiele für Zero-Overhead Abstraktionen in Rust:
Cargo ermöglicht es recht einfach, eigene Libraries und Libraries anderer Entwickler („Crates”) zu nutzen.
Dass Rust nur eine minimale Standard Library anbietet, war eine bewusste Entscheidung.
Chancen:
Risiken:
Beispiele für nicht optimalen Code in Standard Libraries:
rand()
: TODOnet/http
: TODO Projekte wie grpc-go
verwenden eigene HTTP/2-Implementierungenjava.util.Calendar
, java.util.Date
) konnte erst mit Java 8 verbessert werdenasyncio
: TODO curio
, trio
http.server
: TODOsubprocess
: TODOurllib
, urllib2
: TODOstd::sync::mpsc
: die API hat sich über die Jahre als nicht optimal herausgestellt.
Die Implementierung wurde durch das unabhängig entwickelte
crossbeam-channel
ersetzt.Beispiele verbreiteter Libraries, die nicht Teil einer Standard Library sind:
boost
log4j
Spring
Django
Flask
Requests
lazy_static
vs once-cell
rustc_serialize
vs Serde
Datentypen können mittels Traits gruppiert werden. Traits können Methoden sowohl deklarieren, als auch default-implementieren. Member können in Traits jedoch nicht definiert werden.
Traits werden für einen Datentyp entweder in einfachen Fällen automatisch mittels
derive
oder manuell
mittels impl Trait for Datentyp { ... }
implementiert.
Trait-Deklarationen und -Implementierungen können sich in unterschiedlichen Crates befinden. Die Orphan Rule verbietet jedoch, dass ein Trait aus einem externen Crate für einen Datentyp aus einem externen Crate implementiert wird. Dadurch wird sichergestellt, dass es für jede Kombination aus Datentyp und Trait unmissverständlich nur eine einzige Implementierung gibt.
Auto Traits werden mit dem Keyword auto
markiert.
Falls er es für angebracht hält, implementiert der Compiler diese Traits für Datentypen automatisch.
Beispiele sind std::marker::Send
und std::marker::Sync
.
TODO: Blanket Implementations
Um eine Trait-Methode für einen Datentyp aufrufen zu können, muss neben dem Datentyp auch der Trait importiert werden.
Methoden können auch als freistehende Funktion aufgerufen werden.
Ist z.B. die Methode A::f(&self, b: B)
implementiert, so kann
sie für eine Instanz a
von A
nicht nur als a.f(b)
, sondern auch als A::f(a, b)
verwendet
werden.
Von statischem Polymorphismus spricht man, falls Trait-Methoden aufgerufen werden und der Datentyp
zur Kompilierzeit bekannt ist: impl Trait
.
Einschränkungen:
impl Trait
haben.Von dynamischen Polymorphismus spricht man, falls Trait-Methoden aufgerufen werden und der Datentyp
zur Kompilierzeit nicht bekannt ist: dyn Trait
.
Da die Größe des Datentyps, der dyn Trait
erfüllt, zur Kompilierzeit nicht bekannt ist (!Sized
, siehe Abschnitt
„Marker Traits”), kann dyn Trait
alleine nicht genutzt werden.
Vielmehr muss dyn Trait
in einen Wide Pointer verpackt werden (z.B. &dyn Trait
, Box<dyn Trait>
, Arc<dyn Trait>
etc.).
Ein solcher Wide Pointer wird „Trait Object” genannt und enthält neben der Adresse des Datentyps auch eine
Virtual Table mit den Adressen der Methoden-Implementierungen.
Ein Trait Object ist möglich, falls der Trait „object safe” ist:
Self
beziehenEinschränkungen:
Clone
nicht object safe ist. Als Workaround kann das Crate
dyn-clone genutzt werden.TODO
Traits können automatisch vereinigt werden: trait C: A+B {}
.
Datenstrukturen, sowie die für sie implementierten Methoden, können nicht automatisiert vereinigt werden.
TODO Bei generischen Typ-Parametern, Funktionsargumenten und -Return-Werten, sowie bei Trait-Definitionen.
Mehrere Traits können nur dann als Trait Bound einer Closure oder eines Trait Objects verwendet werden, wenn sie Auto Traits sind.
Marker Traits haben keine Methoden. Sie für einen Datentypen zu implementieren, bedeutet lediglich, den Compiler anzuweisen, diesen Datentyp besonders zu behandeln:
Copy
: implementiert ein Datentyp Copy
, so
kann er durch den Compiler implizit dupliziert werden.Send
: implementiert ein Datentyp Send
, so
kann höchstens 1 Thread exklusiven Zugang haben.Sized
:
Sized
, falls er eine zur Kompilierzeit
bekannte, endliche Größe hat.
Nur solche Datentypen können auf dem Stack abgelegt werden.
Für alle anderen können nur Pointer auf dem Stack abgelegt werden.Sized
, nicht jedoch bei Self
in Traits, weil es Trait Objects
verhindert.
?Sized
entfernt werden.Self: Sized
sein. TODOSync
: implementiert ein Datentyp Sync
, so
können mehrere Threads gleichzeitig geteilten Zugang haben.
Erfüllt ein Datentyp weder Send
noch Sync
, dann ist Zugang auf seine Instanz nur aus dem
Thread möglich, aus dem die Instanz stammt.Unpin
: TODODer Compiler validiert ggf. rekursiv, dass alle Bestandteile des Datentyps den Trait bzw. die Trait Bounds erfüllen.
Der kleinste Codebereich, in dem ein Datentyp instantiiert wird, ist der Besitzer (Owner) dieser Instanz. Nur der Besitzer hat auf sie Zugriff.
Der Besitz (Ownership) kann durch einen Move auf andere Codebereiche übergehen:
Zu jedem Zeitpunkt gibt es höchstens einen Besitzer. Der Compiler stellt sicher, dass Bindungen des vorherigen Besitzers nach einer Übertragung nicht mehr verwendet werden können.
Sobald Daten den besitzenden Codebereich verlassen, ohne dass sie von einem anderen Codebereich übernommen wurden, sind sie ungenutzt und ihr Speicherplatz kann und wird durch den Compiler freigegeben. Dies gilt auch für Freigabe anderer Ressourcen, wie geöffnete Dateien, Netzwerkverbindungen oder Locks.
Da immer höchstens ein Codebereich vollen Zugriff hat, sind Fehler aufgrund gleichzeitigen Zugriffs ausgeschlossen.
Die Besitzübertragung mittels Move ist außer bei primitiven Datentypen die Regel. Daten können vor der Übergabe dupliziert (geklont) werden, womit nur das Duplikat den Besitzer wechselt. Eine weitere Möglichkeit die Abgabe des Besitzes zu vermeiden ist das Borrowing (siehe nächster Abschnitt).
Die Duplizierung erfolgt über den Aufruf der Methode clone(), falls für den Datentyp der Trait Clone implementiert ist.
Ist für den Datentyp hingegen der Marker Trait Copy
implementiert, erfolgt die Duplizierung an den entsprechenden Stellen durch den Compiler implizit.
Ein expliziter Aufruf von clone()
ist dann unnötig.
rust-clippy
weißt auf unnötigen Aufrufe von clone()
hin.
Der Compiler implementiert Copy
mittels bitweisem Kopieren.
Bei Datentypen, wie String
bzw. Vec
, welche Pointer auf Bereiche im Heap halten, ist ein
bitweises Kopieren unzulässig, da die duplizierten Pointer zu Speicherfehlern führen würden.
Mittels Borrowing kann anderen Codebereichen temporärer Zugriff auf Daten gewährt (ausgeliehen) werden, ohne dass der Besitz abgegeben werden muss.
Referenzen erlauben Zugriff auf zwei Arten:
&T
: gewährt geteilten Zugriff&mut T
: gewährt exklusiven ZugriffEine Lifetime gibt an, wie lange eine Referenz eines bestimmten Speicherbereichs gültig ist. Sie endet spätestens mit dem Drop der Daten, aber auch bereits mit einem Move.
Der Borrow Checker des Compilers stellt dies sicher. Kann er nicht eindeutig erkennen, dass die Lifetimes eingehalten werden, lehnt er den Code im Zweifel ab. Ein fälschliches Akzeptieren von Code würde dessen Speichersicherheit gefährden.
Die Wahl zwischen Copy und Borrowing ist nicht immer trivial.
TODO:
std::convert::AsRef
: TODO
z.B. AsRef<Path>
.std::convert::From
: TODOstd::convert::Into
: TODOstd::ops::Deref
: TODOstd::borrow::Borrow
: TODOstd::borrow::Cow
: TODO
std::borrow::ToOwned
ist die
Umkehrfunktion.TODO Informed polling, cooperative scheduling.
async
-Funktionen und -Blöcke werden in eine State Machine gewrappt. Der
Returnwert-Typ ist nicht T
, sondern impl Future<Output=T> + '_
.Der Compiler kann nicht überprüfen:
.await
nicht ausgeführt wirdWenn async
Funktionen rekursiv aufgerufen werden sollen, muss der Result-Typ geboxed werden.
Andernfalls hätte der Return-Wert-Typ — ähnlich wie bei rekursiven Enums oder Structs — unendliche
Größe:
Send
Bound: BoxFuture,
zusammen mit
FutureExt::boxed()Send
Bound: LocalBoxFuture,
zusammen mit
FutureExt::boxed_local()async
Closures stehen in Stable Rust
noch nicht zur Verfügungasync
sein.
async
Drop
noch nicht möglich.Send
Trait Bound kann man verhindern durch #[async_trait(?Send)]
bei Deklaration und Implementation.async
:
async
-Funktionen und -Blöcke werden in eine State Machine gewrappt. Der
Returnwert-Typ ist nicht T
, sondern impl Future<Output=T> + '_
.Deref
-Coercion:
deref()
aus dem Trait Deref wird vom Compiler implizit aufgerufen.drop()
wird
implizit aufgerufen, sobald der Wert out-of-scope geht.for
-Schleifen: rufen implizit
die Methode into_iter() des
Traits std::iter::IntoIterator auf.Mit bestimmtem Code kann der Compiler dazu genutzt werden, Eigenschaften von Datentypen entweder zu bestätigen oder mittels Fehlermeldungen zu erklären.
Erklären lassen:
a
:
let _: () = a;
Bestätigen:
T
:
fn assert_object_safety(a: Box<dyn T>) {}
A
:
fn assert_send<T: Send>() {}; assert_send::<A>();
fn assert_sync<T: Sync>() {}; assert_sync::<A>();
A
:
fn assert_contravariance<'a>(a: A<'l>) -> A<'static> { a }
fn assert_covariance<'a>(a: A<'static>) -> A<'l> { a }
Smart Pointer erlauben, Einschränkungen des Borrow Checkers zu umgehen.
„Nicht thread-safe“ bedeutet in Rust, dass der Code nicht kompilieren würde, falls versucht wird, nicht thread-safe Datenstrukturen zwischen Threads auszutauschen.
std::cell
:
&
anstatt unique Referenzen &mut
):
clone()
Cell
:
UnsafeCell
RefCell
:
UnsafeCell
std::rc
:
Rc
:
Weak
:
std::sync
:
Arc<Mutex<T>>
: TODORc<RefCell<T>>
: TODOBox
, Vec
nicht möglich. Für Box
gibt es das experimentelle Attribut
#![feature(box_syntax, box_patterns)].TODO
TODO
cargo tarpaulin --ignore-tests
TODO
cargo tree
: erleichtert zu erkennen, falls ein Crate in verschiedenen Versionen
gleichzeitig verwendet wird.cargo-udeps
: findet unbenutzte eigene Abhängigkeitencargo update
cargo outdated
: findet neuere Versionen der
eigenen Abhängigkeitencargo bloat --time --release -j 1
cargo check
verwenden statt
cargo build
Cargo.toml
im Abschnitt [profile.dev]
definieren:
split-debuginfo = \"unpacked\"
: TODOCargo.toml
im Abschnitt [profile.release]
definieren:
codegen-units = 1
: TODOlto = \"fat\"
: TODO-C target-cpu=native
: TODOpanic = \"abort\"
: TODOstrip=\"symbols\"
: TODOunsafe
:..