- Rust erhöht mit starken Sicherheitsgarantien die Produktivität und Wartbarkeit, weil sich selbst in großen Codebasen Refactorings mit Zuversicht durchführen lassen
- Der Compiler erkennt Bugs im Zusammenhang mit asynchronem Scheduling im Voraus und stärkt die Stabilität, indem er undefiniertes Verhalten verhindert
- Sprachen wie TypeScript haben aufgrund ihres lockeren Typsystems häufig asynchrone Bugs, die erst in der Produktionsumgebung entdeckt werden
- Rusts Typsystem zeigt die Auswirkungen von Codeänderungen klar auf und erhöht so in komplexen Projekten Zuverlässigkeit und die Bereitschaft zu experimentieren
- Zig ist im Gegensatz zu Rust bei der Fehlerbehandlung lockerer und kann deshalb Bugs durch Tippfehler übersehen, was die Zuverlässigkeit senkt
Zusammenfassung und Hintergrund
- Das Backend von Lubeno ist zu 100 % in Rust geschrieben, und die Codebasis hat einen Umfang erreicht, bei dem sie sich nicht mehr vollständig im Kopf überblicken lässt
- In großen Projekten ist es generell schwer, Nebenwirkungen von Änderungen zu erkennen, was normalerweise zu Produktivitätsverlusten führt
- Rusts Sicherheitsgarantien machen bei Codeänderungen die Auswirkungen klar sichtbar und nehmen so die Angst vor Refactorings
- Das trägt langfristig zu besserer Wartbarkeit und höherer Produktivität bei
- Der Text beginnt mit einem Fall, in dem der Rust-Compiler einen asynchronen Bug erkannt hat, und untersucht daran Rusts Produktivitätsvorteile
Beispiel für Rusts Sicherheitsgarantien
- Problemsituation: Eine Struktur wird für gleichzeitigen Zugriff in einen Mutex gepackt, nach dem Erwerb des Locks wird asynchrone Arbeit ausgeführt
let lock = mutex.lock();
db.insert_commit(commit).await;
- Entdeckung des Problems: rust-analyzer zeigte keinen Fehler an, aber in der Datei mit der Router-Definition trat ein Compilerfehler auf
.route("/api/git/post-receive", post(git::post_receive))
^^^^^^^^^^^^^^^^^
error: future cannot be sent between threads safely
- Ursachenanalyse:
- Das Web-Framework erstellt für jede HTTP-Verbindung einen asynchronen Task, und der Task-Scheduler verschiebt Tasks zwischen Threads
- Ein Mutex erfordert, dass derselbe Thread den Lock wieder freigibt; wenn der Thread an einer
.await-Stelle wechselt, kann undefiniertes Verhalten entstehen
- Der Rust-Compiler verfolgt die Lebensdauer des Locks und erkennt, dass es möglicherweise auf einem anderen Thread freigegeben wird
- Lösung: Den Lock vor
.await freigeben
- Bedeutung: Rust verhindert asynchrone Bugs bereits zur Compile-Zeit, selbst wenn sie in der Entwicklungsumgebung schwer zu reproduzieren sind
Vergleichsbeispiel mit TypeScript
- Problemsituation: In TypeScript-Code tritt ein Bug bei einer asynchronen Weiterleitung auf
if (redirect) {
window.location.href = redirect;
}
let content = await response.json();
if (content.onboardingDone) {
window.location.href = "/dashboard";
} else {
window.location.href = "/onboarding";
}
- Ursache des Problems:
window.location.href führt die Weiterleitung nicht sofort aus, sondern plant sie nur ein; der Code läuft weiter
- Durch eine Race Condition kommt es zu einer unbeabsichtigten Weiterleitung
- Lösung: Im
if-Block ein return ergänzen
if (redirect) {
window.location.href = redirect;
return;
}
- Einschränkung: TypeScript hat keine Nachverfolgung von Lebensdauern oder Borrowing-Regeln und kann solche Bugs daher nicht zur Compile-Zeit erkennen
- Sie werden erst in Produktion entdeckt, und das Debugging kostet viel Zeit
Rusts Vorteile beim Refactoring
- In der Webentwicklung bieten Python, Ruby und JavaScript/Node.js anfangs hohe Produktivität, doch mit wachsender Codebasis werden Änderungen durch lockere Kopplung schwieriger
- Nach Änderungen treten unerwartete Fehler auf, was die Bereitschaft verringert, Code anzupassen
- Rusts Typsystem zeigt die Auswirkungen von Änderungen klar auf und nimmt so die Angst vor Refactorings
- Beispiel: Warnungen wie „Diese Änderung könnte andere Teile beeinflussen“ verhindern Probleme im Voraus
- Selbst wenn die Codebasis wächst, steigt die Produktivität weiter, weil bestehender Code wiederverwendet werden kann und Änderungen stabil bleiben
Vergleich mit Tests
- Tests sind bei Refactorings nützlich, um Regressionen zu vermeiden, aber da der Compiler sie nicht erzwingt, können sie ausgelassen werden
- Das Schreiben von Tests bedeutet eine hohe mentale Belastung, weil entschieden werden muss: Abstraktionsebene, Verhalten vs. Implementierungsdetails und welche Fehler verhindert werden sollen
- Rust blockiert mit dem Compiler typische Fehler im Voraus und reduziert so die Entscheidungsbelastung, die sonst bei Tests anfällt
- Eigenschaften, die sich nicht über das Typsystem prüfen lassen, werden durch Tests ergänzt
Vergleich mit Zig
- Zig ist wie Rust eine Systemprogrammiersprache, ist bei der Fehlerbehandlung jedoch lockerer
- Bei Verwendung von switch wird ein Tippfehler erkannt, in einer if-Anweisung jedoch ignoriert, was Zuverlässigkeitsprobleme verursacht
- Rust vermeidet solche Designlücken und prüft Tippfehler und logische Fehler strikt
Implikationen
- Rust verbessert mit Sicherheitsgarantien und einem strengen Typsystem die Produktivität und Stabilität großer Projekte
- Selbst komplexe Probleme wie asynchrone Bugs werden zur Compile-Zeit erkannt, was die Wartungskosten senkt
- Die Beispiele mit TypeScript und Zig zeigen die Risiken lockerer Prüfungen und unterstreichen den Wert von Rusts strengem Compiler
- Rust etabliert sich auch in der Webentwicklung als starkes Werkzeug nicht nur für anfängliche Produktivität, sondern auch für das langfristige Management von Codebasen
Noch keine Kommentare.