Rust ermöglicht es Entwicklern, Memory-Safe-Software zu entwickeln - ohne Garbage Collection und in maschinennativer Geschwindigkeit. Allerdings ist die bei Mozilla entwickelte Programmiersprache auch relativ komplex und weist - zumindest anfänglich - eine eher steile Lernkurve auf.
Die folgenden fünf Programmierfehler sollten Entwickler, die gerade in Rust Fuß fassen (und auch erfahrenere Devs), möglichst nicht begehen.
1. Borrow Checker ausschalten wollen
Ownership, Borrowing und Lifetimes sind integraler Bestandteil von Rust und davon, wie die Sprache Memory Safety ohne Garbage Collection sicherstellt. Einige andere Sprachen bieten Code-Checking-Tools, die auf Sicherheits- oder Memory-Probleme hinweisen, aber es dennoch erlauben, dass der Code kompiliert wird. Rust funktioniert nicht auf diese Art und Weise.
Der Borrow Checker ist der Part des Rust Compilers, der alle Ownership-Prozesse verifiziert - und keine optionale Komponente, die sich nach Belieben abschalten lässt. Code, der nicht durch den Borrow Checker validiert wurde, wird nicht kompiliert. Zur Vertiefung empfiehlt sich ein Blick in die Rust-Dokumentation - genauer gesagt auf die Scoping Rules.
Denken Sie daran, dass Sie Ownership-Probleme umgehen können, indem Sie über .clone()
Kopien anlegen. Auf nicht Performance-intensive Programmkomponenten wird sich das in den seltensten Fällen spürbar auswirken. Das ermöglicht es Ihnen, sich auf die Teile zu konzentrieren, die maximale Zero-Copy-Performance erfordern und herauszufinden, wie sich dort Borrowing und Lifetimes effizienter gestalten lassen.
2. '_' falsch verwenden
Der Variablenname _
(ein einzelner Unterstrich) ist in Rust an ein besonderes Verhalten geknüpft: Der Wert, der in die Variable aufgenommen wird, ist nicht an sie gebunden. Das wird typischerweise für Values genutzt, die direkt verworfen werden. Eine must_use
-Warnung kann beispielsweise typischerweise zum Schweigen gebracht werden, indem ihr ein _
zugewiesen wird. Deshalb sollten Sie _
nicht für Werte verwenden, die über das Statement in dem sie verwendet werden, hinaus bestehen bleiben.
Die Szenarien, auf die Sie achten müssen, sind dabei diejenigen, in denen etwas beibehalten wird, bis es aus dem Scope fällt. Nehmen wir an, es geht um folgenden Code-Block:
let _ = String::from(" Hello World ").trim();
In diesem Beispiel wird der erstellte String sofort nach dem Statement aus dem Scope fallen - also nicht bis zum Ende des Blocks gehalten (der Method-Call soll sicherstellen, dass die Ergebnisse bei der Kompilierung nicht ausgelassen werden).
Der einfachste Weg, diesen Fehler zu vermeiden: Nutzen Sie Konstrukte wie _user
oder _item
möglichst nur für Zuweisungen, die bis zum Ende des Scopes persistent bleiben sollen.
3. Closures wie Funktionen behandeln
Gegeben ist folgende Funktion:
fn function(x: &i32) -> &i32 {
x
}
Sie könnten nun versuchen, diese für einen Rückgabewert der Funktion als Closure auszudrücken:
fn main() {
let closure = |x: &i32| x;
}
Das Problem daran ist, dass es nicht funktioniert. Stattdessen folgt eine Fehlermeldung des Compilers (lifetime may not live long enough
). Ein Weg, das zu umgehen, führt über eine Static Reference:
fn main() {
let _closure: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;
}
4. Destructors nicht verstehen
Wie C++ erlaubt auch Rust, Destructors für Types zu erstellen, die ausgeführt werden, wenn ein Objekt aus dem Scope fällt. Dass das so passiert, ist allerdings nicht garantiert.
Insbesondere nicht, wenn ein Borrow für ein bestimmtes Objekt ausläuft. Letzteres bedeutet aber nicht, dass bereits sein Destructor ausgeführt wurde. Dieser soll nicht immer ausgeführt werden, weil ein Borrow ausgelaufen ist - etwa, wenn ein Pointer ins Spiel kommt.
Die Rust-Dokumentation hält auch zu diesem Themenbereich ausführliche Guidelines bereit.
5. unsafe unterschätzen
Das Keyword unsafe
existiert, um Rust-Code zu taggen, der beispielsweise Raw Pointer dereferenziert. Das ist eine Angelegenheit, mit der Sie bei Ihrer Rust-Programmierarbeit (hoffentlich) nicht oft konfrontiert werden. Falls doch, eröffnen sich dadurch nämlich viele neue Problemwelten.
Ein Raw Pointer Dereferencing - um bei diesem Beispiel zu bleiben - das durch einen unsafe
-Prozess hervorgerufen wurde, resultiert in einer Unbounded Lifetime. Letztere kann unerwartet schnell über das hinauswachsen, was Sie ursprünglich beabsichtigt haben.
Vorausgesetzt, Sie gehen gewissenhaft mit einer Unbounded Reference um, sollten Ihnen solche Probleme erspart bleiben. Aus Sicherheitsgründen empfiehlt es sich jedoch, dereferenzierte Pointer in einer Funktion zu platzieren und Lifetimes an der Funktionsgrenze zu verwenden - statt sie innerhalb des Funktionsbereichs "loszulassen".
6. .unwrap() für makellos halten
Wenn eine Operation ein Result
liefert, gibt es zwei grundlegende Möglichkeiten, damit umzugehen:
Entweder Sie nutzen
.unwrap()
oder einen seiner Verwandten (beispielsweise.unwrap_or()
) oderSie setzen auf ein vollständiges
match
-Statement um einErr
-Ergebnis zu behandeln.
Der wesentliche Vorteil von .unwrap()
ist dabei, dass es praktisch ist: Falls Sie sich in einem Code-Pfad befinden, in dem ein unerwarteter Fehler auftritt, erhalten Sie mit .unwrap()
den Wert, den Sie brauchen, um Ihre Arbeit fortzusetzen. Das hat jedoch auch seinen Preis: Jede einzelne Error Condition verursacht eine Panic und unterbricht das Programm. Panics in Rust sind auch aus gutem Grund nicht behebbar: Sie weisen darauf hin, dass Bugs vorhanden sind.
Wenn Sie .unwrap()
oder eine seiner Varianten verwenden, sollten Sie sich der limitierten Error-Handling-Fähigkeiten bewusst sein. Sie müssen einen Wert übergeben, der mit dem Typ übereinstimmt, den ein OK
-Wert erzeugen würde. Mit match
erlangen Sie eine weitaus größere Behavior-Flexibilität. (fm)
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.