Häufig gestellte Fragen
Auf dieser Seite werden häufig gestellte Fragen über die Programmiersprache Rust beantwortet. Die Seite ist keine vollständige Anleitung und zum Lernen der Sprache ungeeignet. Sie versucht, Antworten auf Fragen zu geben, welche in der Rust-Community immer wieder aufgetreten sind, und verdeutlicht einige Überlegungen hinter den Design-Entscheidungen zu Rust.
Wenn dir auffällt, dass hier eine wichtige oder verbreitete Frage fehlt, dann hilf uns, das zu ändern.
Das Rust-Projekt
Was ist das Ziel des Rust-Projekts?
Das Ziel des Rust-Projekts ist es, eine sichere, nebenläufige und praktikable Systemsprache zu implementieren.
Rust existiert, weil andere, ähnlich abstrakte und effiziente Sprachen folgende Erwartungen nicht erfüllen:
- Es wird zu wenig auf Sicherheit geachtet.
- Die Unterstützung für Nebenläufigkeit ist mangelhaft.
- Für manche Sprachmerkmale gibt es keine praktische, einleuchtende Benutzung.
- Es gibt nur eingeschränkte Kontrolle über Ressourcen.
Rust existiert als eine Alternative, welche sowohl effizienten Code als auch ein komfortables Abstraktionsniveau bietet, und dabei für jeden dieser vier Punkte Verbesserungen vorschlägt.
Wird dieses Projekt von Mozilla kontrolliert?
Nein. Im Jahr 2006 startete Graydon Hoare Rust als Teilzeitprojekt, und so blieb es für 3 Jahre. Erst als Rust 2009 die nötige Reife erreichte, um Kernkonzepte anhand einfacher Tests zu demonstrieren, beteiligte sich Mozilla. Rust wird vorrangig von Mozilla unterstützt, aber eine vielfältige, enthusiastische Community in der ganzen Welt arbeitet daran. Das Rust-Team besteht sowohl aus Mitarbeitern von Mozilla als auch aus unabhängigen Mitgliedern, und rust
auf GitHub hatte bisher schon über 1,900 verschiedene Mitwirkende.
Die Führung des Projektes besteht aus einem Kernteam, welches die Vision und die Prioritäten festlegt und das Projekt von einer globalen Perspektive aus leitet. Es gibt auch kleinere Teams, welche sich um die Entwicklung bestimmter Interessenbereiche kümmern. Das können zum Beispiel der Kern der Sprache, der Compiler, Rust-Bibliotheken, Tooling oder die Moderation der offiziellen Rust-Communities sein. Fortschritt in jedem dieser Bereiche wird über einen RFC-Prozess erreicht. Änderungen, für welche kein RFC notwendig ist, werden normalerweise in einer Pull Request im rustc
Repository diskutiert.
Welche Ziele werden nicht angestrebt?
- Wir verwenden keine besonders modernen Technologien. Alte, etablierte Technologien sind besser.
- Ausdrucksstärke, Minimalismus oder Eleganz sind wünschenswerte, aber untergeordnete Ziele.
- Wir beabsichtigen nicht, alle Eigenschaften und Möglichkeiten von C++ oder anderen Sprachen abzudecken. Rust sollte die häufigsten Fälle abdecken.
- Wir beabsichtigen nicht, 100% statisch, 100% sicher, 100% reflektiv oder zu dogmatisch in irgendeiner anderen Hinsicht zu sein. Es gibt Kompromisse.
- Wir zielen nicht darauf ab, dass Rust auf jeder möglichen Plattform läuft. Rust soll ohne unnötige Kompromisse auf üblichen, verbreiteten Hardware- und Softwareplattformen laufen.
In welchen Projekten nutzt Mozilla Rust?
Hauptsächlich wird Rust in Servo, einer experimentellen Browser Engine, an welcher Mozilla arbeitet, verwendet. Mozilla arbeitet auch daran, weitere Rust-Komponenten in Firefox zu integrieren.
Welche großen Rust-Projekte gibt es?
Die zwei im Moment größten quelloffenen Rust-Projekte sind Servo und der Rust-Compiler selbst.
Wer benutzt sonst noch Rust?
Eine wachsende Zahl von Organisationen!
Wie kann ich Rust einfach ausprobieren?
Der einfachste Weg, Rust auszuprobieren, ist der Playpen - eine Online-Applikation, in welcher man einfach Rust-Code schreiben und Ausführen kann. Wenn du Rust auf deinem eigenen System ausprobieren willst, installiere es und gehe das Guessing Game-Tutorial im Buch durch.
Wie bekomme ich bei Problemen mit Rust Hilfe?
Es gibt viele Wege. Du kannst:
- Einen Forenpost im offiziellen Rust User Forum users.rust-lang.org absetzen.
- Im offiziellen Rust-IRC-Kanal (#rust on irc.mozilla.org) eine Frage stellen.
- Auf Stack Overflow eine Frage stellen (denke daran, sie mit dem „rust“-Tag zu markieren!).
- Im inoffiziellen Rust-Subreddit /r/rust posten.
Warum hat sich Rust über die Zeit so stark verändert?
Das ursprüngliche Ziel von Rust war es, eine sichere, aber einfach zu benutzende Systemprogrammiersprache zu erstellen. Um dieses Ziel zu erreichen, verfolgte Rust eine Vielzahl von Ideen, von denen es manche behielt (Lifetimes, Traits) und andere wieder verwarf (das Typestate-System oder Green Threading). Außerdem wurde ein großer Teil der Rust-Standardbibliothek vor der Veröffentlichung der Version 1.0 neu geschrieben, um mithilfe der Features von Rust eine qualitativ hochwertige, konsistente, plattformübergreifende API anzubieten. Jetzt, wo Rust die Version 1.0 erreicht hat, wird die Stabilität der Sprache garantiert. Obwohl sich die Sprache weiter entwickelt wird Code, der auf aktuellen Versionen von Rust funktioniert, auch in zukünftigen Versionen des Compilers gültig sein.
Wie funktioniert Versionierung in Rust?
Die Sprachversionierung von Rust folgt SemVer. Änderungen an stabilen APIs, welche die Abwärtskompatibilität nicht gewährleisten, sind in minor-Versionen nur erlaubt, wenn sie Fehler oder Sicherheitslücken im Compiler beheben, oder wenn die Änderungen weitere Annotationen für Dispatch oder Typinferenz erforderlich machen. Weitere, detailreichere Richtlinien für minor-Versionsänderungen sind als genehmigte RFCs sowohl für die Sprache als auch die Standardbibliotheken zu finden.
Es gibt drei Veröffentlichungskanäle für Rust: Stable, Beta, und Nightly. Die Kanäle Stable und Beta werden alle sechs Wochen aktualisiert, wobei der aktuelle Nightly zur neuen Beta und die aktuelle Beta das neue Stable wird. Teile der Standardbibliotheken sind als unstable markiert oder durch Feature Gates abgeschirmt. Diese können ausschließlich im Nightly-Kanal verwendet werden. Neue Features sind solange als unstable markiert, bis das Kernteam und zuständige Unterteams ihre Zustimmung zur Freigabe gegeben haben. Diese Herangehensweise erlaubt es Entwicklern, zu experimentieren, ohne die Garantie auf Abwärtskompatibilität zu gefährden.
Mehr Details finden sich im Blogpost “Stability as a Deliverable”.
Kann ich auf dem Beta- oder Stable Channel Features aus dem Unstable Channel verwenden?
Nein, das ist unmöglich. An Rust wird hart gearbeitet, um die Stabilität der Beta- und Stable Kanäle zu gewährleisten. Wir wollen nicht, dass sich jemand auf unstabile Features verlässt, für welche wir keine Stabilität gewährleisten und welche sich jederzeit ändern könnten. Das gibt uns auch die Möglichkeit, Änderungen im Nightly-Kanal realistisch auszutesten, während der Beta- und Stable Kanal stabil und unverändert bleiben.
Alle sechs Wochen werden die Beta- und Stable Kanäle mit den stabilisierten Features aktualisiert. Im Nightly-Kanal gibt es häufig stabilisierende Updates, während die anderen Kanäle seltener Fixes akzeptieren. Wenn du darauf wartest, dass ein Feature im Beta- oder Stable Kanal bereitgestellt wird, dann kannst du die zugehörige Issue mit dem Tag B-unstable
auf dem Issue Tracker finden.
Was sind Feature Gates?
Feature Gates sind der Sprachmechanismus, den Rust verwendet, um Features des Compilers, der Sprache und der Standardbibliotheken zu stabilisieren. Ein Feature hinter einem Gate ist nur im Nightly-Kanal verfügbar, und auch nur dann, wenn es explizit durch ein #[feature]
-Attribut oder das Kommandozeilenargument -Z unstable-options
angefordert wurde. Wenn ein Feature stabilisiert und in den Stable-Kanal übernommen wird, muss es nicht mehr explizit angefordert werden. Dann wird dieses Feature als ungated bezeichnet. Feature Gates erlauben es den Entwicklern, zu experimentieren, während sie in der Entwicklung sind. Erst wenn die Entwickler sich auf eine Implementierung festlegen, halten die Features in der stabilen Sprache Einzug.
Warum eine MIT-ASL2 Doppellizenz?
Die Apache-Lizenz enthält wichtigen Schutz gegen Patentaggressoren, aber ist mit der GPLv2 inkompatibel. Um Probleme bei der Verwendung von Rust mit der GPLv2-Lizenz zu vermeiden, ist es alternativ MIT-lizenziert.
Warum eine BSD-artige Freizügige Lizenz anstelle von MPL oder einer dreifachen Lizenz?
Das liegt zu einem Teil an einer persönlichen Vorliebe des originalen Entwicklers Graydon Hoare, zum anderen daran, dass Programmiersprachen im Gegensatz zu Produkten wie Webbrowsern normalerweise einen weiter gefächerten Einflussbereich und ein vielseitigeres Einsatzgebiet haben. Wir würden gerne möglichst viele dieser potenziellen Mitwirkenden anziehen.
Performance
Wie schnell ist Rust?
Sehr schnell! Rust kann sich bereits in einigen Benchmarks (zu Beispiel dem Benchmarks Game und anderen) mit idiomatischem C und C++ Code messen.
Eins der wichtigsten Prinzipien in Rust (wie auch in C++) sind Zero-Cost Abstractions: Keine der Abstraktionen in Rust verursachen programmweite Verlangsamungen oder einen Mehraufwand zur Laufzeit.
Da Rust auf LLVM aufbaut und deshalb auch versucht, Clang-kompatiblen Code zu generieren, sind Leistungsverbesserungen in LLVM auch für Rust vorteilhaft. Langfristig sollten die detaillierten Informationen des Typsystems Optimierungen ermöglichen, welche in C/C++ unmöglich wären.
Gibt es in Rust einen Garbage Collector?
Nein. Eine der Schlüsselinnovationen von Rust ist es, ohne Garbage Collection Speichersicherheit (keine Segfaults) zu garantieren.
Rust erlangt dadurch einige Vorteile: Vorhersagbare Bereinigung von Ressourcen, niedrige Mehrkosten für Speichermanagement und ein minimales Laufzeitsystem. Diese Eigenschaften ermöglichen es, Rust in beliebige Umgebungen einzubinden, und vereinfachen die Integration von Rust in Sprachen mit GC.
Das Ownership und Borrowing System ermöglicht nicht nur Speichersicherheit ohne einen GC, sondern ist auch in anderen Zusammenhängen nützlich, zum Beispiel für allgemeines Ressourcenmanagement und Nebenläufigkeit.
In Fällen, in denen einfache Ownership-Semantik nicht genügt, nutzen Rust-Programme den üblichen Referenzzähler/Smart Pointer-Typ Rc
und sein Thread-sicheres Gegenstück Arc
.
Es wird jedoch daran gearbeitet, in Zukunft einen optionalen Garbage Collector als Erweiterung anzubieten, um eine gute Integration mit Laufzeitumgebungen wie Spidermonkey und V8 zu ermöglichen, welche Garbage Collection verwenden.
Es gibt auch experimentelle, in purem Rust implementierte Kollektoren, welche ohne Unterstützung des Compilers funktionieren.
Warum ist mein Programm so langsam?
Der Rust-Compiler optimiert Programme nur dann, wenn man das explizit anfordert, da Optimierungen die Kompiliergeschwindigkeit verringern und allgemein während der Entwicklung unerwünscht sind.
Wenn du mit cargo
kompilierst, nutze die --release
option. Wenn du dein Program direkt mit rustc
erstellst, nutze die Option -O
. Beide Optionen schalten Optimierungen ein.
Warum ist die Erstellung meines Programmes so zeitaufwändig?
Rustc übersetzt und optimiert Code. Die High-Level-Abstraktionen in Rust kompilieren zu effizientem Maschinencode, und um diese Übersetzungen durchzuführen, wird Zeit benötigt - insbesondere beim Optimieren.
Die Übersetzungszeit ist jedoch besser als sie scheint und es gibt Anlass zur Hoffnung, dass sie sich noch verbessern wird. Kompilierzeiten von ähnlich umfangreichen Rust- und C++ Projekten sind im Allgemeinen miteinander vergleichbar.
Die häufige Auffassung, dass Rust langsam kompiliert, kommt zum Großteil aus dem Unterschied zwischen dem Kompiliermodell von C++ und Rust: Eine Kompiliereinheit in C++ ist eine Datei, während in Rust immer ein gesamter Crate kompiliert wird, welche aus vielen Dateien bestehen kann. Eine einzelne Datei während der Entwicklung zu verändern führt in C++ normalerweise zu weniger Rekompilation als in Rust. Es wird derzeit viel Arbeit in inkrementelle Programmerstellung investiert, welche die Vorteile des Modells von C++ in Rust übernimmt.
Neben dem Kompilationsmodell gibt es andere Aspekte des Sprachdesigns und der Compilerimplementation, welche die Übersetzungsleistung beeinflussen.
Rust hat zunächst ein relativ komplexes Typsystem, und der Compiler muss einige Zeit darauf verwenden, die Typbedingungen zu überprüfen, welche Rust zur Laufzeit absichern.
Außerdem sind einige Teile des Rust-Compilers ziemlich veraltet. Diese generieren insbesondere LLVM-IR niedriger Qualität, welche LLVM erst „reparieren“ muss. Es gibt Hoffnung, dass zukünftige, MIR-basierte Übersetzungs- und Optimierungsdurchläufe die Arbeit für LLVM leichter machen.
Drittens hat die Nutzung von LLVM auch ihre Kosten: Rust hat dadurch hohe Leistung zur Laufzeit, aber LLVM ist ein großes Framework das nicht auf hohe Leistung zur Übersetzungszeit fokussiert ist, insbesondere bei Eingaben mit mangelhafter Qualität.
Letztlich führt auch die übliche Strategie der Monomorphisierung von Generics (wie in C++) zwar zu schnellem Code zur Laufzeit, aber sie erfordert die Erzeugung von Signifikant mehr Code als andere Strategien. Durch die Verwendung von Trait-Objekten können Rust-Programmierer diese Aufblähung vermeiden, müssen dann aber auf späte Bindung mit ihren bekannten Nachteilen zurückgreifen.
Warum ist die HashMap
in Rust so langsam?
Standardmäßig nutzt HashMap
den SipHash-Algorithmus, der entworfen wurde, um Kollisionsattacken auf Hashtabellen zu verhindern und dabei trotzdem für die häufigsten Anwendungsfälle gute Leistung zu erzielen.
Obwohl SipHash in vielen Fällen hohe Leistung vorweisen kann, ist der Algorithmus für Anwendungsfälle mit kurzen Schlüsseln wie Ganzzahlen merklich langsamer. Deshalb wird von Rust-Programmierern häufig niedrige Leistung bei der Verwendung einer HashMap
beobachtet. In solchen Fällen wird häufig der FNV-Hasher empfohlen, der aber nicht die Kollisionsresistenz von SipHash vorweisen kann.
Warum gibt es keine integrierte Benchmarking-Infrastruktur?
Es gibt eine, welche aber nur im Nightly-Kanal verfügbar ist. Wir planen ein modulares System, welches integrierte Leistungstests ermöglicht. In der Zwischenzeit wird das System als instabil eingeschätzt.
Unterstützt Rust Tail Call Elimination?
Im Allgemeinen nicht. Optimierung von Endrekursion kann unter bestimmten Vorraussetzungen erfolgen, das ist aber nicht gewährleistet. Da die Optimierung ein vielfach erwünschtes Sprachmerkmal ist, wurde das Schlüsselwort become
dafür reserviert, wobei allerdings die technische Umsetzbarkeit noch nicht geklärt ist. Eine vorgeschlagene Erweiterung, welche Tail Call Elimination ermöglichen würde, wurde vorgeschlagen, zunächst aber verschoben.
Hat Rust ein Laufzeitsystem?
Nicht im Sinne von gängigen Sprachen wie Java. Teile der Standardbibliotheken könnte man als “Laufzeitsystem” bezeichnen, die einen Heap, Backtraces, Unwinding, und Stack Guards anbietet. Eine relativ kleine Menge Initialisierungscode läuft vor der main
-Funktion des Benutzers. Die Standardbibliotheken linken außerdem gegen die C-Standardbibliotheken, welche ebenfalls eine ähnliche Laufzeitinitialisierung vornehmen. Rust kann ohne die Standardbibliotheken kompiliert werden, dann ist die Laufzeitumgebung äquivalent zu der von C.
Syntax
Warum geschwungene Klammern? Warum kann die Syntax von Rust nicht Haskell oder Python ähnlicher sein?
Die Benutzung geschweifter Klammern ist eine Entwurfsentscheidung, welche eine Vielzahl von Programmiersprachen getroffen haben. Da Rust hier konsistent bleibt, ist es für in anderen Sprachen erfahrene Programmierer einfacher zu lernen.
Geschweifte Klammern ermöglichen dem Programmierer eine flexible Syntax und dem Kompilierer einen einfacheren Parser.
Ich kann die Klammern um if
-Bedingungen weglassen, warum muss ich sie dann um einzeilige Blöcke setzen? Warum ist der C-Stil nicht erlaubt?
Während C Klammerung um ein if
-Statement aber keine Klammern für einzeilige Blöcke erfordert, trifft Rust die genau entgegengesetzte Wahl. Das trennt die Bedingung klar vom Block und vermeidet die Gefahren der optionalen Klammern, welche zu leicht übersehbaren Fehlern wie Apples goto-fail-Bug führen können.
Warum gibt es keine Syntax für Dictionary-Literale?
Die bevorzugte Vorgehensweise beim Entwurf von Rust war es, die Sprache selbst relativ klein zu halten und dafür mächtige Bibliotheken anzubieten. Rust bietet zwar Literale zur Initialisierung von Arrays und Strings an, aber diese sind die einzigen in die Sprache eingebauten Collection-Typen. Andere, in Bibliotheken definierte Typen, wie zum Beispiel der häufig genutzte Vec
Collection-Typ erlauben die Initialisierung durch Makros wie vec!
.
In der Zukunft wird die Design-Entscheidung, Makros zum Initialisieren von Datenstrukturen zu verwenden, wahrscheinlich auf weitere Datentypen erweitert werden. Zusätzlich zu HashMap
und Vec
sollen Typen wie BTreeMap
unterstützt werden. Wenn du jetzt schon komfortablere Syntax zur Initialisierung von Datenstrukturen benötigst, kannst du dafür dein eigenes Makro definieren.
Wann sollte ich ein implizites Return verwenden?
Rust ist eine stark Ausdruck-orientierte Sprache, und implizite Returns gehören zu diesem Design. Konstrukte wie if
, match
, oder normale Blöcke sind in Rust Ausdrücke. Der folgende Programmcode testet zum Beispiel, ob ein i64
ungerade ist und liefert das Ergebnis mit einem impliziten Return zurück.
fn is_odd(x: i64) -> bool {
if x % 2 != 0 { true } else { false }
}
Das kann natürlich weiter vereinfacht werden:
fn is_odd(x: i64) -> bool {
x % 2 != 0
}
In beiden Beispielen ist die letzte Zeile der Rückgabewert der Funktion. Ein wichtiges Detail ist, dass der Rückgabetyp einer Funktion, welche mit einem Semikolon endet, ()
ist. Dies deutet an, dass kein Wert zurückgegeben wird. Implizite Rückgaben funktionieren nur ohne abschließendes Semikolon, da sonst der Wert des Ausdrucks unterdrückt wird.
Explizite Rückgaben müssen dann benutzt werden, wenn implizite unmöglich sind, zum Beispiel wenn man vor dem Ende des Funktionskörpers einen Wert zurückgeben will. Beide Funktionen im obigen Beispiel hätten mit einem return
und einem Semikolon geschrieben werden können, aber das wäre unnötig ausführlich und gegen die Konventionen von gutem Rust-Code.
Warum werden Funktionssignaturtypen nicht vom Compiler hergeleitet?
Deklarationen in Rust werden normalerweise mit expliziten Typannotationen versehen, während der eigentliche Programmcode mit inferierten Typen arbeitet. Diese Entscheidung ist folgendermaßen begründet:
- Erforderliche Signaturdeklarationen helfen, die Stabilität von Schnittstellen in Modulen und Crates zu gewährleisten.
- Signaturtypen erleichtern dem Programmierer das Verständnis des Programms. Dadurch, dass die Signaturtypen immer explizit lokal im Programm definiert sind, muss eine IDE keinen Inferenzalgorithmus über den gesamten Crate laufen lassen, um den Typ eines Argumentes herauszufinden.
- Da die Argumenttypen auf Funktionsebene festgelegt sind, kann der Inferenzalgorithmus stark vereinfacht werden.
Warum muss ein match
alle Fälle abdecken?
Um Klarheit zu schaffen und Refactoring zu vereinfachen.
Erstens: wenn ein match
jeden Fall eines enum
s abdeckt, führt das zukünftige Hinzufügen einer Variante zu einem Kompilierfehler und nicht einem Fehler zur Laufzeit. Diese Hilfe durch den Kompilierer ermöglicht es dem Rust-Programmierer, zu Refakturisieren, ohne neue Fehler befürchten zu müssen.
Zweitens: abdeckende Prüfung aller Fälle expliziert die Semantik des default-Falles. Allgemein wäre ein nicht-abdeckendes match
nur sicher, wenn der Thread im Fall einer unvorhergesehenen Variante ein panic
auslösen würde. In frühen Versionen von Rust, in denen match
nicht zwingend alle Fälle abdecken musste, wurde dies als Ursache für viele Fehler befunden.
Mit _
kannst du einfach alle weiteren, unspezifizierten Fälle ignorieren:
match val.do_something() {
Cat(a) => { /* ... */ }
_ => { /* ... */ }
}
Arithmetik
Soll ich für mathematische Operationen mit Gleitkommazahlen f32
oder f64
verwenden?
Diese Wahl hängt vom Zweck des Programmes ab.
Wenn du für deine Gleitkommazahlen die größtmögliche Genauigkeit benötigst, dann nutze f64
. Wenn du Speicher- oder Rechenzeiteffizienz benötigst und dafür Abstriche in der Genauigkeit machen kannst (da du pro Wert weniger Bits zur Darstellung zur Verfügung hast), dann ist f32
besser. Auch auf 64-Bit Hardware sind Operationen mit f32
normalerweise schneller. Ein häufiges Beispiel findet man in der Grafikprogrammierung: hier wird typischerweise f32
verwendet, da hohe Leistung nötig ist und 32-Bit Gleitkommazahlen zur Darstellung von Pixeln auf dem Bildschirm ausreichen.
Wähle im Zweifel f64
, um bessere Präzision zu erreichen.
Warum kann ich keine Gleitkommazahlen vergleichen oder sie als Schlüsseltypen für HashMap
oder BTreeMap
verwenden?
Gleitkommazahlen können mit den Operatoren ==
, !=
, <
, <=
, >
, >=
, sowie mit der Funktion partial_cmp()
verglichen werden. ==
and !=
sind Teil des PartialEq
-Traits, während <
, <=
, >
, >=
, und partial_cmp()
Teil des PartialOrd
-Traits sind.
Gleitkommazahlen können nicht mit der Funktion cmp()
verglichen werden, welche Teil des Ord
Traits ist, da es für Gleitkommazahlen keine totale Ordnung gibt. Genauso gibt es keine totale Gleichheitsrelation, weswegen Gleitkommazahlen auch nicht den Eq
-Trait implementieren. Der Grund ist, dass der Gleitkommawert NaN
nicht gleich, kleiner als oder größer als irgendeine anderen Gleitkommazahl (oder sogar NaN
selbst) ist.
Da Gleitkommazahlen weder Eq
noch Ord
implementieren, können sie nicht für Typen verwendet werden, deren Trait-Bounds diese Eigenschaften verlangen, wie zum Beispiel BTreeMap
oder HashMap
. Das ist wichtig, da diese Typen annehmen, dass ihre Schlüssel totale Ordnung oder totale Gleichheit anbieten - anderenfalls würden sie nicht richtig funktionieren.
Es gibt einen Crate, welche f32
und f64
um eine Implementierung von Ord
und Eq
erweitert, die in manchen Fällen nützlich sein kann.
Wie kann ich zwischen numerischen Typen umwandeln?
Es gibt zwei Möglichkeiten: Das as
Schlüsselwort, welches einfache Typumwandlung für primitive Typen vollzieht, und die Traits Into
und From
, welche für einige Typkonversionen implementiert sind (und welche du für eigene Typen selbst implementieren kannst). Die Into
und From
-Traits sind nur in Fällen implementiert, in denen eine verlustfreie Umwandlung möglich ist. Zum Beispiel wird f64::from(0f32)
kompilieren, f32::from(0f64)
aber nicht. Das Schlüsselwort as
hingegen wandelt alle primitiven Typen untereinander um und schneidet wenn nötig deren Werte ab.
Warum kennt Rust keine Inkrement- und Dekrementoperatoren?
Präinkrement und Postinkrement sowie ihre Gegenstücke für Dekrement sind zwar komfortabel, aber auch relativ komplex. Sie erfordern Kenntnis der Ausführungsreihenfolge und können in C und C++ leicht zu subtilen Fehlern oder undefiniertem Verhalten führen. x = x + 1
oder x += 1
sind nur geringfügig länger und dafür eindeutig.
Strings
Wie kann ich einen String
oder Vec<T>
in einen Slice konvertieren (&str
und &[T]
)?
Normalerweise kannst du eine Referenz zu einem String
oder Vec<T>
immer dort übergeben, wo ein Slice passend wäre.
Mithilfe von Deref Coercions können String
s und Vec
s automatisch in ihren jeweiligen Slice-Typen „zerfallen“, wenn man eine Referenz darauf mit &
oder &mut
übergibt.
Methoden, die auf &str
oder &[T]
implementiert wurden, können auf String
und Vec<T>
direkt aufgerufen werden. Der Aufruf some_string.trim()
zum Beispiel funktioniert, obwohl trim
eine Methode von &str
und some_string
ein String
ist.
Manchmal, zum Beispiel in generischem Code, wird manuelle Konversion notwendig. Diese kann man mit dem Slicing-Operator &my_vec[..]
erreicht werden.
Wie kann man &str
in String
und umgekehrt umwandeln?
Die Methode to_string()
wandelt einen &str
zu einem String
um, und String
s werden automatisch zu &str
umgewandelt, wenn man per Borrowing eine Referenz auf sie übergibt. Folgendes Beispiel soll beide Fälle veranschaulichen:
fn main() {
let s = "Jane Doe".to_string();
say_hello(&s);
}
fn say_hello(name: &str) {
println!("Hello {}!", name);
}
Worin unterscheiden sich die beiden String-Typen?
String
ist ein auf dem Heap allokierter Buffer von UTF-8 Bytes, welcher durch eine ‘owned’-Referenz gebunden ist. Veränderbare (mutable) String
s können modifiziert werden, wobei ihre Kapazität angepasst wird. &str
ist lediglich eine “Einsicht” mit fester Größe in einen String
. Dieser String
ist normalerweise auf dem Heap allokiert, wenn die Slice durch Dereferenzierung eines String
s entstanden ist. Er kann auch im statischen Speicher liegen, wenn der String ein Literal aus dem Programmcode ist.
&str
ist ein primitiver Typ der Sprache Rust, während String
in der Standardbibliothek definiert ist.
Wie kann ich in O(1) auf ein Zeichen in einem String
zugreifen?
Das ist unmöglich, außer wenn über den String im Vornherein so viel bekannt ist, dass der Index eines Zeichens genau berechnet werden kann. comment <> (You cannot. At least not without a firm understanding of what you mean by “character”, and preprocessing the string to find the index of the desired character)
In Rust sind Strings in UTF-8-kodiert. In ASCII wäre ein einzelnes Zeichen auch genau ein Byte, aber dies ist in UTF-8 nicht zwingend der Fall. Ein Byte wird Code Unit genannt (in UTF-16 sind Code Units 2 Bytes lang; in UTF-32 sind es 4 Bytes). Ein Code Point besteht aus einem oder mehreren Code Units, und mehrere Code Points formen Grapheme Cluster, welche am ehesten als Zeichen interpretiert werden können.
Obwohl man einfach auf den Bytes eines UTF-8 Strings indizieren könnte, kann man nicht in konstanter Zeit den i
ten Code Point oder Grapheme Cluster erreichen, da sie alle unterschiedlich lang sein können. Es ist allerdings möglich, einen konkreten Grapheme Cluster oder Code Point zu erreichen, wenn man genau weiß, wo er beginnt.
Funktionen wie str::find()
und Regex-Matches geben Byte-Indizes an, wodurch dieser Zugriff auf Byte-Ebene ermöglicht wird.
Warum sind Strings standardmäßig UTF-8-kodiert?
Der str
-Typ ist UTF-8-kodiert, weil Text (vor allem in Endian-agnostischen Netzwerkübertragungen) sehr häufig in dieser Form vorkommt. Wir sind der Meinung, dass Eingabe/Ausgabe standardmäßig nicht das Neukodieren von Text erfordern sollte.
Das bedeutet, dass auf einen spezifischen Code Point in einem String nur durch eine O(n)-Operation zugegriffen werden kann (wobei ein Byte an bekannter Position natürlich weiterhin nur O(1) kostet). Einerseits ist das ein unerwünschter Nachteil; Andererseits ist dieses Problem voller Abwägungen und Trade-Offs. Ein paar wichtige Merkmale:
Einen str
nach ASCII-Codepoints zu durchsuchen kann immer noch sicher Byte für Byte geschehen. Mit .as_bytes()
kann man mit O(1)-Kosten einen u8
gewinnen, welcher zu einem ASCII-char
umgewandelt oder mit einem ASCII-char
verglichen werden kann. Durch das gute Design von UTF-8 kann zum Beispiel ein '\n'
-Byte weiterhin als Zeilenumbruch interpretiert werden.
Die meisten „zeichenorientierten“ Operationen auf Text funktionieren nur bei sehr einschränkenden Annahmen, wie etwa dass der Text nur ASCII-Bytes enthält. Außerhalb des ASCII-Bereiches wird häufig sowieso ein komplexerer (nicht laufzeitkonstanter) Algorithmus zur Ermittlung der linguistischen Einheit (Glyph, Wort, Abschnitt) verwendet. Wir empfehlen, einen „ehrlichen“, linguistisch korrekten, anerkannten Unicode-Algorithmus zu verwenden.
Der char
-Typ ist UTF-32-kodiert. Wenn du sicher bist, dass du einen Algorithmus verwenden musst, welcher jeden Codepoint einzeln betrachtet, kannst du einfach einen type wstr = [char]
definieren. Dann kannst du in einem einen str
in ihn entpacken, mit welchem du dann als wstr
arbeiten kannst. In anderen Worten: Die Tatsache, das die Sprache standardmäßig nicht in UTF-32 enkodiert, soll dich nicht daran hindern, Strings in irgendeinem anderen Enkoding zu verarbeiten.
Eine detailliertere Erklärung, warum UTF-8 üblicherweise UTF-16 oder UTF-32 vorgezogen werden sollte, findet sich im UTF-8 Everywhere Manifesto.
Welchen String-Typ sollte ich verwenden?
Rust hat vier Paare von Stringtypen, von welchen jeder einen bestimmten Sinn hat. In jedem dieser Paare befindet sich ein Owned-Typ sowie ein Slice-Typ. Die Typen sind folgendermaßen organisiert:
Slice-Typ | Owned-Typ | |
---|---|---|
UTF-8 | str |
String |
Betriebssystemkompatibel | OsStr |
OsString |
C-kompatibel | CStr |
CString |
Systempfad | Path |
PathBuf |
Jeder String-Typen dient einem anderen Zweck. String
und str
sind UTF-8-kodierte, allgemein verwendbare Strings. OsString
und OsStr
sind nach den Vorgaben der jeweiligen Plattform enkodiert und sollten benutzt werden, um mit dem Betriebssystem zu interagieren. CString
und CStr
sind Rusts Gegenstück zu C-Strings und werden in FFI (Foreign Function Interface)-Code genutzt. PathBuf
und Path
sind bequeme Wrapper um OsString
und OsStr
, welche Methoden zur Dateipfad-Manipulation anbieten.
Wie kann ich eine Funktion schreiben, welche sowohl &str
als auch String
annimmt?
Es gibt je nach Verwendung der Funktion verschiedene Möglichkeiten:
- Wenn die Funktion einen Owned-String benötigt, aber jede Art von String empfangen soll, dann nutze einen generischen Typen mit
Into<String>
-Bound. - Wenn die Funktion einen String-Slice benötigt, aber jede Art von String empfangen soll, dann nutze einen generischen Typen mit
AsRef<str>
-Bound. - Wenn es keine Rolle spielt, welchen String-Typ die Funktion bekommt, und du mit beiden Möglichkeiten einheitlich umgehen willst, dann nutze
Cow<str>
als Parametertyp.
Nutzung von Into<String>
In diesem Beispiel wird die Funktion sowohl Owned-Strings als auch String-Slices akzeptieren und dann entweder nichts tun oder die Eingabe innerhalb des Funktionskörpers zu einem Owned-String umwandeln. Diese Konversion muss explizit geschehen und wird ansonsten nicht stattfinden.
fn accepts_both<S: Into<String>>(s: S) {
let s = s.into(); // This will convert s into a `String`.
// ... the rest of the function
}
Nutzung von AsRef<str>
In diesem Beispiel wird die Funktion sowohl Owned-Strings als auch String-Slices akzeptieren und dann entweder nichts tun oder die Eingabe innerhalb der Funktion zu einem String-Slice umwandeln. Dies kann automatisch geschehen, indem die Eingabe als Referenz betrachtet wird:
fn accepts_both<S: AsRef<str>>(s: &S) {
// ... the body of the function
}
Nutzung von Cow<str>
In diesem Beispiel akzeptiert die Funktion einen Cow<str>
, der nicht ein generischer Typ ist, sondern ein Container, welcher je nach Nutzung einen Owned-String oder String-Slice enthält.
fn accepts_cow(s: Cow<str>) {
// ... the body of the function
}
Collections
Ist es möglich, Datenstrukturen wie Vektoren oder verkettete Listen in Rust effizient zu implementieren?
Es ist unnötig, diese Datenstrukturen zu Nutzung in deinem eigenen Programm selber zu schreiben, da effiziente Implementierungen durch die Standardbibliothek angeboten werden.
Wenn du aber einfach nur lernen willst, dann wirst du wahrscheinlich unsafe-Code verwenden müssen. Diese Datenstrukturen können zwar direkt in sicherem Rust implementiert werden, aber die Leistung einer unsafe-Implementierung wird wahrscheinlich besser sein. Der einfache Grund dafür ist, dass die Implementierung von Vektoren und verketteten Listen Zeigermanipulationen und Speicherzugriffe erfordert, welche in sicherem Rust verboten sind.
Doppelt verkettete Listen erfordern zum Beispiel, dass auf jeden Knoten zwei veränderliche Referenzen verweisen. Dies verletzt aber die Aliasing-Regeln für veränderliche Referenzen (es darf nur höchstens eine auf ein Objekt bestehen). Du kannst dieses Problem umgehen, indem du Weak<T>
nutzt, aber darunter wird die Leistung stark leiden. Mit unsicherem Code kannst du die Mutable-Aliasing-Regel umgehen, aber dann musst du manuell sicherstellen, dass dein Code die Speichersicherheit nicht verletzt.
Wie kann ich über eine Collection iterieren ohne sie zu konsumieren / zu verschieben?
Die einfachste Art ist, die IntoIterator
-Implementierung der Referenzen auf Collections zu nutzen. Zum Beispiel für &Vec
:
let v = vec![1,2,3,4,5];
for item in &v {
print!("{} ", item);
}
println!("\nLength: {}", v.len());
In Rust nutzen die for
-Schleifen die into_iter()
-Funktion aus dem IntoIterator
-Trait der zu iterierenden Collection. Über alle Typen, die den IntoIterator
-Trait anbieten, kann mit einer for
-Schleife iteriert werden. Für &Vec
und &mut Vec
ist IntoIterator
ebenfalls implementiert. Das bedeutet, dass ein Iterator durch into_iter()
den Inhalt der Collection als Referenz betrachtet, statt ihn durch einen Move zu konsumieren. Dies gilt auch für die anderen Standard-Collections.
Wenn du einen konsumierenden Iterator benötigst, dann schreibe die for
-Schleife ohne &
oder &mut
in der Iteration.
Direkten Zugriff auf einen Iterator, welcher Referenzen auf den Inhalt anbietet, erhältst du normalerweise durch den Aufruf der iter()
-Methode.
Warum muss ich die Größe eines Arrays in der Deklaration angeben?
Du musst das nicht zwingend tun. Wenn du ein Array direkt deklarierst, wird die Größe durch die Anzahl der Elemente bestimmt. Aber wenn du eine Funktion deklarierst, welche ein Array fester Größe annimmt, muss der Compiler wissen wie groß dieses Array sein wird.
Anzumerken ist, dass Rust momentan keine Generics für Arrays verschiedener Größe anbietet. Wenn du einen zusammenhängenden Container einer Variablen Anzahl von Werten annehmen willst, nutze einen Vec
oder ein Slice (abhängig davon, ob du Ownership benötigst).
Ownership
Wie kann ich eine Datenstruktur mit Zyklen implementieren?
Es gibt mindestens vier Möglichkeiten, welche ausführlich in Too Many Linked Lists) beschrieben werden:
- Du kannst
Rc
undWeak
nutzen, um geteilte Ownership zu Knoten zu erhalten. Dann musst du die Kosten von Memory Management in Kauf nehmen. - Du kannst
unsafe
-Code mit rohen Pointern nutzen. Dies wird effizient sein, aber es umgeht die Sicherheitsgarantien. - Du kannst Vektoren und Indexe in diesen Vektoren benutzen. Hier sind einige Beispiele und Erklärungen für diese Herangehensweise: several available.
- Du kannst ‘borrowed’ Referenzen mit
UnsafeCell
nutzen. Es gibt für diese Herangehensweise Erklärungen und Beispielcode.
Wie kann ich ein Struct mit Referenzen zu seinen eigenen Feldern definieren?
Das ist möglich, aber nutzlos. Das Struct ist dann permanent von sich selbst „ausgeliehen“ und kann nicht bewegt werden. Hier ein Beispiel:
use std::cell::Cell;
#[derive(Debug)]
struct Unmovable<'a> {
x: u32,
y: Cell<Option<&'a u32>>,
}
fn main() {
let test = Unmovable { x: 42, y: Cell::new(None) };
test.y.set(Some(&test.x));
println!("{:?}", test);
}
Was ist der Unterschied zwischen Pass-By-Value, Konsumieren, Verschieben (Moving), und der Übertragung von Ownership?
Alle diese Begriffe sind äquivalent. In jedem Fall bedeuten sie, dass der Wert zu einem neuen Besitzer übertragen und aus dem Besitz des vorherigen entfernt wurde. Der vorherige Besitzer kann den Wert nicht mehr nutzen. Wenn ein Typ den Copy
-Trait anbietet, dann wird der Wert des ursprünglichen Besitzers nicht invalidiert und kann weiterhin benutzt werden.
Warum können Werte eines Typs nach dem übergeben an eine Funktion wiederverwendet werden, während die Wiederverwendung von Werten anderen Typs zu einem Fehler führt?
Wenn ein Typ den Copy
-Trait anbietet, dann wird ein Wert dieses Typs bei der Übergabe an eine Funktion kopiert. Alle numerischen Typen in Rust implementieren Copy
, aber Struct-Typen implementieren den Trait nicht standardmäßig. Sie werden also standardmäßig mit Ownership übergeben. Das bedeutet, dass ein Struct nach der Übergabe nicht mehr benutzt werden kann, wenn es nicht (samt Ownership) am Ende der aufgerufenen Funktion wieder zurückgegeben wird.
Wie gehe ich mit einem „Use of moved Value“-Fehler um?
Diese Fehlermeldung bedeutet, dass du versuchst auf einen Wert zuzugreifen, der den Besitzer gewechselt hat. Die erste Frage ist, ob die Übergabe des Besitzes nötig war: Wenn der Wert einer Funktion übergeben wurde, dann könnte es möglich sein die Funktion so umzuschreiben, dass sie nur eine Referenz entgegennimmt.
Wenn der übergebene Typ den Clone
-Trait implementiert, dann wird ein Aufruf zu clone()
vor der Übergabe eine Kopie des Wertes erstellen und das Original zur weiteren Verwendung freigeben. Da durch Klonen weitere Allokationen notwendig werden, sollte dies der letzte Ausweg sein.
Wenn der Übergebene Wert dein eigener Typ ist, dann könntest du Copy
(für implizites Kopieren anstelle von Klonen) oder Clone
(explizite Kopie) selbst implementieren. Copy
kann häufig durch #[derive(Copy, Clone)]
erhalten werden (Copy
erfordert Clone
), und Clone
mit #[derive(Clone)]
.
Wenn diese Möglichkeiten nicht gegeben sind, dann könntest du die Funktion, welche den Besitz des Wertes erfordert so modifizieren, dass sie den Besitz am Ende wieder zurückgibt.
Nach welchen Regeln richtet sich die Verwendung von self
, &self
, oder &mut self
in Methodendeklarationen?
- Nutze
self
, wenn eine Methode Besitz vom Wertes ergreifen soll. - Nutze
&self
, wenn eine Methode lediglich eine Nur-Lese-Referenz auf den Wert benötigt. - Nutze
&mut self
, wenn eine Methode den Wert verändern, aber nicht davon Besitz ergreifen soll.
Wie kann ich den Borrow-Checker verstehen?
Zum Evaluieren von Rust-Code nutzt der Borrow Checker nur ein paar wenige Regeln, welche im Abschnitt über Borrowing erklärt werden. Diese Regeln sind:
Erstens darf ein Borrow höchstens so lange bestehen wie der eigentliche Besitzer. Zweitens darfst du zu jedem Zeitpunnkt nur eine dieser beiden Arten von Borrow haben:
- Eine oder mehr Referenzen (&T) zu einer Ressource.
- Genau eine veränderbare Referenz (&mut T).
Diese Regeln sind zwar einfach, sie konsistent zu befolgen aber nicht, vor allem wenn man nicht gewohnt ist, über Lifetimes und Ownership nachzudenken.
Der erste Schritt zum Verständnis des Borrow Checkers ist es, seine Fehlermeldungen zu studieren. Es wurde viel Arbeit investiert, um die Qualität der Hilfestellungen des Borrow Checkers zu erhöhen. Wenn du ein Problem mit dem Borrow Checker hast, ist der erste Schritt, langsam und vorsichtig die Fehlermeldung zu lesen. Verändere deinen Code erst, nachdem du den Fehler verstanden hast.
Der zweite Schritt ist, die Container-Typen der Standardbibliothek, welche mit Ownership und Mutability zu tun haben, kennenzulernen. Dies sind zum Beispiel Cell
, RefCell
, und Cow
. Diese Typen sind nützliche und notwendige Werkzeuge, um bestimmte Ownership- und Mutabilityverhältnisse auszudrücken, und wurden für minimale Leistungskosten entworfen.
Der wichtigste Bestandteil, um den Borrow Checker zu verstehen, ist Übung. Die starke Garantien der statischen Analyse von Rust sind streng und von anderen Programmiersprachen verschieden. Es wird einige Zeit dauern, mit Allem vollständig zurechtzukommen.
Wenn du dich mit dem Borrow Checker allzu sehr abmühen musst und dir die Geduld ausgeht, ist die Rust-Community jederzeit für dich da.
Wann sollte ich Rc
verwenden?
Die Funktion des nichtatomaren, referenzzählenden Containers Rc
wird in der offiziellen Dokumentation erläutert. Kurz gesagt kann man Rc
und seinen threadsicheren Cousin Arc
verwenden, um gemeinsamen Besitz einer Ressource auszudrücken und diese Ressource automatisch freizugeben, wenn kein Besitzer mehr Zugriff darauf hat.
Wie gebe ich eine Closure aus einer Funktion zurück?
Um eine Closure aus einer Funktion herausreichen zu können, muss sie eine Move Closure sein, welche mit dem Schlüsselwort move
deklariert wird. Wie im Buch zu Rust erklärt, gibt dies der Closure eine eigene Kopie ihrer eingefangenen Variablen, die vom Stack Frame der Elternfunktion unabhängig sind. Eine andere Rückgabe von Closures wäre unsicher, da dies Zugriff auf nicht mehr gültige Variablen gewähren würde. In anderen Worten: Es würde das Auslesen potentiell ungültigen Speichers ermöglichen. Die Closure muss außerdem von einer Box
umgeben werden, damit sie auf dem Heap allokiert wird. Im Buch kannst du mehr darüber lesen.
Was sind Deref Coercions und wie funktionieren sie?
Eine Deref Coercion ist eine nützliche Umwandlung, die automatisch Referenzen auf Zeigertypen (also zum Beispiel &Rc<T>
or &Box<T>
) zu Referenzen auf ihren Inhalt konvertiert.
Deref Coercions existieren, um einen ergonomischeren Umgang mit Rust zu ermöglichen und werden durch den Deref
-Trait implementiert.
Eine Implementierung von Deref gibt an, dass der implementierende Typ durch den Aufruf der deref
-Methode zu einem Zieltyp konvertiert werden kann. Dabei nimmt die Methode eine unveränderliche Referenz zum aufrufenden Typ an und gibt eine Referenz mit derselben Lifetime zum Zieltyp zurück. Der *
-Präfix ist eine Kurznotation für die deref
-Methode.
Der Name „Coercion“ kommt aus der hier im Buch erklärten Regel:
Wenn du einen Typ
U
hast, welcherDeref<Target=T>
implementiert, dann können Werte von&U
automatisch in&T
konvertiert (coerced) werden.
Wenn du beispielsweise einen &Rc<String>
hast, wird er nach dieser Regel automatisch zu einem &String
, welcher dann auf die gleiche Weise zu einem &str
wird. Wenn also eine Funktion einen &str
-Parameter entgegennimmt, kannst du einen &Rc<String>
direkt übergeben, woraufhin alle Umwandlungen automatisch durch den Deref
-Trait geschehen.
Die häufigsten Arten der Deref Coercion sind:
&Rc<T>
zu&T
&Box<T>
zu&T
&Arc<T>
zu&T
&Vec<T>
zu&[T]
&String
zu&str
Lifetimes
Welchen Zweck haben Lifetimes?
Lifetimes sind Rusts Antwort auf die Frage der Speichersicherheit. Sie erlauben es Rust, Speichersicherheit ohne die Laufzeitkosten von Garbage-Collection zu erlangen. Sie basieren auf einer Vielzahl akademischer Arbeiten, welche im Rust book nachgeschlagen werden können.
Warum ist die Syntax für Lifetimes so, wie sie ist?
Die 'a
-Syntax kommt aus der ML-Familie der Programmiersprachen, wo 'a
benutzt wird, um einen generischen Parameter zu markieren. In Rust sollte der Syntax eindeutig und auffällig sein und neben Traits und Referenzen in eine Typdeklaraion (Signatur) passen. Alternative syntaktische Repräsentationen wurden diskutiert, von denen aber keine eindeutige Vorteile vorwiesen.
Wie kann ich in einer Funktion einen Wert alloziieren, um dann eine Referenz darauf zurückzugeben?
Du musst sicherstellen, dass das alloziierte Objekt länger lebt als die Funktion. Das kannst du erreichen, indem du die Ausgabe-Lifetime an eine Eingabe-Lifetime bindest:
type Pool = TypedArena<Thing>;
// (Die Lifetime 'a ist nur zur Erklärung explizit angegeben.
// Du kannst sie nach den in einem späteren FAQ-Eintrag
// erklärten Elision-Regeln weglassen.)
fn create_borrowed<'a>(pool: &'a Pool,
x: i32,
y: i32) -> &'a Thing {
pool.alloc(Thing { x: x, y: y })
}
Eine Alternative Vorgehensweise wäre, die Referenzen vollständig wegzulassen und einen Typ mit Ownership wie zum Beispiel String
zurückzugeben:
fn happy_birthday(name: &str, age: i64) -> String {
format!("Hello {}! You're {} years old!", name, age)
}
Diese Vorgehensweise ist einfacher, aber hat oft unnötige Allokationen zufolge.
Warum haben manche Referenzen Lifetimes, wie &'a T
, und manche anderen wie &T
nicht?
Eigentlich haben alle Referenzen eine Lifetime, aber meistens musst du sie nicht explizit angeben. Die Regeln sind wie folgt:
- Innerhalb eines Funktionskörpers musst du nie explizit eine Lifetime angeben; Der korrekte Wert sollte immer inferiert werden.
- Innerhalb einer Funktionssignatur (zum Beispiel für die Typen der
Parameter oder den Rückgabetyp), sind explizite Lifetimes manchmal notwendig.
Hier nutzen Lifetimes ein einfaches Vorgabeschema namens
Lifetime elision,
welches aus den drei folgenden Regeln besteht:
- Jede ausgelassene Lifetime in den Funktionsparametern wird ein eigener Lifetime-Parameter.
- Wenn es genau eine explizite oder ausgelassene Eingabe-Lifetime gibt, dann wird diese Lifetime allen ausgelassenen Lifetimes der Rückgabetypen dieser Funktion zugewiesen.
- Wenn es mehrere Eingabe-Lifetimes gibt, von denen eine
&self
oder&mut self
ist, dann wird die Lifetime vonself
allen ausgelassenen Rückgabe-Lifetimes zugeordnet.
- In einer
struct
- oderenum
-Definition müssen alle Lifetimes explizit angegeben werden.
Wenn diese Regeln nicht anwendbar sind, wird der Rust-Compiler eine Fehlermeldung zusammen mit einer potenziellen Lösung ausgegeben. Diese Lösung hängt vom konkreten Schritt des Inferenzvorganges ab, in dem der Fehler aufgetreten ist.
Wie kann Rust Freiheit von Nullzeigern und „hängenden Zeigern“ garantieren?
Der einzige Weg, einen Wert vom Typ &Foo
oder &mut Foo
zu konstruieren ist, einen existierenden Wert vom Typ Foo
anzugeben, auf den die Referenz zeigt. Die Referenz „borgt“ sich den originalen Wert für einen gegeben Abschnitt des Codes (nämlich der Lifetime der Referenz) aus. Während der Dauer der „Ausborgung“ kann der Wert nicht an einen neuen Besitzer übergeben, verändert oder freigegeben werden.
Wie drücke ich die Abwesenheit eines Wertes aus, ohne null
zu verwenden?
Das kannst du mit dem Option
-Typ erreichen, welcher entweder ein Some(T)
oder None
sein kann. Some(T)
zeigt an, dass ein Wert vom Typ T
in der Option vorhanden ist, während None
die Abwesenheit anzeigt.
Generics
Was ist Monomorphisierung?
Monomorphisierung spezialisiert jeden Aufruf einer generischen Funktion oder Struktur mit einer spezifischen Instanz basierend auf den Parametertypen der Funktionaufrufe (oder Verwendungen der Struktur).
Für jede einzigartige Menge von Typen, mit welcher eine generische Funktion instanziiert wird, erstellt der Compiler eine neue Kopie der Funktion. Diese Strategie wird auch von C++ genutzt und resultiert in schnellem Code, welcher für jeden Aufruf spezialisiert ist und frühe Bindung nutzen kann. Allerdings kann sie bei vielen voneinander verschiedenen Aufruftypen auch dazu führen, dass die Größe der generierten Ausführbaren Datei größer ist als bei anderen Aufrufstrategien.
Für Funktionen, welche anstelle von Typparametern Trait Objects annehmen, wird keine Monomorphisierung durchgeführt. Stattdessen werden Methoden für Trait Objects dynamisch zur Laufzeit entschieden.
Was ist der Unterschied zwischen einer Funktion und einer Closure, welche keine Variablen einfängt?
Funktionen und Closures sind äquivalente Operationen, haben aber zur Laufzeit aufgrund ihrer verschiedenen Implementierungen unterschiedliche Repräsentationen.
Funktionen sind eingebaute Primitive der Sprache, während Closures eigentlich syntaktischer Zucker für einen von drei Traits sind: Fn
, FnMut
, und FnOnce
. Beim Kompilieren einer Closure wird der Rust-Compiler automatisch ein Struct erstellen, für das der entsprechende Trait implementiert ist und der die entgegengenommenen Variablen als Attribute enthält. der Trait ermöglicht es, dieses Struct wie eine Funktion aufzurufen. Reguläre Funktionsdefinitionen können keine Variablen aus ihrer Umgebung einfangen.
Der große Unterschied zwischen diesen Traits ist, wie sie mit dem self
-Parameter umgehen. Fn
nimmt &self
, FnMut
nimmt &mut self
, und FnOnce
nimmt self
.
Sogar wenn eine Closure gar keine Umgebungsvariablen entgegennimmt, wird sie zur Laufzeit als zwei Zeiger repräsentiert, genau wie jede andere Closure.
Was sind Typen höherer Ordnung, wozu brauche ich sie, und warum hat Rust sie nicht?
Typen höherer Ordnung sind Typen mit noch ausstehenden Parametern. Typkonstruktoren wie Vec
, Result
, und HashMap
sind Beispiele für Typen höherer Ordnung: Sie erfordern einige weitere Typparameter um einen tatsächlichen Typ wie Vec<u32>
darzustellen. Unterstützung für Typen höherer Ordnung würde bedeuten, dass diese „unvollständigen“ Typen überall dort benutzt werden können, wo sonst auch „vollständige“ Typen verwendet werden können, also auch als generische Parameter für Funktionen.
Jeder vollständige Typ wie i32
, bool
, oder char
ist von der Art *
(diese Notation kommt aus der Typtheorie). Ein Typ mit einem Parameter, wie Vec<T>
ist von der Art * -> *
, was bedeutet, dass Vec<T>
einen vollständigen Typ wie i32
nimmt und einen vollständigen Typ wie Vec<i32>
zurückgibt.
Ein Typ mit drei Parametern, wie HashMap<K, V, S>
ist von der Art * -> * -> * -> *
, und nimmt drei vollständige Typen (wie i32
, String
, and RandomState
) entgegen, um einen neuen vollständigen Typ HashMap<i32, String, RandomState>
zu konstruieren.
Zusätzlich zu diesen Beispielen können Typkonstruktoren auch Lifetime-Argumente entgegennehmen, welche wir mit Lt
bezeichnen wollen. Der Typ slice::Iter
hat zum Beispiel die Art Lt -> * -> *
, weil er mit einer Lifetime und einem vollständigen Typ konstruiert werden muss: Iter<'a, u32>
.
Die fehlende Unterstützung von Typen höherer Ordnung erschwert das Schreiben von bestimmten Arten generischen Codes. Es ist insbesondere problematisch, Konzepte wie Iteratoren zu abstrahieren, da diese oft mit mindestens einer Lifetime parametrisiert sind. Dies hat die Erstellung von Traits verhindert, die über Collections abstrahieren.
Ein weiteres häufiges Beispiel sind Konzepte wie Funktoren oder Monaden. Beide sind Typkonstruktoren statt einfacher Typen.
Bisher hat Rust keine Typen höherer Ordnung weil ihnen gegenüber anderen Verbesserungen keine Priorität zugewiesen wurde. Weil der Entwurf eine große, querschneidende Veränderung ist, wollen wir auch vorsichtig damit umgehen. Aber es gibt keinen tiefgreifenden Grund, der Typen höherer Ordnung unmöglich machen würde.
Was bedeuten benannte Parameter wie <T=Foo>
in generischen Typen?
Diese werden Assoziierte Typen genannt und erlauben es, Trait-Bounds auszudrücken, für die eine where
-Klausel nicht ausreicht.
Eine generische Einschränkung X: Bar<T=Foo>
bedeutet: „X
muss den Trait Bar
implementieren, und die Implementierung von Bar
muss für den assoziierten Typ T
den Typ Foo
annehmen.“. Beispiele für Typ-Einschränkungen, welche nicht mit einer where
-Klausel ausgedrückt werden können, sind zum Beispiel Trait-Objekte wie Box<Bar<T=Foo>>
.
Assoziierte Typen existieren, weil Generics oft mit Familien von Typen umgehen müssen, wobei ein Typ alle anderen in der Familie bestimmt. Ein Trait eines Graphen zum Beispiel könnte als Self
-Typ den Graphen selber haben, sowie assoziierte Typen für Knoten und Kanten. Jeder Graph-Typ bestimmt dann eindeutig die assoziierten Typen. Die Verwendung von assoziierten Typen vereinfacht die Arbeit mit solchen Typfamilien stark und bietet in vielen Fällen auch bessere Typinferenz.
Kann ich Operatoren überladen? Welche und wie?
Du kannst eigene Implementierung bestimmte Operatoren über die zugehörigen Traits definieren: Add
für +
, Mul
für *
und so weiter. Das Ganze sieht folgendermaßen aus:
use std::ops::Add;
struct Foo;
impl Add for Foo {
type Output = Foo;
fn add(self, rhs: Foo) -> Self::Output {
println!("Addition");
self
}
}
Die folgenden Operatoren können überladen werden:
Operation | Trait |
---|---|
+ |
Add |
+= |
AddAssign |
binary - |
Sub |
-= |
SubAssign |
* |
Mul |
*= |
MulAssign |
/ |
Div |
/= |
DivAssign |
unary - |
Neg |
% |
Rem |
%= |
RemAssign |
& |
BitAnd |
| |
BitOr |
| = |
BitOrAssign |
^ |
BitXor |
^= |
BitXorAssign |
! |
Not |
<< |
Shl |
<<= |
ShlAssign |
>> |
Shr |
>>= |
ShrAssign |
* |
Deref |
mut * |
DerefMut |
[] |
Index |
mut [] |
IndexMut |
Warum gibt es die Unterscheidung zwischen Eq
/PartialEq
und Ord
/PartialOrd
?
Die Werte mancher Typen in Rust sind nur partiell geordnet oder kennen nur partielle Gleichheit. In einer partiellen Ordnung kann es vorkommen, dass bei zwei verschiedenen Werte eines Typs der eine weder kleiner noch größer als der andere ist. Partielle Gleichheit bedeutet, dass Werte des Typs ungleich sich selbst sein können.
Gleitkommazahlen (f32
and f64
) sind gute Beispiele für beide Fälle. Gleitkommatypen können den Wert NaN
(“Not a Number”) annehmen. NaN
ist ungleich sich selbst (NaN == NaN
ist false
) und nicht kleiner oder größer als jeder beliebige Gleitkommawert. Deshalb implementieren sowohl f32
als auch f64
PartialOrd
und PartialEq
, nicht aber Ord
oder Eq
.
Wie in der obigen Frage zu Gleitkommazahlen erklärt, ist diese Unterscheidung wichtig, da manche Collection-Typen eine totale Ordnung oder Vergleichbarkeit benötigen, um korrekt zu funktionieren.
Ein- und Ausgabe
Wie lese ich eine Datei in einen String
ein?
Mit der read_to_string()
-Methode, die auf dem Read
-Trait in std::io
definiert ist.
use std::io::Read;
use std::fs::File;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut f = try!(File::open(path));
let mut s = String::new();
try!(f.read_to_string(&mut s)); // `s` enthält den Inhalt von "foo.txt"
Ok(s)
}
fn main() {
match read_file("foo.txt") {
Ok(_) => println!("Datei gelesen!"),
Err(err) => println!("Fehler beim Lesen der Datei: {}", err)
};
}
Wie lese ich effektiv aus einer Datei?
Der Typ File
implementiert den Read
-Trait, der eine Reihe an Funktionen zum Lesen und Schreiben von Daten bereitstellt, etwa read()
, read_to_end()
, bytes()
, chars()
, und take()
. Jede dieser Funktionen liest einen bestimmte Menge an Daten aus der jeweiligen Datei. read()
liest so viele Daten, wie das Eingabe-Ausgabe-System in einem einzigen Aufruf zur Verfügung stellt. read_to_end()
liest den gesamten Puffer in einen Vektor ein und fordert dabei so viel Speicher an wie notwendig. bytes()
und chars()
liefern Iteratoren über die Bytes bzw. Zeichen einer Datei. Zu guter Letzt ermöglicht es take()
, eine beliebige Anzahl Bytes aus der Datei zu lesen. Zusammen genommen sollten diese Funktionen ausreichen, um effektiv aus jeder beliebigen Datei zu lesen.
Für gepufferte Eingabe gibt es den BufReader
-Struct, der hilft, die Anzahl der Systemaufrufe während des Lesens zu verringern.
Wie setze ich asynchrone Ein- und Ausgabe in Rust um?
Es gibt mehrere aktive Bibliotheken zu asynchronem I/O in Rust, wie etwa mio, tokio, mioco, coio-rs, und rotor.
Wie kann ich auf die Kommandozeilenargumente meines Programms zugreifen?
Die einfachste Möglichkeit ist Args
, das einen Iterator über die Argumente zur Verfügung stellt.
Wenn du nach etwas mächtigeren suchst, gibt es eine Reihe an Optionen auf crates.io.
Fehlerbehandlung
Warum kennt Rust keine Exceptions?
Exceptions erschweren das Verständnis von Kontrollfluss, drücken Gültigkeit und Ungültigkeit außerhalb des Typsystems aus und spielen schlecht mit Multithreading zusammen (ein wichtiges Ziel von Rust).
Rust zieht einen typbasierten Ansatz zur Fehlerbehandlung vor, der ausführlich im Buch beschrieben wird. Dieser passt besser zum Kontrollfluss, der Nebenläufigkeit und dem Rest der Sprache.
Was hat es mit den dauernden unwrap()
-Aufrufen auf sich?
unwrap()
ist eine Funktion, die den Wert aus einer Option
oder einem Result
entpackt und eine Panic auslöst, wenn dieser Wert nicht vorhanden ist.
unwrap()
ist kein guter Weg, um Fehlersituationen wie falsche Benutzereingaben abzufangen. In Produktionscode dient unwrap()
eher als Assertion, um das Vorhandensein eines Werts als Invariante sicherzustellen, deren Verletzung einen Bug darstellt und das Programm sofort abbrechen soll.
Die Funktion ist auch für Prototypen geeignet, in denen noch keine Fehlerbehandlung implementiert werden soll. Außerdem ist sie für Codebeispiele praktisch, in denen die Fehlerbehandlung vom Ziel des Programms ablenken würde.
Warum bekomme ich einen Compilerfehler in Beispielcode, der das try!
-Makro benutzt?
Das liegt wahrscheinlich am Rückgabetyp der Funktion. Das try!
-Makro entpackt im Erfolgsfall den Wert aus einem Result
oder kehrt aus der aufrufenden Funktion mit dem Fehler zurück, den das Result
beschreibt. Damit funktioniert try!
nur in Funktionen die ihrerseits ein Result
zurückgeben. Dessen Err
-Wert muss außerdem mit From::from(err)
aus dem Fehlertyp des try!
-Arguments konstruiert werden können. Insbesondere kann try!
somit auch nicht in der main
-Funktion verwendet werden.
Gibt es einen einfacheren Fehlerbehandlungsmechanismus, als überall Result
zu verwenden?
Result
s-Werte wirst du immer mit einem unwrap()
los, nur ist das meistens nicht das was du möchtest. Result
ist ein Anzeichen dafür, dass eine Operation möglicherweise fehlschlagen kann. Rust zwingt dich diese Fehlerpfade explizit zu behandeln, um die Robustheit von Programmen gegenüber Fehlersituationen zu fördern. Es gibt Helfer wie das try!
-Makro, das das Propagieren von Fehlern angenehmer macht.
Wenn du einen Fehler wirklich nicht behandeln möchtest kannst du auf unwrap()
zurückgreifen. Das bedeutet aber, dass dein Code im Fehlerfall eine Panic erzeugt, die im Normalfall den Prozess sofort beendet.
Nebenläufigkeit
Kann ich statische Werte in mehreren Threads benutzen, ohne auf unsafe
-Code zurückzugreifen?
Schreibende Zugriffe sind sicher, solange sie synchronisiert erfolgen. Dazu kann etwa ein Mutex
(spät initialisiert über lazy-static) oder ein AtomicUsize
(regulär initialisiert) verwendet werden.
Allgemein gesprochen können all die Typen in einer statischen Variablen benutzt werden, die Sync
aber nicht Drop
implementieren.
Makros
Kann ich ein Makro schreiben, das neue Bezeichner erzeugt?
Momentan nicht. Rust-Makros sind &„hygiensich“ und vermeiden damit absichtlich das Reservieren oder Erzeugen von Bezeichnern, die unerwartete Kollisionen mit anderem Code auslösen können. Ihre Fähigkeiten unterscheiden sich grundsätzlich von Makros, wie sie vom C-Präprozessor bekannt sind. Makro-Aufrufe können nur an explizit erlaubten Positionen im Code auftauchen: An Stelle von Definitionen (items) auf Modulebene, Methodendeklarationen, Statements, Ausdrücken, und Patterns. Sie können nicht benutzt werden, um eine partielle Methodendeklaration oder Variablendeklaration zu vervollständigen.
Fehlersuche und Werkzeuge
Wie finde ich Fehler in meinem Rust-Programm?
Rust-Programme können mit gdb oder lldb debuggt werden - genau wie C und C++. Jede Rust-Installation kommt mit rust-gdb, rust-lldb oder beidem, je nachdem was die Plattform unterstützt. Dabei handelt es sich um Wrapper um gdb und lldb, die Rust-Datenstrukturen lesbar ausgeben können (pretty printing).
rustc
behauptet, dass eine Panic im Code er Standardbibliothek aufgetreten ist. Wie kann ich den Fehler in meinem Code finden?
Dieser Fehler tritt meistens auf, wenn aus Anwendungscode heraus unwrap()
auf einem None
- oder Err
-Wert aufgerufen wird. Ist beim Start des Programms die Umgebungsvariable RUST_BACKTRACE=1
gesetzt, wird zusammen mit der Panic der Aufrufstapel ausgegeben, der zum Fehler geführt hat. Es hilft auch, das Projekt im Debug-Modus zu übersetzen (Standardverhalten für cargo build
) und einen Debugger wie rust-gdb
oder rust-lldb
einzusetzen.
Welche IDE sollte ich benutzen?
Es gibt eine ganze Reihe an Entwicklungsumgebungen für Rust, die alle auf der Seite zu IDEs vorgestellt werden.
gofmt
ist toll! Wo ist rustfmt
?
rustfmt
findest du hier. Das Tool wird aktiv weiterentwickelt, um Rust-Code so leserlich und vorhersagbar wie möglich zu gestalten.
Low-Level
Kann ich Bytes wie mit memcpy
kopieren?
Wenn du nur einen Slice klonen möchtest, kannst du dafür clone_from_slice
verwenden.
Bytesequenzen, die sich möglicherweise überlappen, können mit copy
kopiert werden. Bist du dir sicher, dass sich Quelle und Ziel nicht überlappen, funktioniert auch das (etwas schnellere) copy_nonoverlapping
. Beide Funktionen sind unsafe
, da sie die Sicherheitsgarantien von Rust verletzen können. Sie sollten nur mit Vorsicht eingesetzt werden.
Kann Rust auch ohne Standardbibliothek vernünftig funktionieren?
Absolut. Rust-Programme können die Standardbibliothek mit dem #![no_std]
-Attribut abwählen. Damit kann weiterhin die Rust-Core-Bibliothek verwendet werden, die nur die plattformunabhängigen Primitive der Standardbibliothek enthält. Damit enthält sie keine Funktionalität zu I/O, Nebenläufigkeit, Heap-Allokation oder ähnlichem.
Ist es möglich, ein Betriebssystem in Rust schreiben?
Ja! Es gibt tatsächlich bereits mehrere Projekte, die genau dieses Ziel verfolgen.
Wie kann ich numerische Datentypen wie i32
oder f64
in Little- oder Big-Endian-Kodierung von einem Stream lesen oder in einen Stream schreiben?
Diese Funktionalität wird vom byteorder-Crate zur Verfügung gestellt.
Garantiert Rust ein spezifisches Layout von Datenstrukturen?
Standardmäßig nicht. Allgemein gesprochen ist das Layout von enum
s und struct
s undefiniert. Das erlaubt Compileroptimierungen wie das Wiederverwenden von Padding für Enum-Diskriminanten, Kombinieren von Varianten verschachtelter Enums und Umordnen von Struct-Feldern, um Padding zu verringern. C-ähnlichen Enums (solchen ohne Daten innerhalb der Varianten) kann eine definierte Repräsentation zugewiesen werden:
enum CLike {
A,
B = 32,
C = 34,
D
}
Solche Enums dürfen das Attribut #[repr(C)]
tragen, um sie auf die Repräsentation festzulegen, die sie in äquivalentem C-Code hätten. Das erlaubt die Verwendung von Rust-Enums in FFI-Code, der nach C-Enums übersetzt. Das Attribut kann auch auf Structs angewendet werden, um ihnen das Layout des entsprechenden C-Structs zu verpassen.
Plattformübergreifende Programmierung
Was ist der idiomatische Weg, plattformspezifisches Verhalten in Rust auszudrücken?
Plattformspezifisches Verhalten kann mit Attributen zur bedingten Übersetzung wie target_os
, target_family
oder target_endian
beschrieben werden.
Ist Rust für die Android/iOS-Programmierung tauglich?
Ja! Es gibt bereits einige Beispiele für funktionierende Rust-Programme unter Android und iOS. Das Aufsetzen erfordert etwas Arbeit, Rust kommt mit beiden Plattformen aber wunderbar zurecht.
Kann ich mein Rust-Programm in einem Webbrowser ausführen?
Wahrscheinlich. Rust hat experimentelle Unterstützung für asm.js und WebAssembly.
Wie funktioniert Cross-Compilation in Rust?
Rust kann Code für andere Systeme übersetzen, erfordert dafür aber etwas Vorbereitsungsarbeit. Jeder Rust-Compiler kann als Cross-Compiler arbeiten, Bibliotheken müssen aber zunächst für die Zielplattform übersetzt werden.
Rust vertreibt Kopien der Standardbibliothek für jede unterstützte Plattform; sie liegen in den rust-std-*
-Dateien der Distribution für die jeweilige Plattform. Es existiert jedoch noch kein Automatismus, um diese Bibliotheken für Cross-Compilation zu installieren.
Module und Crates
Wie verhalten sich Crates und Module zueinander?
- Ein Crate ist eine Übersetzungseinheit, also die kleinste Einheit, auf der der Rust-Compiler arbeiten kann.
- Ein Modul ist eine (möglicherweise verschachtelte) Organisationseinheit innerhalb eines Crates.
- Ein Crate enthält ein implizites, unbenanntes Modul auf Wurzelebene.
- Rekursive Definitionen können sich über mehrere Module erstrecken, nicht aber über mehrere Crates.
Warum kann der Rust-Compiler die Bibliothek nicht finden, die ich eingebunden habe?
Hier gibt es eine Reihe von Möglichkeiten, aber ein häufiger Grund ist, dass die use
-Deklaration nicht relativ zur Crate-Wurzel angegeben wurde. Versuche deine Deklarationen so zu schreiben, dass sie den gleichen Pfad haben wie wenn sie in der Wurzeldatei deines Projekts importiert würden.
Es gibt außerdem noch die Schlüsselwörter self
und super
, die es erlauben, Pfade relativ zum aktuellen oder zum Elternmodul anzugeben.
Mehr Informationen findest du im Kapitel “Crates and Modules” des Rust Book.
Warum muss ich Moduldateien mit mod
im Crate deklarieren, anstatt sie einfach mit use
einzubinden?
Module können in Rust auf zwei Arten deklariert werden: Direkt im Code, oder in einer separaten Datei. Hier ein Beispiel beider Varianten:
// In main.rs
mod hello {
pub fn f() {
println!("Hallo!");
}
}
fn main() {
hello::f();
}
// In main.rs
mod hello;
fn main() {
hello::f();
}
// In hello.rs
pub fn f() {
println!("Hallo!");
}
Im ersten Beispiel wird das Modul in der Datei definiert, in der es auch benutzt wird. Im zweiten Beispiel weist die Moduldeklaration den Compiler darauf hin, die Definition aus hello.rs
oder hello/mod.rs
zu laden.
mod
deklariert also ein neues Modul, wogegen sich use
auf ein anderswo existierendes Modul bezieht und dessen Inhalte in den aktuellen Gültigkeitsbereich übernimmt.
Wie kann ich Cargo dazu bringen, einen Proxy zu verwenden?
Wie in der Cargo-Dokumentation (englisch) beschrieben, kann ein Proxyserver eingerichtet werden, indem die proxy
-Variable in der [http]
-Sektion der Konfigurationsdatei gesetzt wird.
Warum kann der Compiler eine Implementierung nicht finden, obwohl ich das Modul des Typs bereits importiert habe?
Für Methoden, die über einen Trait definiert werden, muss die Trait-Deklaration explizit mit importiert werden. Es genügt also nicht, das Modul zu importieren, in dem der Trait für den Struct implementiert wird, sondern der Trait selbst muss zusätzlich eingebunden werden.
Warum kann der Compiler use
-Deklarationen nicht einfach herleiten?
Er könnte wahrscheinlich, aber das willst du wahrscheinlich gar nicht. In einfachen Fällen könnte der Compiler wohl das korrekte Modul finden, indem er nach passenden Deklarationen zu einem Bezeichner sucht, das klappt im Allgemeinen aber nicht. Jede Entscheidungsregel bei Namenskonflikten würde in einigen Fällen für Überraschung sorgen, und Rust zieht es vor die Herkunft von Symbolen explizit zu benennen.
So könnte der Compiler etwa festlegen, dass bei einem Namenskonflikt das erste importierte Modul Vorrang hat. Definieren also die beiden Module foo
und bar
jeweils den Bezeichner baz
, foo
ist aber das erste registrierte Modul, so würde der Compiler ein use foo::baz;
einfügen.
mod foo;
mod bar;
// use foo::baz // wird vom Compiler eingefügt
fn main() {
baz();
}
Wenn du sicher weißt, dass das passieren wird, kann das einige wenige Tastenanschläge einsparen. Dieser Gewinn wird aber mit deutlich höheren Wahrscheinlichkeit an überraschenden Fehlermeldungen erkauft, falls mit baz()
eigentlich bar::baz
gemeint war. Auch reduziert diese Logik die Lesbarkeit des Codes, weil ein Funktionsaufruf plötzlich von der Importreihenfolge abhängt. Diesen Trade-Off wollen wir nicht eingehen.
Zukünftig könnten jedoch IDEs die Auflösung von Deklarationen erleichtern: Hilfestellung beim Finden des richtigen Imports, aber explizite Modulpfade im Code.
Wie kann ich in Rust dynamische Bibliotheken laden?
Dynamische Bibliotheken können mit libloading importiert werden, das ein plattformübergreifendes System für dynamisches Linken bereitstellt.
Warum hat crates.io keine Namensräume?
Übersetzung der offiziellen Erklärung zum Design von https://crates.io:
Im ersten Monat nach der Veröffentlichung von crates.io wurden wir von mehreren Nutzern nach der Möglichkeit gefragt, Pakete mit Namensräumen einzuführen.
Auch wenn Namensräume es Autoren einfacher macht, generische Namen für Pakete zu wählen, erhöhen sie die Komplexität, mit der Crates im Rust-Code und in der Kommunikation zwischen Entwicklern referenziert werden. Auf den ersten Blick erlauben sie mehreren Entwicklern, Namen wie
http
zu wählen, das sorgt aber nur dafür, dass diese Pakete alswycats' http
oderreem's http
bekannt werden, was keine wirkliche Verbesserung gegenüber längeren Namen wiewycats-http
oderreem-http
bringt.Es hat sich herausgestellt, dass Entwickler in Ökosystemen ohne Namensräume dazu tendieren, kreativere Namen (wie
nokogiri
statttenderlove's libxml2
) zu wählen. Diese Namen sind meist kurz und gut zu merken, gerade aufgrund der fehlenden Hierarchie. Das macht es einfacher, unmissverständlich über Pakete zu sprechen und erschafft spannende Markennamen. Auch haben wir den Erfolg von Umgebungen mit zigtausenden Paketen wie NPM und RubyGems gesehen, die wunderbar mit einem einheitlichen Namensraum zurechtkommen.Kurz gesagt sind wir nicht der Meinung, dass das das Cargo-Ökosystem davon profitieren würde, wenn Piston einen Namen wie
bvssvni/game-engine
statt dem einfach zu merkendenpiston
bekommen hätte (mit der Möglichkeit, dass ein Anderer Entwicklerwycats/game-engine
veröffentlicht).Da Namensräume auf verschiedene Arten strikt komplexer ist und sie bei bedarf später hinzugefügt werden können, sollten sie irgendwann nötig werden, werden wir vorerst bei einem gemeinsamen Namensraum bleiben.
Bibliotheken
Wie kann ich eine HTTP-Anfrage absetzen?
Die Standardbibliothek stellt keine HTTP-Implementierung zur Verfügung, deshalb musst du dafür einen externen Crate bemühen. reqwest ist einer der einfachsten. Er setzt auf hyper auf und ist in Rust geschrieben, es gibt jedoch eine Vielzahl an Alternativen. Der curl-Crate ist beispielsweise weit verbreitet und bildet eine Anbindung an die curl-Bibliothek.
Wie kann ich in Rust eine grafische Benutzeroberfläche schreiben?
Es gibt eine große Auswahl von GUI-Frameworks für Rust.
Wie kann ich JSON oder XML parsen?
Serde ist die empfohlene Bibliothek für (De-)Serialisierung von Rust-Datenstrukturen von und in eine Reihe von Datenformaten.
Gibt es einen Standard-Crate für 2D-Vektorgrafik?
Noch nicht! Lust einen zu bauen?
Wie kann ich in Rust ein OpenGL-Programm schreiben?
Glium ist die größte Bibliothek für OpenGL-Programmierung in Rust. GLFW ist auch eine solide Option.
Kann ich in Rust ein Computerspiel schreiben?
Ja! Die wichtigste Bibliothek für Spieleprogrammierung in Rust ist Piston, und es gibt sowohl ein Subreddit für Spieleprogrammierung in Rust, als auch einen IRC-Kanal (#rust-gamedev
im Mozilla-IRC) zu diesem Thema.
Design Patterns
Ist Rust objektorientiert?
Rust ist eine Mehrparadigmensprache. Viele Konzepte aus OOP-Sprachen können nach Rust übernommen werden, aber nicht alle, und nicht immer mit dem gewohnten Abstraktionsmechanismus.
Wie kann ich objektorientierte Konzepte in Rust abbilden?
Kommt darauf an. Es gibt Möglichkeiten, um objektorientierte Konzepte wie Mehrfachvererbung nach Rust zu übersetzen, das Ergebnis kann aber aufgrund der nicht objektorientierten Natur von Rust deutlich von seinem Äquivalent in einer OOP-Sprache abweichen.
Wie kann ich einen Struct mit optionalen Parametern konfigurieren?
Die einfachste Möglichkeit ist es, den Typ Option
als Parameter für Funktionen wie new
zu verwenden. Alternativ kannst du das Builder Pattern implementieren, mit dem einzelne Attribute durch Funktionsaufrufe an einen Builder belegt werden, bevor der eigentliche Struct konstruiert wird.
Wie benutze ich globale Variablen in Rust?
Globale Konstanten werden mit dem Schlüsselwort const
deklariert, globale Variablen mit static
. Beachte, dass das Verändern einer static mut
-Variablen unsafe
-Code erfordert, da es Data Races erlaubt, deren Absenz von Safe Rust garantiert wird. Ein wichtiger Unterschied zwischen const
und static
ist, dass auf static
-Variablen Referenzen gebildet werden können - const
-Werte haben keine definierte Speicheradresse. Für weitere Informationen zum Thema const
vs. static
sei auf das Buch verwiesen.
Wie kann ich Compilezeit-Konstanten definieren, die prozedural berechnet werden?
Rust hat momentan nur eingeschränkte Unterstützung für Compilezeit-Konstanten. Du kannst Werte eines primitiven Typs mittels const
-Deklarationen definieren (ähnlich zu static
, aber unveränderlich und ohne eine feste Speicheradresse). Funktionen können ebenfalls const
sein.
Konstanten, die sich mit diesen Mechanismen nicht beschreiben lassen, können mit dem lazy-static
-Crate erzeugt werden, der Compilezeit-Berechnung durch Auswertung bei der ersten Benutzung einer globalen Variablen emuliert.
Kann ich vor Betreten der `main`-Funktion Initialisierungscode ausführen?
Rust kennt keine Programmausführung vor main
. Am nächsten kommt dem Konzept der lazy-static
-Crate, der dieses Verhalten nachbildet, indem globale Variablen bei ihrer ersten Benutzung initialisiert werden.
Erlaubt Rust Werte für globale Variablen, die keine Compilezeit-Konstanten sind?
Nein. Globale Variablen können keine nicht-compilezeit-konstanten Konstruktoren und überhaupt keine Destruktoren besitzen. Statische Konstruktoren sind unerwünscht, da bei gegenseitigen Abhängigkeiten eine Initialisierungsreihenfolge nicht ohne weiteres garantiert werden kann. Codeausführung vor Beginn der main
-Funktion wird weithin als Misfeature gesehen, weswegen es in Rust nicht umgesetzt ist.
Das C++ FQA hat einen Eintrag zum “static initialization order fiasco”, und Eric Lipperts Blog schreibt über die Herausforderungen, die das Feature in C# mit sich gebracht hat.
Nicht-konstante Initialisierer in globalen Variablen können mit dem lazy-static nachgebildet werden.
Andere Sprachen
Wie kann ich ein C-Konstrukt wie struct X { static int X; }
in Rust implementieren?
Rust kennt keine static
-Attribute wie im obigen Programmausschnitt. Stattdessen kannst du eine static
-Variable auf Modulebene deklarieren, die nur im umgebenden Modul sichtbar ist.
Wie kann ich einen C-ähnlichen Enum in einen Integer konvertieren und umgekehrt?
Ein Enum kann mit einem as
-Cast in einen Integer überführt werden, wie etwa e as i64
(wobei e
ein Enum ist).
Die Gegenrichtung kann mit einem match
-Statement erreicht werden, das Zahlenwerte auf Enum-Werte abbildet.
Warum erzeugen Rust-Programme größere Binaries als C?
Mehrere Faktoren tragen dazu bei, dass Rust-Programme standardmäßig größere Binaries erzeugen als funktionell äquivalente C-Programme. Grundsätzlich optimiert Rust die Performance komplexer Anwendungen, nicht die Größe kleiner Beispielprogramme.
Monomorphisierung
Rust monomorphisiert seine Generics, was bedeutet, dass eine generische Funktion oder ein generischer Typ für jeden konkreten Typ, mit dem er instanziiert wird, neu übersetzt wird. Das ist ähnlich zum Verhalten von Templates in C++. Zum Beispiel werden im folgenden Programm
fn foo<T>(t: T) {
// ... tu irgendwas
}
fn main() {
foo(10); // i32
foo("hello"); // &str
}
zwei unabhängige Versionen von foo
im fertigen Programm auftauchen, jeweils für i32
und &str
spezialisiert. Das erlaubt effiziente frühe Bindung der generischen Funktion, sorgt aber für größere Binaries.
Debug-Symbole
Rust-Programme beinhalten auch im Release-Modus einige Debug-Symbole. Diese ermöglichen Backtraces bei Panics und können mit strip
oder einem ähnlichen Tool aus dem Binary entfernt werden. Der Release-Modus in Cargo ist zum Optimierungslevel 3 in rustc äquivalent. Mittlerweile kennt Rust eine alternative Optimierungsstrategie (Level s
oder z
), mit der der Compiler versucht, die Größe statt der Performance des Programms zu optimieren.
Jemalloc
Rust benutzt standardmäßig jemalloc als Allokator, was die erzeugten Binaries etwas vergrößert. Jemalloc bietet einen konsistentere Allokation mit günstigerer Performance als viele systemeigenen Allokatoren. In Zukunft wird es auch einfacher sein, benutzerdefinierte Allokatoren in Rust einzubinden.
Link-Time Optimization
Rust benutzt standardmäßig keine Link-Time-Optimization, kann aber so eingerichtet werden. Das erhöht das Optimierungspotential des Rust-Compilers und kann einen kleinen Effekt auf die Größe der Binaries haben. Dieser Effekt wird vor allem in Kombination mit den oben genannten Optimierungsstrategien sichtbar.
Standardbibliothek
Die Standardbibliothek von Rust beinhaltet libbacktrace und libunwind, die in manchen Programmen unerwünscht sein können. Mit dem Crate-Attribut #![no_std]
können ohne diese Bibliotheken kleinere Binaries erzeugt werden, die aber üblicherweise wesentliche Änderungen am Code nach sich ziehen. Rust-Code, der ohne die Standardbibliothek geschrieben wurde, ist funktionell äquivalentem C-Code ähnlich.
Als Beispiel liest das folgende C-Programm einen Namen ein und begrüßt den Benutzer mit diesem Namen.
#include <stdio.h>
int main(void) {
printf("Wie heißt du?\n");
char input[100] = {0};
scanf("%s", input);
printf("Hallo %s!\n", input);
return 0;
}
In Rust würde man dieses Programm womöglich wie folgt schreiben:
use std::io;
fn main() {
println!("Wie heißt du?");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
println!("Hallo {}!", input);
}
Dieses Programm wird übersetzt ein größeres Binary produzieren und mehr Speicher in Anspruch nehmen als das C-Programm. Es ist aber nicht wirklich gleichwertig zum obigen C-Code. Das äquivalente Rust-Programm würde eher so aussehen:
#![feature(lang_items)]
#![feature(libc)]
#![feature(no_std)]
#![feature(start)]
#![no_std]
extern crate libc;
extern "C" {
fn printf(fmt: *const u8, ...) -> i32;
fn scanf(fmt: *const u8, ...) -> i32;
}
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
unsafe {
printf(b"Wie heißt du?\n\0".as_ptr());
let mut input = [0u8; 100];
scanf(b"%s\0".as_ptr(), &mut input);
printf(b"Hallo %s!\n\0".as_ptr(), &input);
0
}
}
#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"] fn panic_fmt() -> ! { loop {} }
#[lang="stack_exhausted"] extern fn stack_exhausted() {}
Dieser Code sollte in seinem Speicherverbrauch grob der C-Version entsprechen. Diese Reduktion wird mit zusätzlicher Komplexität und dem Fehlen statischer Garantien erkauft, die Rust üblicherweise gewährt (und die hier mit dem Schlüsselwort unsafe
umgangen wurden).
Warum hat Rust keine stabile ABI wie C, und warum muss ich Symbole mit extern annotieren?
Das Festlegen einer ABI ist eine große Entscheidung, die zukünftige Änderungen an der Sprache behindern könnte. Da Rust erst im Mai 2015 Version 1.0 erreicht hat, ist es noch zu früh, sich an dieser Stelle festzulegen. Das bedeutet nicht, dass es nie eine stabile ABI geben wird (auch wenn C++ es viele Jahre ohne Spezifikation einer ABI geschafft hat).
Über das Rust-Schlüsselwort extern
kann mit spezifischen ABIs wie der C-ABI interagiert werden.
Kann Rust-Code C-Code aufrufen?
Ja. Rust wurde so entworfen, dass C-Code genauso effizient aufgerufen kann wie aus C++.
Kann C-Code Rust-Code Aufrufen?
Ja. Der Rust-Code muss mit einer extern
-Deklaration versehen werden, die ihn C-ABI-kompatibel macht. Eine solche Funktion kann als Funktionszeiger an C übergeben werden oder sogar direkt aus C aufgerufen werden, wenn sie mit dem #[no_mangle]
-Attribut erhält, um Symbol-Mangling zu unterdrücken.
Ich kann bereits perfektes C++ schreiben. Welche Vorteile bietet mir Rust?
Modernes C++ implementiert viele Features, die das Schreiben sicheren und korrekten Codes weniger fehleranfällig macht. Es ist jedoch immer noch sehr einfach, Speicherfehler zu verursachen. Die C++-Hauptentwickler arbeiten daran die Prävalenz dieser Problematik zu verringern, die Sprache lässt aber durch ihre lange Geschichte und die notwendige Rückwärtskompatibilität nur eingeschränkt Änderungen zu.
Rust wurde vom ersten Tag an mit dem Ziel entworfen, eine sichere Systemprogrammiersprache zu sein. Sie ist damit nicht von historischen Entscheidungen belastet, die das Entwickeln sicheren Codes in C++ so kompliziert machen. In C++ wird Sicherheit durch strenge Selbstdisziplin erreicht und kann leicht verletzt werden. In Rust ist Sicherheit die Vorgabe. Die Sprache eröffnet so die Möglichkeit, mit weniger erfahrenen Entwicklern zusammenzuarbeiten, ohne den Code wieder und wieder auf Sicherheitslücken prüfen zu müssen.
Wie lässt sich die Templatespezialisierung aus C++ in Rust umsetzen?
Rust hat zur Zeit noch kein Äquivalent zu Templatespezialisierung, daran wird jedoch gearbeitet. Ähnliche Effekte können aber mittels Assoziierter Typen erzielt werden.
Was hat das Ownership-System von Rust mit Move Semantics aus C++ zu tun?
Die zugrundeliegenden Konzepte sind ähnlich, in der Praxis funktionieren die beiden Systeme jedoch grundsätzlich verschieden. In beiden Fällen fungiert das Verschieben (Move) eines Werts als Möglichkeit, den Besitz zugrundeliegender Ressourcen zu übertragen. So überträgt der Move eines Strings etwa den Stringpuffer, anstatt ihn zu kopieren.
In Rust ist Ownership Transfer das Standardverhalten. So wird eine Funktion, die einen String
als Argument nimmt, den Besitz am übergebenen String-Wert übernehmen:
fn process(s: String) { }
fn caller() {
let s = String::from("Hello, world!");
process(s); // Überträgt den Besitz von `s` an `process`
process(s); // Fehler: `caller` besitzt `s` an dieser Stelle nicht mehr
}
Wie im Codebeispiel oben zu sehen, überträgt der erste Aufruf an process
den Besitz an der Variablen s
. Der Compiler führt über den Besitz von Werten Buch, sodass der zweite Aufruf an process
einen Fehler zur Folge hat - ein gültiges Programm darf den Besitz eines Werts nicht zweimal aufgeben. Rust wird das Verschieben eines Werts auch verhindern, solange noch eine aktive Referenz darauf existiert.
C++ verfolgt einen anderen Ansatz. In C++ werden Werte per Vorgabe kopiert, in dem ihr Kopierkonstruktor aufgerufen wird. Es ist allerdings möglich, Funktionen zu deklarieren, die ihre Argumente als “rvalue-Referenz”, wie etwa string&&
übernehmen. Dies deutet darauf hin, dass die aufgerufene Funktion den Besitz an Ressourcen des Werts übernimmt. Der Aufrufer muss dazu entweder einen temporären Wert übergeben oder einen gebundenen Wert explizit mit std::move
verschieben. Das obige Beispiel würde in C++ etwa wie folgt aussehen:
void process(string&& s) { }
void caller() {
string s("Hello, world!");
process(std::move(s));
process(std::move(s));
}
C++-Compiler müssen über Ownership nicht Buch führen, sodass der obige Code ohne Warnungen oder Fehler übersetzt. In C++ bleibt der Besitz der Variablen s
weiterhin bei caller
(nicht aber der interne Puffer des Strings), sodass trotzdem der Destruktor von s
ausgeführt wird, sobald caller
zurückkehrt. In Rust wird drop
dagegen nur durch den neuen Besitzer des Werts aufgerufen.
Wie kann ich aus Rust C++-Funktionen aufrufen und umgekehrt?
Rust und C++ können C als gemeinsame Schnittstelle benutzen. Sowohl Rust als auch C++ besitzen ein Foreign Function Interface nach C über das sie miteinander kommunizieren können. Falls das Schreiben von C-Bindings zu langwierig wird, kannst du jederzeit auf rust-bindgen zurückgreifen, ein Tool, das dir hilft automatisch funktionierende C-Bindings für Rust zu bauen.
Hat Rust Konstruktoren, wie sie aus C++ bekannt sind?
Nein. Funktionen erfüllen den gleichen Zweck wie Konstruktoren, ohne die Sprachkomplexität zu erhöhen. Der übliche Name für eine Konstrukorfunktion in Rust ist new()
, dabei handelt es sich aber lediglich um eine Namenskonvention. new()
ist tatsächlich eine Funktion wie jede andere. Hier ein Beispiel:
struct Foo {
a: i32,
b: f64,
c: bool,
}
impl Foo {
fn new() -> Foo {
Foo {
a: 0,
b: 0.0,
c: false,
}
}
}
Hat Rust Kopierkonstruktoren?
Nicht direkt. Typen, die den Copy
-Trait implementieren, führen eine C-artige, “flache Kopie” ohne zusätzliche Arbeit oder Funktionsaufrufe durch. Das entspricht dem Konzept der trivially copyable types aus C++. Es ist nicht möglich, Copy
mit abweichendem Verhalten zu implementieren. Dazu dient der Clone
-Trait, dessen Funktionalität durch einen expliziten Aufruf an die clone
-Methode angesprochen wird. Die benutzerdefininerte Kopieroperation wird absichtlich explizit gehalten um die zugrundeliegende Komplexität und potentiell teure Operationen sichtbar zu machen.
Hat Rust Move-Konstruktoren?
Nein. Die Werte aller Typen werden Byte für Byte via memcpy
verschoben. Das macht das Schreiben generischen unsafe
-Codes deutlich einfacher, da das Zuweisen, Übergeben und Zurückgeben eines Werts nie Seiteneffekte wie Unwinding zur Folge haben kann.
Was haben Go und Rust gemeinsam, und wo unterscheiden sich die Sprachen?
Rust und Go haben grundsätzlich verschiedene Designziele. Alle Unterschiede aufzuzählen wäre zu umfangreich, im Folgenden werden einige der wichtigsten genannt.
- Rust arbeitet auf einer niedrigeren Ebene als Go. So setzt Rust im Gegensatz zu Go beispielsweise keinen Garbage Collector voraus. Allgemein bietet Rust ähnlich viel Kontrolle über das Verhalten eines Programms wie C oder C++.
- Der Fokus von Rust liegt darin, Sicherheit und Effizienz zu gewährleisten, gleichzeitig aber für High-Level-Abstraktionen zugänglich zu sein. Go konzentriert sich darauf, eine kleine, einfache Sprache zu sein, die schnell kompiliert und gut mit einer großen Auswahl an Tools zusammenzuarbeiten.
- Rust hat im Gegensatz zu Go gute Unterstützung für Generics.
- Rust ist stark von der Welt der Funktionalprogrammierung beeinflusst, was sich etwa im Typsystem äußert, das die Typklassen aus Haskell in der Form von Traits übernimmt. Go hat ein einfacheres Typsystem, das mit Interfaces einfache generische Programmierung ermöglicht.
Wie lassen sich Traits in Rust mit den Typklassen von Haskell vergleichen?
Rust-Traits sind ähnlich, aber weniger mächtig als Haskell-Typklassen, da Rust keine typen höherer Ordnung unterstützt. Assoziierte Typen in Rust entsprechen Typfamilien in Haskell.
Einige spezifische Unterschiede zwischen Haskell und Rust:
- Rust-Traits haben einen impliziten ersten Parameter
Self
.trait Bar
in Rust entspricht damitclass Bar self
in Haskell, undtrait Bar<Foo>
entsprichtclass Bar foo self
. - “Supertraits” oder “Superklassen-Constraints” werden in Rust
trait Sub: Super
geschrieben, in Haskellclass Super self => Sub self
. - Rust erlaubt keine verwaisten Instanzen (orphan instances), d.h. Trait-Implementierungen, die weder im Modul des Traits noch im Modul des Typs liegen. Haskell erlaubt dies, was zu abweichenden Kohärenzregeln führt.
- Die Auflösung von
impl
-Blöcken berücksichtigt die relevantenwhere
-Klauseln, um zwischenimpl
-Instanzen zu wählen oder Überlappungen festzustellen. Haskell berücksichtigt dabei nur die Bedingungen derinstance
-Deklaration und vernachlässigt Einschränkungen, die an anderen Stellen angegeben wurden. - Eine Teilmenge der Traits in Rust (Object Safe Traits) können für späte Bindung mittels Traitobjekten benutzt werden. Das selbe ist in Haskell mit dem
ExistentialQuantification
-Feature von GHC verfügbar.
Dokumentation
Warum gibt es so viele falsche Antworten zu Rust auf Stack Overflow?
Rust hatte bis zur Veröffentlichung von Version 1.0 im Mai 2015 eine lange Entwicklungsgeschichte hinter sich. Währenddessen hat sich die Sprache signifikant geändert, und einige Antworten auf Stack Overflow beziehen sich auf Sprach- und Bibliothekskonstrukte, die sich mittlerweile geändert haben.
Mit der Zeit werden immer mehr Antworten für die aktuelle Version von Rust entstehen, sodass der Anteil der veralteten Antworten abnehmen wird.
Wie kann ich die Entwickler auf Fehler in der Rust-Dokumentation hinweisen?
Probleme in der Dokumentation kannst du im Rust-Compiler Issue Tracker melden. Lies vorab unsere Richtlinien, um effektiv mitwirken zu können.
Wo finde ich die rustdoc-Dokumentation für eine Bibliothek, von der mein Projekt abhängt?
Wenn du cargo doc
aufrufst, um Dokumentation für dein Projekt zu generieren, wird automatisch auch die Dokumentation für aktiven Versionen aller Abhängigkeiten erzeugt. Diese landet im Unterverzeichnis target/doc
deines Projekts. Um die Dokumentation nach der Erstellung zu öffnen, kannst du cargo doc --open
benutzen oder einfach direkt target/doc/index.html
in deinem Browser öffnen.