To Panic or Not to Panic

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu To Panic or Not to Panic. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab hängt die Entscheidung, ob panic! aufgerufen werden soll oder ein Result zurückgegeben werden soll, von der Wiederherstellbarkeit der Fehlersituation und den Optionen, die dem aufrufenden Code zur Verfügung stehen.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/ErrorHandlingandDebuggingGroup -.-> rust/panic_usage("panic! Usage") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100411{{"To Panic or Not to Panic"}} rust/integer_types -.-> lab-100411{{"To Panic or Not to Panic"}} rust/string_type -.-> lab-100411{{"To Panic or Not to Panic"}} rust/function_syntax -.-> lab-100411{{"To Panic or Not to Panic"}} rust/expressions_statements -.-> lab-100411{{"To Panic or Not to Panic"}} rust/method_syntax -.-> lab-100411{{"To Panic or Not to Panic"}} rust/panic_usage -.-> lab-100411{{"To Panic or Not to Panic"}} rust/traits -.-> lab-100411{{"To Panic or Not to Panic"}} end

To panic or Not to panic

Wie entscheidest du dann, wann du panic! aufrufen sollst und wann du Result zurückgeben sollst? Wenn der Code in Panik gerät, gibt es keine Möglichkeit, sich zu erholen. Du könntest panic! für jede Fehlersituation aufrufen, ob es eine Möglichkeit der Wiederherstellung gibt oder nicht, aber dann trägst du die Entscheidung, dass eine Situation nicht wiederherstellbar ist, für den aufrufenden Code. Wenn du wählst, ein Result-Wert zurückzugeben, gibst du dem aufrufenden Code Optionen. Der aufrufende Code könnte wählen, auf eine Weise zu versuchen, sich zu erholen, die für seine Situation geeignet ist, oder er könnte entscheiden, dass ein Err-Wert in diesem Fall nicht wiederherstellbar ist, also kann er panic! aufrufen und deine wiederherstellbare Fehlermeldung in eine nicht wiederherstellbare verwandeln. Daher ist das Zurückgeben von Result eine gute Standardauswahl, wenn du eine Funktion definierst, die fehlschlagen kann.

In Situationen wie Beispielen, Prototyp-Code und Tests ist es passender, Code zu schreiben, der in Panik gerät, anstatt ein Result zurückzugeben. Lass uns untersuchen, warum, und diskutieren dann Situationen, in denen der Compiler nicht erkennen kann, dass ein Fehler unmöglich ist, aber du als Mensch es kannst. Der Abschnitt wird mit einigen allgemeinen Richtlinien abschließen, wie du entscheiden sollst, ob du in Bibliothekscode in Panik gerätst.

Beispiele, Prototyp-Code und Tests

Wenn du ein Beispiel schreibst, um ein bestimmtes Konzept zu veranschaulichen, kann das Einbeziehen von robustem Fehlerbehandlungs-Code das Beispiel unklarer machen. In Beispielen ist es verständlich, dass ein Aufruf einer Methode wie unwrap, die in Panik geraten kann, als Platzhalter für die Art und Weise gedacht ist, wie deine Anwendung Fehler behandeln soll, was je nach dem, was der Rest deines Codes macht, variieren kann.

Ähnlich sind die Methoden unwrap und expect sehr praktisch beim Prototyping, bevor du bereit bist, zu entscheiden, wie du Fehler behandeln willst. Sie hinterlassen klare Markierungen in deinem Code, wann du bereit bist, dein Programm robuster zu gestalten.

Wenn ein Methodenaufruf in einem Test fehlschlägt, willst du, dass der gesamte Test fehlschlägt, auch wenn diese Methode nicht die zu testende Funktionalität ist. Da panic! die Art und Weise ist, wie ein Test als fehlgeschlagen markiert wird, sollte genau das passieren, wenn du unwrap oder expect aufrufst.

Fälle, in denen du mehr Informationen hast als der Compiler

Es wäre ebenfalls angemessen, unwrap oder expect aufzurufen, wenn du andere Logik hast, die gewährleistet, dass der Result-Wert einen Ok-Wert haben wird, aber die Logik ist etwas, das der Compiler nicht versteht. Du hast immer noch einen Result-Wert, den du behandeln musst: Die von dir aufgerufene Operation hat im Allgemeinen immer noch die Möglichkeit, fehlzuschlagen, auch wenn dies in deiner speziellen Situation logisch unmöglich ist. Wenn du durch manuelles Überprüfen des Codes sicher sein kannst, dass du niemals einen Err-Varianten haben wirst, ist es völlig akzeptabel, unwrap aufzurufen, und es ist sogar besser, den Grund in der expect-Nachricht zu dokumentieren, warum du glaubst, dass du niemals einen Err-Varianten haben wirst. Hier ist ein Beispiel:

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
 .parse()
 .expect("Hardcoded IP address should be valid");

Wir erstellen eine IpAddr-Instanz, indem wir einen hardcodierten String parsen. Wir können sehen, dass 127.0.0.1 eine gültige IP-Adresse ist, daher ist es hier akzeptabel, expect zu verwenden. Ein hardcodierter, gültiger String ändert jedoch den Rückgabetyp der parse-Methode nicht: Wir erhalten immer noch einen Result-Wert, und der Compiler wird uns weiterhin dazu zwingen, das Result so zu behandeln, als wäre der Err-Varianten eine Möglichkeit, weil der Compiler nicht intelligent genug ist, zu erkennen, dass dieser String immer eine gültige IP-Adresse ist. Wenn die IP-Adresszeichenfolge von einem Benutzer stammt, anstatt in das Programm hardcodiert zu sein, und daher tatsächlich die Möglichkeit eines Fehlschlags hat, würden wir auf jeden Fall den Result auf eine robusterere Weise behandeln wollen. Die Erwähnung der Annahme, dass diese IP-Adresse hardcodiert ist, wird uns dazu veranlassen, expect in besseren Fehlerbehandlungs-Code zu ändern, wenn wir in Zukunft die IP-Adresse von einer anderen Quelle beziehen müssen.

Richtlinien für die Fehlerbehandlung

Es ist ratsam, dass dein Code in Panik gerät, wenn es möglich ist, dass dein Code in einem schlechten Zustand endet. Im Rahmen dieses Kontexts ist ein schlechter Zustand gegeben, wenn eine Annahme, Garantie, Vereinbarung oder Invarianz verletzt wurde, wie wenn ungültige Werte, widersprüchliche Werte oder fehlende Werte an deinen Code übergeben werden - plus eine oder mehrere der folgenden:

  • Der schlechte Zustand ist etwas, das unerwartet ist, im Gegensatz zu etwas, das gelegentlich wahrscheinlich passiert, wie wenn ein Benutzer Daten im falschen Format eingibt.
  • Dein Code nach diesem Punkt muss darauf vertrauen, nicht in diesem schlechten Zustand zu sein, anstatt bei jedem Schritt nach dem Problem zu prüfen.
  • Es gibt keine gute Möglichkeit, diese Information in den Typen, die du verwendest, zu kodieren. Wir werden anhand eines Beispiels erläutern, was wir damit meinen, in "Encoding States and Behavior as Types".

Wenn jemand deinen Code aufruft und ungültige Werte übergibt, ist es am besten, einen Fehler zurückzugeben, wenn du kannst, damit der Benutzer der Bibliothek entscheiden kann, was er in diesem Fall tun möchte. In Fällen jedoch, in denen das Fortfahren unsicher oder schädlich sein könnte, ist die beste Option möglicherweise, panic! aufzurufen und die Person, die deine Bibliothek verwendet, über das Bug in ihrem Code zu informieren, damit sie es während der Entwicklung beheben können. Ähnlich ist panic! oft angemessen, wenn du auf externen Code zugreifst, der außerhalb deiner Kontrolle ist und einen ungültigen Zustand zurückgibt, den du nicht beheben kannst.

Wenn jedoch ein Fehler erwartet wird, ist es passender, ein Result zurückzugeben, als panic! aufzurufen. Beispiele sind ein Parser, der fehlerhafte Daten erhält, oder ein HTTP-Anfrage, die einen Status zurückgibt, der darauf hinweist, dass du eine Rate-Limit erreicht hast. In diesen Fällen gibt das Zurückgeben eines Result an, dass ein Fehler eine erwartete Möglichkeit ist, die der aufrufende Code entscheiden muss, wie er damit umgehen soll.

Wenn dein Code eine Operation ausführt, die einen Benutzer bei Verwendung ungültiger Werte gefährden könnte, sollte dein Code zunächst überprüfen, ob die Werte gültig sind, und in Panik geraten, wenn die Werte ungültig sind. Dies ist hauptsächlich aus Sicherheitsgründen: Das Versuchen, auf ungültige Daten zu operieren, kann deinen Code für Schwachstellen anfällig machen. Dies ist der Hauptgrund, warum die Standardbibliothek panic! aufrufen wird, wenn du versuchst, auf einen Speicherbereich außerhalb der Grenzen zuzugreifen: Das Versuchen, auf Speicher zuzugreifen, der nicht zum aktuellen Datenstruktur gehört, ist ein häufiges Sicherheitsproblem. Funktionen haben oft Vereinbarungen: Ihr Verhalten wird nur gewährleistet, wenn die Eingaben bestimmte Anforderungen erfüllen. Wenn die Vereinbarung verletzt wird, ist es sinnvoll, in Panik zu geraten, da eine Verletzung der Vereinbarung immer einen Fehler auf der aufrufenden Seite anzeigt, und es ist keine Art von Fehler, für die du möchten, dass der aufrufende Code explizit handhaben muss. Tatsächlich gibt es keine vernünftige Möglichkeit, dass der aufrufende Code sich erholt; die aufrufenden Programmierer müssen den Code beheben. Die Vereinbarungen für eine Funktion, insbesondere wenn eine Verletzung dazu führt, dass in Panik geraten wird, sollten in der API-Dokumentation für die Funktion erklärt werden.

Allerdings wäre es umständlich und lästig, in allen deinen Funktionen viele Fehlerprüfungen durchzuführen. Glücklicherweise kannst du das Typsystem von Rust (und somit die vom Compiler durchgeführte Typüberprüfung) verwenden, um viele der Prüfungen für dich durchzuführen. Wenn deine Funktion einen bestimmten Typ als Parameter hat, kannst du mit der Logik deines Codes fortfahren, indem du weißt, dass der Compiler bereits sichergestellt hat, dass du einen gültigen Wert hast. Beispielsweise, wenn du einen Typ hast, anstatt eine Option, erwartet dein Programm, dass es etwas gibt, anstatt nichts. Dein Code muss dann nicht zwei Fälle für die Some und None Varianten behandeln: Es wird nur einen Fall für das definitiv vorhandene einen Wert haben. Code, der versucht, nichts an deine Funktion zu übergeben, wird nicht einmal kompilieren, sodass deine Funktion nicht prüfen muss, auf diesen Fall zur Laufzeit. Ein weiteres Beispiel ist das Verwenden eines vorzeichenlosen ganzzahligen Typs wie u32, das gewährleistet, dass der Parameter niemals negativ ist.

Erstellen benutzerdefinierter Typen für die Validierung

Lassen Sie uns die Idee, das Typsystem von Rust zu verwenden, um sicherzustellen, dass wir einen gültigen Wert haben, einen Schritt weiter verfolgen und uns ansehen, wie wir einen benutzerdefinierten Typ für die Validierung erstellen. Denken Sie sich das Raten-Spiel aus Kapitel 2 zurück, in dem unser Code den Benutzer aufforderte, eine Zahl zwischen 1 und 100 zu erraten. Wir haben nie überprüft, ob der vom Benutzer geratene Wert zwischen diesen Zahlen lag, bevor wir ihn mit unserer Geheimzahl verglichen; wir haben nur überprüft, dass die Vermutung positiv war. In diesem Fall waren die Folgen nicht sehr schlimm: Unsere Ausgabe von "Zu hoch" oder "Zu niedrig" wäre immer noch korrekt. Aber es wäre eine nützliche Verbesserung, den Benutzer zu leiten, auf gültige Vermutungen zu kommen, und unterschiedliches Verhalten zu haben, wenn der Benutzer eine Zahl ausserhalb des Bereichs errät, im Gegensatz zu dem, wenn der Benutzer beispielsweise Buchstaben eingibt.

Eine Möglichkeit, dies zu tun, wäre, die Vermutung als i32 statt nur als u32 zu parsen, um potenziell negative Zahlen zuzulassen, und dann eine Prüfung hinzuzufügen, ob die Zahl im Bereich liegt, wie folgt:

Dateiname: src/main.rs

loop {
    --snip--

    let guess: i32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };

    if guess < 1 || guess > 100 {
        println!("The secret number will be between 1 and 100.");
        continue;
    }

    match guess.cmp(&secret_number) {
        --snip--
}

Der if-Ausdruck prüft, ob unser Wert ausserhalb des Bereichs liegt, informiert den Benutzer über das Problem und ruft continue auf, um die nächste Iteration der Schleife zu starten und nach einer weiteren Vermutung zu fragen. Nach dem if-Ausdruck können wir mit den Vergleichen zwischen guess und der Geheimzahl fortfahren, indem wir wissen, dass guess zwischen 1 und 100 liegt.

Dies ist jedoch keine ideale Lösung: Wenn es absolut entscheidend wäre, dass das Programm nur auf Werten zwischen 1 und 100 operiert, und es viele Funktionen mit dieser Anforderung hätte, wäre es lästig, eine solche Prüfung in jeder Funktion durchzuführen (und könnte die Leistung beeinträchtigen).

Stattdessen können wir einen neuen Typ erstellen und die Validierungen in einer Funktion ablegen, um eine Instanz des Typs zu erstellen, anstatt die Validierungen überall zu wiederholen. Auf diese Weise ist es sicher für Funktionen, den neuen Typ in ihren Signaturen zu verwenden und die Werte, die sie erhalten, vertrauensvoll zu verwenden. Listing 9-13 zeigt eine Möglichkeit, einen Guess-Typ zu definieren, der nur eine Instanz von Guess erstellen wird, wenn die new-Funktion einen Wert zwischen 1 und 100 erhält.

Dateiname: src/lib.rs

1 pub struct Guess {
    value: i32,
}

impl Guess {
  2 pub fn new(value: i32) -> Guess {
      3 if value < 1 || value > 100 {
          4 panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

      5 Guess { value }
    }

  6 pub fn value(&self) -> i32 {
        self.value
    }
}

Listing 9-13: Ein Guess-Typ, der nur mit Werten zwischen 1 und 100 fortfahren wird

Zuerst definieren wir eine Struktur namens Guess, die ein Feld namens value hat, das eine i32 enthält [1]. Hier wird die Zahl gespeichert.

Dann implementieren wir eine assoziierte Funktion namens new auf Guess, die Instanzen von Guess-Werten erstellt [2]. Die new-Funktion ist definiert, einen Parameter namens value vom Typ i32 zu haben und einen Guess zurückzugeben. Der Code im Körper der new-Funktion testet value, um sicherzustellen, dass es zwischen 1 und 100 liegt [3]. Wenn value diese Prüfung nicht besteht, rufen wir panic! auf [4], was den Programmierer, der den aufrufenden Code schreibt, darüber informieren wird, dass er einen Bug hat, den er beheben muss, da das Erstellen eines Guess mit einem value ausserhalb dieses Bereichs den Vertrag verletzen würde, auf den Guess::new zurückgreift. Die Bedingungen, unter denen Guess::new in Panik geraten könnte, sollten in ihrer öffentlich zugänglichen API-Dokumentation diskutiert werden; wir werden die Dokumentationskonventionen behandeln, die die Möglichkeit eines panic! in der API-Dokumentation anzeigen, die Sie im Kapitel 14 erstellen. Wenn value die Prüfung besteht, erstellen wir einen neuen Guess mit seinem value-Feld auf den value-Parameter gesetzt und geben den Guess zurück [5].

Als nächstes implementieren wir eine Methode namens value, die self borrrowt, keine weiteren Parameter hat und eine i32 zurückgibt [6]. Diese Art von Methode wird manchmal als Getter bezeichnet, weil ihr Zweck darin besteht, einige Daten aus ihren Feldern zu erhalten und zurückzugeben. Diese öffentliche Methode ist notwendig, weil das value-Feld der Guess-Struktur privat ist. Es ist wichtig, dass das value-Feld privat ist, damit der Code, der die Guess-Struktur verwendet, nicht direkt value setzen darf: Der Code außerhalb des Moduls muss die Guess::new-Funktion verwenden, um eine Instanz von Guess zu erstellen, wodurch sichergestellt wird, dass es keine Möglichkeit gibt, dass ein Guess ein value hat, das nicht von den Bedingungen in der Guess::new-Funktion überprüft wurde.

Eine Funktion, die einen Parameter hat oder nur Zahlen zwischen 1 und 100 zurückgibt, könnte dann in ihrer Signatur erklären, dass sie einen Guess nimmt oder zurückgibt, anstatt eine i32, und müsste keine weiteren Prüfungen in ihrem Körper vornehmen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "To Panic or Not to Panic" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.