Der Einfluss von Rust auf meine Programmierung

Vor etwa einem Jahr bin ich über Rust gestolpert - wieder einmal. Ich habe Rust immer als den "Gipfel der Programmierung" angesehen, weil der Compiler Speichersicherheit durchsetzt und einige seltsame Fehlermeldungen ausgibt. Trotz der (vermeintlich) steilen Lernkurve habe ich beschlossen, dass es an der Zeit ist, es auszuprobieren. Ich habe angefangen, Rust zu lernen, und da ich ein "Learning by doing"-Typ bin, habe ich beschlossen, Rust in meinem Masterarbeit. Nach einigen anfänglichen, aber vorhersehbaren Schwierigkeiten habe ich die Sprache immer besser beherrscht.
Als ich mich an das Schreiben von Rust-Code gewöhnt hatte, stellte ich fest, dass sich meine Art, Code zu schreiben, dramatisch verändert hatte. Meine Einstellung zu veränderbaren Daten und dazu, welchem Teil einer Anwendung oder Funktion die Daten gehören, hat sich verbessert. Lassen Sie mich meine Erkenntnisse mit Ihnen teilen, und damit auch eine kurze Einführung in Rust.

Lassen Sie sich nicht von Verweisen und Hinweisen einschüchtern
Rust wurde als Programmiersprache auf Systemebene entwickelt. Sie ermöglicht es Ingenieuren, das zu tun, was sie tun - nur besser, schneller, stärker.
Ein Beispiel: Die Programmierung auf Systemebene gilt als das Zauberreich der Technik. Dieses Thema birgt jede Menge Fallstricke, da der Speicher manuell gehandhabt werden muss (neben anderen Eigenheiten). Rust wurde entwickelt, um diese Barriere einzureißen und ein Ökosystem zu schaffen, das eine schnelle Codeausführung ohne die Angst vor Speicherbeschädigung ermöglicht.
Daher gibt es in Rust Referenzen, Zeiger, Referenzzähler und andere unheimlich klingende Dinge. Wie auch immer, lassen Sie sich von diesen Dingen nicht einschüchtern. Wenn Sie eine "Hochsprache" wie C#, Java oder JavaScript verwendet haben, sind Sie bereits mit Referenzen und Zeigern vertraut. In den meisten Sprachen werden komplexe Typen (auch Objekte genannt) als Referenz übergeben, während primitive Typen (wie Ganzzahlen, Strings usw.) als Wert übergeben werden.
Nach Wert bedeutet, dass die empfangende Funktion eine Kopie des effektiven Wertes erhält, und Sie können den Wert nicht so ändern, dass die aufrufende Funktion die Änderung bemerkt. Im Gegensatz dazu, durch Verweis übergibt eine Speicheradresse an die Funktion, in der sie den komplexen Typ findet. Das bedeutet, dass beide Funktionen dieselbe Instanz des Objekts nutzen. Die Änderung einer Eigenschaft des Typs führt dazu, dass die aufrufende Funktion nach der Ausführung der Funktion die gleiche Änderung sieht.
Rust hat eine Notation für Referenzen (die klassische: &
) und ermöglicht es Ihnen somit, auszuwählen, ob Sie ein Objekt per Verweis oder per Wert wünschen. Auch wenn die Parameter komplexe Typen sind. Sie können nun explizit auswählen, ob Sie einen Klon Ihres komplexen Objekts (nach Wert) oder nur einen Verweis auf das Objekt benötigen.
Kämpfen Sie nicht gegen den Compiler
Als ich meine Rust-Reise begann, war ich wütend auf den Compiler. Er ließ mich nicht tun, was ich wollte. Wie sich herausstellte, hatte er von Anfang an recht. Sobald ich anfing, die Meldungen anzunehmen und mein Verständnis für die Sprache und ihre Konzepte wuchs, begann ich die Compiler-Meldungen zu lieben.
Rust hat einen der besten Compiler-Fehler, die ich in meiner Karriere als Softwareentwickler gesehen habe. Der Compiler meldet einen Fehler und weist in der gleichen Fehlermeldung darauf hin was ging schief und wie reparieren Sie es.
Ich möchte Ihnen ein Beispiel geben:
fn inputs() -> Vec<(i32, i32)> {
return vec![(0, 0)];
}
fn main() {
let scores = inputs().iter().map(|(a, b)| {
a + b
});
println!("{}", scores.sum::<i32>());
}
Der obige Code erzeugt einen Vektor mit ganzzahligen Tupeln. Die aufrufende Funktion (main) erhält die Eigentümerschaft über den Vektor und gibt ihn sofort an iter() weiter. Daher geht der temporäre Wert aus dem Anwendungsbereich und wird aus dem Speicher gelöscht (mehr dazu später...). Dieser Code führt zu dem folgenden Fehler:
5 | let scores = inputs().iter().map(|(a, b)| {
| ^^^^^^^^ creates a temporary which is freed while still in use
6 | a + b
7 | });
| - temporary value is freed at the end of this statement
8 | println!("{}", scores.sum::<i32>());
| ------ borrow later used here
help: consider using a `let` binding to create a longer lived value
|
5 ~ let binding = inputs();
6 ~ let scores = binding.iter().map(|(a, b)| {
|
For more information about this error, try `rustc --explain E0716`.
Der Compiler gibt nicht nur das Problem mit dem Code korrekt an, sondern liefert auch eine Lösung für das Problem. Dies funktioniert bei verschiedenen Fehlern während der Kompilierung (wie z.B. das doppelte Ausleihen von eigenen Daten oder das Benötigen eines Verweises anstelle eines Wertes usw...). Alle Fehler können mit dem --erläutern Sie
Flagge.
Dadurch habe ich ein besseres Verständnis für solche Fehlermeldungen entwickelt. In anderen Sprachen sind die Meldungen vielleicht nicht so hilfreich, aber ich habe gelernt, ihre Macken zu umgehen und die richtigen Informationen zu analysieren, die ich brauche, um mein Problem zu beheben. Es gibt sogar einige Tools, die einem dabei helfen (zum Beispiel in TypeScript: https://ts-error-translator.vercel.app/).
Dateneigentum, Veränderbarkeit und Speichersicherheit sind wichtig
Der Rust-Compiler ist ein harter Brocken. Im obigen Abschnitt wurde Code gezeigt, der außerhalb des Gültigkeitsbereichs liegt und auf den nicht mehr sicher zugegriffen werden kann. Der Compiler beschwert sich über die Lebensdauer und gibt Ihnen eine Lösung für das Problem. Da Rust keinen Garbage Collector hat, wird der Speicher "manuell" verwaltet. Allerdings verwaltet die Sprache den Speicher für Sie, Sie müssen ihn nicht wie in C oder C++ selbst verwalten.
In Verbindung mit der Speichersicherheit (und der Lebensdauer von Daten) ist ein weiteres Kernkonzept, das Rust zu einer interessanten Sprache macht, das Eigentum an Daten und deren Veränderbarkeit. In Rust gilt für jede Variable (d.h. Daten) "write once, read many". Dies verbietet den mehrfachen schreibbaren Zugriff auf Daten und verhindert generell Seiteneffekte.
fn take_a_copy(data: String) { /* do something */ }
fn take_a_read_only_reference(data: &str) { /* do something */ }
Bei den beiden obigen Funktionen gibt es zwei verschiedene Arten von Dateneigentum (oder deren Übernahme). Die erste Funktion übernimmt die String-Daten als Wert. Der Aufrufer dieser Funktion muss einen neuen String erstellen oder den Besitz vollständig an die Funktion übergeben. Wird die Eigentümerschaft an die Funktion übergeben, kann sie zurückgegeben werden, aber der Aufrufer ist nicht verpflichtet und kann die Eigentümerschaft behalten. Die zweite Funktion erfordert einen Verweis auf eine Zeichenkette, jedoch nur mit Lesezugriff. Damit können die Daten "ausgeliehen" werden, aber das Eigentum liegt weiterhin beim Aufrufer.
fn main() {
let the_str: String = "Hello, world!".to_string();
take_a_copy(the_str);
take_a_copy(the_str); // <-- This will produce a compiler error.
}
Der Compiler wird sich beschweren:
use of moved value: `the_str`
value used here after move
Das Eigentum wurde von der nehmen_eine_kopie
und die weitere Verwendung derselben Variablen führt zu dem Fehler. Auch ein weiteres "Ausleihen" der Daten ist nicht möglich.
Das Erstellen eines Klons funktioniert jedoch, da dabei eine neue Kopie der Daten erstellt wird und die Eigentümerschaft direkt an den Aufrufenden weitergegeben wird. Eine weitere Möglichkeit ist die Verwendung von Nur-Lese-Referenzen:
fn main() {
let the_ref: &str = "Hello, world!";
let the_str: String = "Hello, world!".to_string();
take_a_copy(the_str.clone());
take_a_copy(the_str.clone());
take_a_read_only_reference(&the_str);
take_a_read_only_reference(&the_str);
take_a_read_only_reference(&the_ref);
take_a_read_only_reference(&the_ref);
}
Der obige Code lässt sich gut kompilieren, da die nehmen_eine_kopie
Funktion erhält in beiden Fällen einen Klon und die einen_nur_gelesenen_Vergleich nehmen
Funktion hat nur Lesezugriff auf einen Verweis auf die Daten (d.h. eine "Leseausleihe").
Dieses Konzept hat meine Denkweise über Daten und ihre Veränderbarkeit drastisch verbessert. Ich habe meinen Codestil in anderen Sprachen angepasst, um den Eigentumsregeln von Rust zu entsprechen. Ich erstelle Kopien von Daten, wo sie benötigt werden, und übergebe sie "per Referenz" (Standard in vielen Sprachen, wenn komplexe Typen verwendet werden), achte aber darauf, den Schreibzugriff jeweils nur einmal zu erlauben. Dieses Konzept trägt dazu bei, Seiteneffekte auf Ihre Daten auf elegante Weise abzumildern. Weitere Informationen über das Thema Dateneigentum finden Sie im Rust-Buch: Was ist Eigentum? und Referenzen und Kreditaufnahme.
Schlussfolgerungen
Rust hat meine persönliche Sicht auf Code positiv verändert. Es spielt keine Rolle, ob man die Sprache in einer Arbeitsumgebung verwendet oder nicht. Trotz der Lernkurve ist es eine nützliche Sprache, die man lernen kann.
Vor allem die Konzepte zum Dateneigentum und zu den Lebenszeiten haben es mir angetan. Ich ertappe mich oft dabei, dass ich jetzt über den Datenbesitz nachdenke und den Code entsprechend anpasse. Nachdem ich den Code geändert habe, wird er meistens noch lesbarer und verständlicher.
Der perfekte Ort, um mit dem Lernen zu beginnen, ist das Buch über die Programmiersprache Rust: https://doc.rust-lang.org/book/. Das Buch ist gut strukturiert und erfahrene Entwickler können die Teile, in denen Variablen eingeführt werden, überspringen. Ich persönlich empfehle jedoch, die Kapitel über Eigentum, Eigenschaften und Lebenszeiten sehr sorgfältig zu lesen.

Geschrieben von
Christoph Bühler