32 Punkte von GN⁺ 2025-12-07 | Noch keine Kommentare. | Auf WhatsApp teilen
  • Es werden Coding-Gewohnheiten vorgestellt, die Rusts Typsystem und Compiler aktiv nutzen, um Bugs im Vorfeld zu verhindern
  • Anhand von Beispielen für anfällige Code Smells wie Vektor-Indizierung, übermäßige Nutzung von Default, unvollständige match-Anweisungen und unnötige boolesche Parameter werden Alternativen erläutert
  • Das zentrale Prinzip ist, Strukturen so zu entwerfen, dass der Compiler Invarianten erzwingt, etwa mithilfe von Pattern Matching, privaten Feldern und dem Attribut #[must_use]
  • Es werden konkrete defensive Techniken auf Code-Ebene vorgestellt, darunter die Verwendung von TryFrom, vollständiges Destrukturieren von Structs, temporäre Mutabilität und Validierung im Konstruktor
  • Solche Muster sind essenziell, um bei Refactorings Stabilität zu sichern und die langfristige Wartbarkeit zu verbessern

Überblick über defensives Programmieren

  • Stellen mit Kommentaren wie // this should never happen sind Punkte, an denen implizite Invarianten verletzt werden
    • In den meisten Fällen hat der Entwickler nicht alle Randfälle oder künftigen Codeänderungen berücksichtigt
  • Der Rust-Compiler garantiert Speichersicherheit, aber Fehler in der Business-Logik sind weiterhin möglich
  • Kleine gewohnheitsmäßige Muster (Idioms) aus langjähriger Praxiserfahrung können die Codequalität deutlich verbessern

Code Smell: Vektor-Indizierung

  • Die Form if !vec.is_empty() { let x = &vec[0]; } birgt ein Risiko für Runtime-Panics, weil Längenprüfung und Indizierung getrennt sind
  • Verwendet man Slice-Pattern-Matching (match vec.as_slice()), kann der Compiler alle Zustände zur Prüfung erzwingen
    • Leere Vektoren, einzelne Elemente, doppelte Elemente und andere Fälle lassen sich explizit behandeln
  • Das ist ein typisches Beispiel für Design, bei dem der Compiler Invarianten garantiert

Code Smell: unbedachter Einsatz von Default

  • ..Default::default() führt zu Problemen wie dem Risiko ausgelassener Initialisierung bei neuen Feldern und impliziten Wertzuweisungen
  • Wenn alle Felder explizit initialisiert werden, erzwingt der Compiler auch das Setzen neuer Felder
  • Mit Formen wie let Foo { field1, field2, .. } = Foo::default(); kann man eine Default-Struktur destrukturieren und anschließend gezielt überschreiben
    • So entsteht ein Gleichgewicht zwischen Standardwerten und expliziten Overrides

Code Smell: fragile Trait-Implementierungen

  • Wenn Struct-Felder für Vergleiche vollständig destrukturiert werden, führt das Hinzufügen neuer Felder zu Compiler-Fehlern als Warnsignal
    • Beispiel: In einer PartialEq-Implementierung let Self { size, toppings, .. } = self;
  • Kommt ein neues Feld wie extra_cheese hinzu, wird eine erneute Prüfung der Vergleichslogik erzwungen
  • Dasselbe Prinzip lässt sich auch auf andere Traits wie Hash, Debug und Clone anwenden

Code Smell: TryFrom statt From erforderlich

  • Wenn eine Konvertierung nicht immer erfolgreich sein kann, sollte statt From explizit TryFrom verwendet werden, um die Möglichkeit eines Fehlers sichtbar zu machen
  • Die Verwendung von unwrap_or_else ist ein Signal dafür, dass potenzielles Scheitern verborgen wird; ein früher Abbruch (fail fast) ist sicherer

Code Smell: unvollständiges match

  • Catch-all-Patterns wie _ => {} bergen das Risiko ausgelassener Fälle, wenn neue Variants hinzukommen
  • Werden alle Variants explizit aufgelistet, warnt der Compiler bei fehlender Behandlung neuer Fälle
  • Dieselbe Logik kann in Formen wie Variant3 | Variant4 gruppiert werden

Code Smell: übermäßige Verwendung des Platzhalters _

  • Wenn nur _ verwendet wird, ist unklar, welche Variable ausgelassen wurde
  • Mit expliziten Namen wie has_fuel: _, has_crew: _ verbessert sich die Lesbarkeit

Pattern: temporäre Mutabilität (Temporary Mutability)

  • Wenn Daten nur während der Initialisierung mutierbar sein sollen, kann man Formen wie let mut data = ...; data.sort(); let data = data; verwenden
  • Mit Block-Scopes lässt sich verhindern, dass temporäre Variablen nach außen sichtbar werden
    • Beispiel: let data = { let mut d = get_vec(); d.sort(); d };
  • In Initialisierungsabläufen mit mehreren temporären Variablen ist so eine klare Abgrenzung der Gültigkeitsbereiche möglich

Pattern: Konstruktorvalidierung erzwingen

  • Beim Erzeugen eines Structs sollte zwingend Validierungslogik durchlaufen werden
    • Mit einem Feld wie _private: () wird direkte Konstruktion von außen verhindert
    • Das Attribut #[non_exhaustive] verhindert Konstruktion außerhalb des Crates und signalisiert zukünftige Erweiterbarkeit
  • Wenn dies auch innerhalb interner Module erzwungen werden soll, kann eine verschachtelte Modulstruktur mit einem privaten Typ (Seal) verwendet werden
    • Da Seal nur intern existiert, ist direkte Konstruktion außerhalb von new() nicht möglich
  • Werden Felder privat gehalten und Getter bereitgestellt, bleibt der unveränderliche Zustand gewahrt
  • Kriterien für die Anwendung
    • Externen Code blockieren: _private oder #[non_exhaustive]
    • Internen Code blockieren: privates Modul + Seal
    • Validierungslogik in Garantien auf Compiler-Ebene überführen

Pattern: Verwendung des Attributs #[must_use]

  • #[must_use] verhindert, dass wichtige Rückgabewerte ignoriert werden
    • Beispiel: #[must_use = "Configuration must be applied to take effect"]
  • Ignoriert ein Nutzer den Rückgabewert, gibt der Compiler eine Warnung aus
  • Das ist ein einfaches, aber starkes defensives Mittel, das auch in der Standardbibliothek, etwa bei Result, breit eingesetzt wird

Code Smell: boolesche Parameter

  • Formen wie fn process_data(..., compress: bool, encrypt: bool, validate: bool) sind semantisch unklar und fehleranfällig durch vertauschte Reihenfolge
  • Mit enum Compression, enum Encryption usw. lässt sich die Absicht explizit ausdrücken
  • Bei mehreren Optionen sollte ein Parameter-Struct (Params struct) verwendet werden
    • Mit Methoden wie ProcessDataParams::production() lässt sich die Wiederverwendbarkeit durch Voreinstellungen verbessern
  • Beim Hinzufügen neuer Optionen bleibt der Einfluss auf bestehende Aufrufstellen minimal

Automatisierung mit Clippy-Lints

  • Wichtige defensive Muster lassen sich automatisch mit Clippy-Lints prüfen
    • indexing_slicing: direkte Indizierung verbieten
    • fallible_impl_from: TryFrom statt From empfehlen
    • wildcard_enum_match_arm: _-Pattern verbieten
    • fn_params_excessive_bools: vor zu vielen booleschen Parametern warnen
    • must_use_candidate: Kandidaten für #[must_use] vorschlagen
  • Über #![deny(clippy::...)] oder Konfiguration in Cargo.toml lässt sich dies projektweit anwenden

Fazit

  • Der Kern defensiven Programmierens in Rust besteht darin, mithilfe von Typsystem und Compiler Invarianten explizit und überprüfbar zu machen
  • Diese Muster tragen dazu bei, bei Refactorings Stabilität zu sichern, die Wahrscheinlichkeit von Bugs zu minimieren und die langfristige Wartbarkeit zu stärken
  • Es ist ein Ansatz, der das Prinzip verwirklicht: „Der beste Bug ist der, der gar nicht erst kompiliert.“

Noch keine Kommentare.

Noch keine Kommentare.