3 Tage Rust
« | 11 Feb 2023 | »In der Nachbarabteilung meiner Firma wurde festgestellt, dass dotNet Software “nicht ganz so gut performt” und deshalb musste eine schnellere Sprache her. Das wurde dann nicht C++, weil das angeblich zu kompliziert ist, sondern Rust.
Und weil in der bestellten Rust-Schulung für Mitarbeiter noch ein Platz frei war, durfte ich mich da auch hineinsetzen.
Als C und C++ Entwickler sehe ich da eine “Konkurrenz” aufkommen, deren
Notwendigkeit sich mir eigentlich nicht erschließt. … aber das ist nicht
verwunderlich, genau so ging es mir vor über 10 Jahren schon mit
Objective-C.
Wenn man sein Handwerk beherrscht, sieht man nicht die Notwendigkeit es
durch etwas anderes zu ersetzen.
Und so fiel mir intensiv auf, dass (wie immer) alles nur anders benannt ist um sich zu unterscheiden, und im Endeffekt auch “nur programmiert” wird.
Beim Längeren Nachdenken versteht man dann aber doch einige Dinge besser und kann ihren (wenn auch seltsam wirkenden) Ursprung nachvollziehen.
Die Vorteile
Formzwang
Als C/C++ Mensch liebt und hasst man die völlige Freiheit im Code gleichzeitig, denn man selbst kann sich frei entfalten, und weil das alle so machen, versteht man den Code des Nachbar nicht mehr.
Der Rust Compiler geht hier so weit, dass er Verletzungen des Codingstyles
als Warnung anzeigt. Wer also
fn saySomething()
schreibt, bekommt sofort mitgeteilt, dass es besser
fn say_something()
heißen sollte.
Und ich gebe zu, dass auch ich an mir selbst inzwischen leide, dass ich mich mal darauf festgelegt habe, in C anders zu arbeiten als in C++, damit man die Ebenen besser unterscheiden kann … aber lesbarer wird die Sache im Ganzen dadurch nicht.
Lebensdauer und Konstanten
Dass Instanzen von Variablen eine begrenzte Lebensdauer haben, ist üblich, doch Rust verfolgt jede Variable und Referenz und vermag daher dem Anwender gleich zu melden, wenn dieser auf etwas zugreift, was nicht mehr existiert.
In C++ hingegen dürfen wir sogar lokale Variablen als Referenz aus einer Funktion zurückgeben, was selten eine gute Idee ist.
Auch das transferieren einer Variable in eine andere folgt festen Spielregeln, womit es erst einmal ausgeschlossen ist, dass zwei Threads auf die gleiche Speicherzelle zugreifen dürfen. Erst mit zusätzlichem Aufwand werden solche Zugriffe möglich, was in C++ ohne Warnung oft “als Unfall” passiert.
Dass außerdem erst einmal alles “konstant” ist und explizit als “veränderbar”
(mut
) markiert werden muss, ist entgegen der Norm. Doch wenn man auch hier
wieder an Multithreading denkt, ergeben sich damit enorme Sicherheiten, die
man in anderen Sprachen schnell und oft leichtsinnig verliert.
Fehlerbehandlung
Und wer ein Fan von C-Returncodes ist und C++
Exceptions hasst, der wird
Rusts enum Result<T, E>
richtig lieben.
Mit dem ?
-Operator wird nämlich das
if(!ok) return error;
in ein Zeichen am Ende eines Aufrufs hineinkodiert.
Man programmiert also den Erfolgsfall weiter und darf sich darauf verlassen,
dass Rust den Fehler automatisch zurückgibt, bis er irgendwo explizit
behandelt wird … und das alles ohne Exception-Overhead.
… und vieles mehr
Natürlich kann man jetzt nicht alle Vorzüge auflisten, die lernt man in jedem Rust-Tutorial. Doch die 3 genannten Punkte gefallen mir persönlich sehr.
Die andere Seite
Wenn eine Sprache aber so andersartig und seltsam ist, dass sie mit wenig vergleichbar ist, dann stellt sich einem schon die Frage, ob das gut, und vor allem ob das sinnvoll ist.
Traits vs. Klassen
Ein Mathematiker mag es anders sehen, doch ich als Embedded-Programmierer schätze das Interface- Objekt- und Vererbungsmodell der Sprachen C++, C#, und Java wirklich sehr.
Rust kennt dieses Konzept nicht, sondern lässt uns Traits
für jedes
neue Element implementieren. Parallel dazu werden Attribute eingesetzt
um “ähnliche” Eigenschaften magisch hinzuzufügen.
Während mir in C++ eine Klasse ganz genau sagt, von wem sie Eigenschaften bekommen (geerbt) hat, kann in Rust jeder Typ im Nachhinein durch ein Trait “erweitert” werden. Übersichtlich ist das nicht.
Keine Trennung von Kernsprache und Bibliothek
Während in C++ Operatoren als Teil der Kernsprache überladen werden können,
nutzt Rust (für mich willkürlich definierte) Traits.
Wer für eine Struktur das Trait std::ops::Add
implementiert, der kann dann
seine Instanzen mit obj1 + obj2
addieren, was ja schön und gut ist.
Doch dass dafür von der Kernsprache in einen Bibliotheksnamensraum
hineingegriffen werden muss um das Operatorsymbol +
zu “überladen”,
ist meiner Erfahrung nach kein guter Designansatz.
Uneinheitliche Typenbestimmung
Der Rust-Compiler rät sehr geschickt herum um den Typ einer Variablen
festzustellen. Man könnte diesen explizit festlegen, doch das macht
man nur, wenn es sein muss.
Wann man was definieren muss scheint mir recht kompliziert definiert
zu sein. Manchmal reicht ein _
als Platzhalter, dass der Compiler
später den Typen einfügt, manchmal nicht.
Manchmal braucht man den “Turbofish” (mit Ähnlichkeit zu expliziten
C++ Template Instanziierungen, z.B.: foo().bar::<Vec<i32>>::()
),
um Typen festzulegen, weil es nicht anders geht, und manchmal errät der
Compiler alles ohne Typenangaben.
Selbst unser Trainer im Kurs konnte das nicht immer so genau sagen.
… und vieles mehr …
Ich will jetzt gar nicht von der hässlichen Lifetime-Syntax wie
fn foo<'a>(x: &'a i32)
anfangen, kurz gesagt: Rust ist so
fremdartig, dass es wenig Anknüpfpunkte (und Zusammenarbeit) mit
anderen Sprachen gibt.
Fazit
Der für mich größte Nachteil von Rust ist: Es hat kein standardisiertes
ABI um seine Features
ins System exportieren zu können, schlimmer noch, es will auch gar keines.
Rust kann Bibliotheken und Sourcen nur in ein großes Binary
zusammenkompilieren.
Das entspricht reinen statischen Bibliotheken in C++. Und das fände ich eigentlich sogar ganz gut … doch dass man eben gar keine nativen Rust DLLs oder SOs (ohne Kunstgriffe) erzeugen kann, ist eine kritische Einschränkung.
Einzige Option: eine C-API. Man kann Rust Code in C-Funktionen kapseln, damit sie von anderen Modulen benutzt werden können. Doch genau hier gehen dann alle Sicherheitsfeatures von Rust wieder verloren und man muss die sicheren Rust-Strings in Null-terminierte-C-Strings umwandeln, usw.
Als Sprache finde ich Rust allerdings sehr interessant und ich hoffe,
dass C++ noch einiges daraus lernen wird.
Für mich wird es jedoch über eine “einfache” C-Anbindung vermutlich
wenig Gründe geben, irgend etwas in Rust zu machen.
Dafür habe ich in C/C++ einfach schon 15 Jahre Vorsprung.
Außerdem … inzwischen gibt es ja mit ZIG die nächste Programmiersprache, die besser und performanter als Rust sein soll. Und wenn immer alle von vorne anfangen “ihre eigene” Sprache neu zu entwickeln, gibt es auf lange Sicht auch keinen Fortschritt.