Fehlerbehandlung mit Result

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 Recoverable Errors With Result. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab lernen wir, wie wir mit der Result-Enumeration in Rust wiederherstellbare Fehler behandeln, was uns ermöglicht, Fehler zu interpretieren und auf sie zu reagieren, ohne das Programm zu beenden.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") 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/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/mutable_variables -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/string_type -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/function_syntax -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/expressions_statements -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/method_syntax -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/panic_usage -.-> lab-100410{{"Fehlerbehandlung mit Result"}} rust/operator_overloading -.-> lab-100410{{"Fehlerbehandlung mit Result"}} end

Wiederherstellbare Fehler mit Result

Die meisten Fehler sind nicht so schwerwiegend, dass das Programm vollständig beendet werden muss. Manchmal scheitert eine Funktion aus einem Grund, den man leicht interpretieren und darauf reagieren kann. Beispielsweise, wenn Sie versuchen, eine Datei zu öffnen und diese Operation scheitert, weil die Datei nicht existiert, möchten Sie möglicherweise die Datei erstellen, anstatt den Prozess zu beenden.

Denken Sie sich aus "Handling Potential Failure with Result" zurück, dass die Result-Enumeration wie folgt definiert ist, mit zwei Varianten, Ok und Err:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Die T und E sind generische Typparameter: Wir werden Generics im Kapitel 10 genauer besprechen. Was Sie im Moment wissen müssen, ist, dass T den Typ des Werts darstellt, der im erfolgreichen Fall innerhalb der Ok-Variante zurückgegeben wird, und E den Typ des Fehlers darstellt, der im fehlerhaften Fall innerhalb der Err-Variante zurückgegeben wird. Da Result diese generischen Typparameter hat, können wir den Result-Typ und die auf ihm definierten Funktionen in vielen verschiedenen Situationen verwenden, in denen der Erfolgswert und der Fehlerwert, die wir zurückgeben möchten, unterschiedlich sein können.

Lassen Sie uns eine Funktion aufrufen, die einen Result-Wert zurückgibt, weil die Funktion fehlschlagen kann. In Listing 9-3 versuchen wir, eine Datei zu öffnen.

Dateiname: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

Listing 9-3: Öffnen einer Datei

Der Rückgabetyp von File::open ist ein Result<T, E>. Der generische Parameter T wurde von der Implementierung von File::open mit dem Typ des Erfolgswerts, std::fs::File, das ein Dateihandle ist, ausgefüllt. Der Typ von E, der im Fehlerwert verwendet wird, ist std::io::Error. Dieser Rückgabetyp bedeutet, dass der Aufruf von File::open erfolgreich sein kann und einen Dateihandle zurückgeben kann, von dem wir lesen oder schreiben können. Der Funktionsaufruf kann auch fehlschlagen: Beispielsweise kann die Datei nicht existieren oder wir haben möglicherweise keine Berechtigung, auf die Datei zuzugreifen. Die File::open-Funktion muss einen Weg haben, uns mitzuteilen, ob es erfolgreich war oder nicht, und uns gleichzeitig entweder den Dateihandle oder die Fehlerinformationen geben. Genau diese Information vermittelt die Result-Enumeration.

Im Fall, dass File::open erfolgreich ist, wird der Wert in der Variable greeting_file_result eine Instanz von Ok sein, die einen Dateihandle enthält. Im Fall, dass es fehlschlägt, wird der Wert in greeting_file_result eine Instanz von Err sein, die weitere Informationen über die Art des aufgetretenen Fehlers enthält.

Wir müssen dem Code in Listing 9-3 hinzufügen, um unterschiedliche Aktionen zu unternehmen, je nachdem, welchen Wert File::open zurückgibt. Listing 9-4 zeigt eine Möglichkeit, das Result mit einem grundlegenden Werkzeug, dem match-Ausdruck, zu behandeln, den wir im Kapitel 6 besprochen haben.

Dateiname: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error);
        }
    };
}

Listing 9-4: Verwenden eines match-Ausdrucks, um die Result-Varianten zu behandeln, die möglicherweise zurückgegeben werden

Beachten Sie, dass wie bei der Option-Enumeration die Result-Enumeration und ihre Varianten durch den Präambel in den Gültigkeitsbereich gebracht wurden, so dass wir nicht Result:: vor den Ok- und Err-Varianten im match-Arm angeben müssen.

Wenn das Ergebnis Ok ist, wird dieser Code den inneren file-Wert aus der Ok-Variante zurückgeben, und wir weisen dann diesen Dateihandle-Wert der Variablen greeting_file zu. Nach dem match können wir den Dateihandle zum Lesen oder Schreiben verwenden.

Der andere Arm des match behandelt den Fall, in dem wir einen Err-Wert von File::open erhalten. In diesem Beispiel haben wir uns entschieden, die panic!-Makro aufzurufen. Wenn es in unserem aktuellen Verzeichnis keine Datei namens hello.txt gibt und wir diesen Code ausführen, werden wir die folgende Ausgabe des panic!-Makros sehen:

thread 'main' panicked at 'Problem opening the file: Os { code:
 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:8:23

Wie üblich gibt uns diese Ausgabe genau an, was schiefgelaufen ist.

Matching auf verschiedene Fehler

Der Code in Listing 9-4 wird panic! auslösen, unabhängig davon, warum File::open fehlschlägt. Wir möchten jedoch unterschiedliche Aktionen für verschiedene Fehlgründe unternehmen. Wenn File::open fehlschlägt, weil die Datei nicht existiert, möchten wir die Datei erstellen und den Handle für die neue Datei zurückgeben. Wenn File::open aus irgendeinem anderen Grund fehlschlägt - beispielsweise, weil wir keine Berechtigung hatten, die Datei zu öffnen - möchten wir, dass der Code auf die gleiche Weise panic! auslöst, wie er es in Listing 9-4 tat.为此,我们添加了一个内部的 match 表达式,如清单 9-5 所示。

Dateiname: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => {
                match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!(
                        "Problem creating the file: {:?}",
                        e
                    ),
                }
            }
            other_error => {
                panic!(
                    "Problem opening the file: {:?}",
                    other_error
                );
            }
        },
    };
}

Listing 9-5: Behandeln unterschiedlicher Arten von Fehlern auf verschiedene Weise

Der Typ des Werts, den File::open innerhalb der Err-Variante zurückgibt, ist io::Error, eine Struktur, die von der Standardbibliothek bereitgestellt wird. Diese Struktur hat eine Methode kind, die wir aufrufen können, um einen io::ErrorKind-Wert zu erhalten. Die Enumeration io::ErrorKind wird von der Standardbibliothek bereitgestellt und hat Varianten, die die verschiedenen Arten von Fehlern repräsentieren, die aus einem io-Operation resultieren können. Die Variante, die wir verwenden möchten, ist ErrorKind::NotFound, die angibt, dass die Datei, die wir versuchen, zu öffnen, noch nicht existiert. Wir matchen daher auf greeting_file_result, aber wir haben auch eine innere Matches auf error.kind().

Die Bedingung, die wir in der inneren Matches überprüfen möchten, ist, ob der Wert, der von error.kind() zurückgegeben wird, die NotFound-Variante der ErrorKind-Enumeration ist. Wenn dies der Fall ist, versuchen wir, die Datei mit File::create zu erstellen. Da File::create ebenfalls fehlschlagen kann, benötigen wir einen zweiten Arm im inneren match-Ausdruck. Wenn die Datei nicht erstellt werden kann, wird eine andere Fehlermeldung ausgegeben. Der zweite Arm der äußeren Matches bleibt gleich, sodass das Programm bei jedem Fehler außer dem Fehler aufgrund der fehlenden Datei panic! auslöst.

Alternativen zu match mit Result<T, E>

Das sind wirklich viele match! Der match-Ausdruck ist sehr nützlich, aber auch ziemlich primitiv. Im Kapitel 13 lernen Sie über Closures, die mit vielen der auf Result<T, E> definierten Methoden verwendet werden. Diese Methoden können kürzer sein als die Verwendung von match, wenn Sie Result<T, E>-Werte in Ihrem Code behandeln.

Zum Beispiel ist hier eine andere Möglichkeit, die gleiche Logik wie in Listing 9-5 zu schreiben, diesmal mit Closures und der unwrap_or_else-Methode:

// src/main.rs
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Obwohl dieser Code das gleiche Verhalten wie Listing 9-5 hat, enthält er keine match-Ausdrücke und ist lesbarer. Kommen Sie nach dem Lesen von Kapitel 13 zurück zu diesem Beispiel und suchen Sie die unwrap_or_else-Methode in der Standardbibliotheksdokumentation auf. Viele weitere dieser Methoden können riesige geschachtelte match-Ausdrücke aufräumen, wenn Sie mit Fehlern umgehen.

Schnellzugänge für Panik bei Fehler: unwrap und expect

Das Verwenden von match funktioniert gut genug, kann aber etwas umständlich sein und kommuniziert die Absicht nicht immer gut. Der Result<T, E>-Typ hat viele Hilfsmethoden definiert, um verschiedene, spezifischere Aufgaben durchzuführen. Die unwrap-Methode ist eine Kurzschlussmethode, die genauso implementiert ist wie der match-Ausdruck, den wir in Listing 9-4 geschrieben haben. Wenn der Result-Wert die Ok-Variante ist, gibt unwrap den Wert innerhalb von Ok zurück. Wenn das Result die Err-Variante ist, ruft unwrap das panic!-Makro für uns auf. Hier ist ein Beispiel für das Funktionieren von unwrap:

Dateiname: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Wenn wir diesen Code ausführen, ohne die Datei hello.txt zu haben, werden wir eine Fehlermeldung aus dem panic!-Aufruf sehen, den die unwrap-Methode ausführt:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49

Ähnlich gibt uns die expect-Methode die Möglichkeit, auch die panic!-Fehlermeldung zu wählen. Das Verwenden von expect anstelle von unwrap und das Angeben guter Fehlermeldungen kann Ihre Absicht vermitteln und das Auffinden der Quelle einer Panik einfacher machen. Die Syntax von expect sieht so aus:

Dateiname: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
     .expect("hello.txt sollte in diesem Projekt enthalten sein");
}

Wir verwenden expect genauso wie unwrap: um den Dateihandle zurückzugeben oder das panic!-Makro aufzurufen. Die Fehlermeldung, die expect bei seinem Aufruf von panic! verwendet, wird der Parameter sein, den wir an expect übergeben, und nicht die Standard-panic!-Meldung, die unwrap verwendet. So sieht es aus:

thread 'main' panicked at 'hello.txt sollte in diesem Projekt enthalten sein: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10

In produktionsreifer Code wählen die meisten Rustaceans expect statt unwrap und geben mehr Kontext darüber, warum die Operation immer erfolgreich sein sollte. Auf diese Weise haben Sie, wenn Ihre Annahmen jemals als falsch erwiesen werden, mehr Informationen, die Sie bei der Fehlersuche verwenden können.

Weiterreichen von Fehlern

Wenn die Implementierung einer Funktion etwas aufruft, das fehlschlagen kann, können Sie statt der Fehlerbehandlung innerhalb der Funktion selbst den Fehler an den aufrufenden Code zurückgeben, sodass dieser entscheiden kann, was zu tun ist. Dies wird als weiterreichen des Fehlers bezeichnet und gibt dem aufrufenden Code mehr Kontrolle, da dort möglicherweise mehr Informationen oder Logik vorhanden ist, die bestimmt, wie der Fehler behandelt werden soll, als Sie in Ihrem Codekontext zur Verfügung haben.

Beispielsweise zeigt Listing 9-6 eine Funktion, die einen Benutzernamen aus einer Datei liest. Wenn die Datei nicht existiert oder nicht gelesen werden kann, gibt diese Funktion diese Fehler an den Code zurück, der die Funktion aufgerufen hat.

Dateiname: src/main.rs

use std::fs::File;
use std::io::{self, Read};

1 fn read_username_from_file() -> Result<String, io::Error> {
  2 let username_file_result = File::open("hello.txt");

  3 let mut username_file = match username_file_result {
      4 Ok(file) => file,
      5 Err(e) => return Err(e),
    };

  6 let mut username = String::new();

  7 match username_file.read_to_string(&mut username) {
      8 Ok(_) => Ok(username),
      9 Err(e) => Err(e),
    }
}

Listing 9-6: Eine Funktion, die Fehler an den aufrufenden Code zurückgibt, indem sie match verwendet

Diese Funktion kann auf eine viel kürzere Weise geschrieben werden, aber wir beginnen zunächst, sie vielmehr manuell zu schreiben, um die Fehlerbehandlung zu erkunden; am Ende werden wir die kürzere Weise zeigen. Schauen wir uns zuerst den Rückgabetyp der Funktion an: Result<String, io::Error> [1]. Dies bedeutet, dass die Funktion einen Wert vom Typ Result<T, E> zurückgibt, wobei der generische Parameter T mit dem konkreten Typ String und der generische Typ E mit dem konkreten Typ io::Error ausgefüllt wurde.

Wenn diese Funktion erfolgreich verläuft, ohne Probleme, erhält der Code, der diese Funktion aufruft, einen Ok-Wert, der eine String enthält - den username, den diese Funktion aus der Datei gelesen hat [8]. Wenn diese Funktion Probleme遇到, erhält der aufrufende Code einen Err-Wert, der eine Instanz von io::Error enthält, die weitere Informationen über die Probleme enthält. Wir haben io::Error als Rückgabetyp dieser Funktion gewählt, weil dies恰巧 der Typ des Fehlerwerts ist, der von beiden Operationen zurückgegeben wird, die wir in der Funktionskörper dieser Funktion aufrufen, die fehlschlagen können: die File::open-Funktion [2] und die read_to_string-Methode [7].

Der Funktionskörper beginnt mit dem Aufruf der File::open-Funktion [2]. Dann behandeln wir den Result-Wert mit einem match, ähnlich wie dem match in Listing 9-4. Wenn File::open erfolgreich ist, wird der Dateihandle in der Mustervariable file [4] zum Wert in der mutablen Variable username_file [3] und die Funktion setzt fort. Im Err-Fall rufen wir statt panic! das return-Schlüsselwort auf, um ganz frühzeitig aus der Funktion auszusteigen und den Fehlerwert von File::open, jetzt in der Mustervariable e, als Fehlerwert dieser Funktion an den aufrufenden Code zurückzugeben [5].

Wenn wir also einen Dateihandle in username_file haben, erstellt die Funktion dann eine neue String in der Variable username [6] und ruft die read_to_string-Methode auf dem Dateihandle in username_file auf, um den Inhalt der Datei in username zu lesen [7]. Die read_to_string-Methode gibt ebenfalls ein Result zurück, da sie fehlschlagen kann, auch wenn File::open erfolgreich war. Wir brauchen daher ein weiteres match, um dieses Result zu behandeln: Wenn read_to_string erfolgreich ist, hat unsere Funktion erfolgreich abgeschlossen, und wir geben den Benutzernamen aus der Datei, der jetzt in username ist, in einem Ok zurück. Wenn read_to_string fehlschlägt, geben wir den Fehlerwert auf die gleiche Weise zurück, wie wir den Fehlerwert im match zurückgegeben haben, das den Rückgabewert von File::open behandelt hat. Wir müssen jedoch nicht explizit return sagen, da dies der letzte Ausdruck in der Funktion ist [9].

Der Code, der diesen Code aufruft, wird dann das Erhalten eines Ok-Werts, der einen Benutzernamen enthält, oder eines Err-Werts, der eine io::Error enthält, behandeln. Es liegt an dem aufrufenden Code, zu entscheiden, was mit diesen Werten zu tun ist. Wenn der aufrufende Code einen Err-Wert erhält, kann er panic! aufrufen und das Programm abstürzen, einen Standardbenutzernamen verwenden oder den Benutzernamen an einem anderen Ort als aus einer Datei abrufen, beispielsweise. Wir haben nicht genug Informationen darüber, was der aufrufende Code tatsächlich tun möchte, daher leiten wir alle Erfolg- oder Fehlerinformationen nach oben weiter, damit er sie entsprechend behandeln kann.

Dieses Muster des Weiterreichsens von Fehlern ist in Rust so üblich, dass Rust den Fragezeichen-Operator ? bereitstellt, um dies zu erleichtern.

Ein Schnellzugang zum Weiterreichen von Fehlern: Der?-Operator

Listing 9-7 zeigt eine Implementierung von read_username_from_file, die die gleiche Funktionalität wie in Listing 9-6 hat, aber diese Implementierung verwendet den ?-Operator.

Dateiname: src/main.rs

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

Listing 9-7: Eine Funktion, die Fehler an den aufrufenden Code zurückgibt, indem sie den ?-Operator verwendet

Der ?, der nach einem Result-Wert platziert ist, ist so definiert, dass er fast auf die gleiche Weise funktioniert wie die match-Ausdrücke, die wir in Listing 9-6 definiert haben, um die Result-Werte zu behandeln. Wenn der Wert des Result ein Ok ist, wird der Wert innerhalb von Ok aus diesem Ausdruck zurückgegeben und das Programm wird fortgesetzt. Wenn der Wert ein Err ist, wird das Err aus der gesamten Funktion zurückgegeben, als hätten wir das return-Schlüsselwort verwendet, sodass der Fehlerwert an den aufrufenden Code weitergeleitet wird.

Es gibt einen Unterschied zwischen dem, was der match-Ausdruck aus Listing 9-6 tut, und dem, was der ?-Operator tut: Fehlerwerte, auf denen der ?-Operator aufgerufen wird, gehen durch die from-Funktion, die in dem From-Trait in der Standardbibliothek definiert ist, die verwendet wird, um Werte von einem Typ in einen anderen zu konvertieren. Wenn der ?-Operator die from-Funktion aufruft, wird der empfangene Fehlertyp in den Fehlertyp konvertiert, der in dem Rückgabetyp der aktuellen Funktion definiert ist. Dies ist nützlich, wenn eine Funktion einen Fehlertyp zurückgibt, um alle Möglichkeiten zu repräsentieren, wie eine Funktion fehlschlagen kann, auch wenn Teile aus vielen verschiedenen Gründen fehlschlagen können.

Zum Beispiel könnten wir die read_username_from_file-Funktion in Listing 9-7 ändern, um einen benutzerdefinierten Fehlertyp namens OurError zurückzugeben, den wir definieren. Wenn wir auch impl From<io::Error> for OurError definieren, um eine Instanz von OurError aus einem io::Error zu konstruieren, dann werden die ?-Operator-Aufrufe im Körper von read_username_from_file die from-Funktion aufrufen und die Fehlertypen konvertieren, ohne dass wir weitere Code in die Funktion hinzufügen müssen.

Im Kontext von Listing 9-7 wird der ? am Ende des File::open-Aufrufs den Wert innerhalb eines Ok an die Variable username_file zurückgeben. Wenn ein Fehler auftritt, wird der ?-Operator frühzeitig aus der gesamten Funktion heraus zurückgeben und jedem Err-Wert den aufrufenden Code geben. Dasselbe gilt für den ? am Ende des read_to_string-Aufrufs.

Der ?-Operator eliminiert viel Boilerplate-Code und vereinfacht die Implementierung dieser Funktion. Wir könnten diesen Code sogar noch weiter verkürzen, indem wir Methodenaufrufe direkt nach dem ? verketten, wie in Listing 9-8 gezeigt.

Dateiname: src/main.rs

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

Listing 9-8: Verkettung von Methodenaufrufen nach dem ?-Operator

Wir haben die Erstellung der neuen String in username ans Anfang der Funktion verschoben; dieser Teil hat sich nicht geändert. Anstatt eine Variable username_file zu erstellen, haben wir den Aufruf von read_to_string direkt an das Ergebnis von File::open("hello.txt")? angehängt. Wir haben immer noch einen ? am Ende des read_to_string-Aufrufs, und wir geben immer noch einen Ok-Wert, der username enthält, zurück, wenn sowohl File::open als auch read_to_string erfolgreich sind, anstatt Fehler zurückzugeben. Die Funktionalität ist wiederum die gleiche wie in Listing 9-6 und Listing 9-7; dies ist nur eine andere, ergonomischere Weise, ihn zu schreiben.

Listing 9-9 zeigt eine Möglichkeit, dies noch kürzer zu machen, indem fs::read_to_string verwendet wird.

Dateiname: src/main.rs

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

Listing 9-9: Verwenden von fs::read_to_string anstatt die Datei zu öffnen und dann zu lesen

Das Lesen einer Datei in einen String ist eine ziemlich häufige Operation, daher bietet die Standardbibliothek die bequeme fs::read_to_string-Funktion, die die Datei öffnet, eine neue String erstellt, den Inhalt der Datei liest, den Inhalt in diese String setzt und ihn zurückgibt. Natürlich gibt uns das Verwenden von fs::read_to_string keine Möglichkeit, alle Fehlerbehandlungen zu erklären, daher haben wir es zuerst auf die längere Weise gemacht.

Wo der?-Operator verwendet werden kann

Der ?-Operator kann nur in Funktionen verwendet werden, deren Rückgabetyp mit dem Wert kompatibel ist, auf dem der ? verwendet wird. Dies liegt daran, dass der ?-Operator definiert ist, um einen frühen Rückgabewert eines Werts aus der Funktion durchzuführen, auf die gleiche Weise wie der match-Ausdruck, den wir in Listing 9-6 definiert haben. In Listing 9-6 hat der match einen Result-Wert verwendet, und der frühe Rückgabebranch hat einen Err(e)-Wert zurückgegeben. Der Rückgabetyp der Funktion muss ein Result sein, damit er mit diesem return kompatibel ist.

In Listing 9-10 schauen wir uns den Fehler an, den wir erhalten, wenn wir den ?-Operator in einer main-Funktion mit einem Rückgabetyp verwenden, der nicht kompatibel mit dem Typ des Werts ist, auf dem wir den ? verwenden.

Dateiname: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

Listing 9-10: Versuch, den ? in der main-Funktion zu verwenden, die () zurückgibt, wird nicht kompilieren.

Dieser Code öffnet eine Datei, was fehlschlagen kann. Der ?-Operator folgt dem Result-Wert, der von File::open zurückgegeben wird, aber diese main-Funktion hat den Rückgabetyp (), nicht Result. Wenn wir diesen Code kompilieren, erhalten wir die folgende Fehlermeldung:

error[E0277]: the `?` operator can only be used in a function that returns
`Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | / fn main() {
4 | |     let greeting_file = File::open("hello.txt")?;
  | |                                                ^ cannot use the `?`
operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not
implemented for `()`

Diese Fehlermeldung zeigt an, dass wir nur erlaubt sind, den ?-Operator in einer Funktion zu verwenden, die Result, Option oder einen anderen Typ zurückgibt, der FromResidual implementiert.

Um den Fehler zu beheben, haben Sie zwei Möglichkeiten. Eine Möglichkeit ist, den Rückgabetyp Ihrer Funktion zu ändern, um ihn mit dem Wert zu kompatibilisieren, auf dem Sie den ?-Operator verwenden, solange Sie keine Einschränkungen haben, die dies verhindern. Die andere Möglichkeit ist, einen match oder eine der Result<T, E>-Methoden zu verwenden, um das Result<T, E> auf die passende Weise zu behandeln.

Die Fehlermeldung erwähnt auch, dass ? auch mit Option<T>-Werten verwendet werden kann. Wie bei der Verwendung von ? auf Result können Sie ? nur auf Option in einer Funktion verwenden, die einen Option zurückgibt. Das Verhalten des ?-Operators, wenn er auf einem Option<T> aufgerufen wird, ist ähnlich zu seinem Verhalten, wenn er auf einem Result<T, E> aufgerufen wird: Wenn der Wert None ist, wird das None zu diesem Zeitpunkt frühzeitig aus der Funktion zurückgegeben. Wenn der Wert Some ist, ist der Wert innerhalb von Some der resultierende Wert des Ausdrucks, und die Funktion setzt fort. Listing 9-11 hat ein Beispiel für eine Funktion, die das letzte Zeichen der ersten Zeile im angegebenen Text findet.

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

Listing 9-11: Verwendung des ?-Operators auf einem Option<T>-Wert

Diese Funktion gibt Option<char> zurück, weil es möglich ist, dass dort ein Zeichen ist, aber es ist auch möglich, dass es keines gibt. Dieser Code nimmt das text-String-Slice-Argument und ruft die lines-Methode darauf auf, die einen Iterator über die Zeilen im String zurückgibt. Da diese Funktion die erste Zeile untersuchen möchte, ruft sie next auf dem Iterator auf, um den ersten Wert aus dem Iterator zu erhalten. Wenn text die leere Zeichenkette ist, wird dieser Aufruf von next None zurückgeben, in diesem Fall verwenden wir ?, um zu stoppen und None aus last_char_of_first_line zurückzugeben. Wenn text nicht die leere Zeichenkette ist, wird next einen Some-Wert zurückgeben, der einen String-Slice der ersten Zeile in text enthält.

Der ? extrahiert den String-Slice, und wir können chars auf diesem String-Slice aufrufen, um einen Iterator seiner Zeichen zu erhalten. Wir interessieren uns für das letzte Zeichen in dieser ersten Zeile, daher rufen wir last auf, um das letzte Element im Iterator zurückzugeben. Dies ist eine Option, weil es möglich ist, dass die erste Zeile die leere Zeichenkette ist; beispielsweise, wenn text mit einer leeren Zeile beginnt, aber auf anderen Zeilen Zeichen hat, wie in "\nhi". Wenn es jedoch ein letztes Zeichen in der ersten Zeile gibt, wird es im Some-Variant zurückgegeben. Der ?-Operator in der Mitte gibt uns eine präzise Möglichkeit, diese Logik auszudrücken, was uns ermöglicht, die Funktion in einer Zeile zu implementieren. Wenn wir den ?-Operator nicht auf Option verwenden könnten, müssten wir diese Logik mit mehr Methodenaufrufen oder einem match-Ausdruck implementieren.

Beachten Sie, dass Sie den ?-Operator auf einem Result in einer Funktion verwenden können, die Result zurückgibt, und Sie können den ?-Operator auf einem Option in einer Funktion verwenden, die Option zurückgibt, aber Sie können nicht mischen und match. Der ?-Operator konvertiert ein Result nicht automatisch in ein Option oder umgekehrt; in diesen Fällen können Sie Methoden wie die ok-Methode auf Result oder die ok_or-Methode auf Option verwenden, um die Konvertierung explizit durchzuführen.

Bisher haben alle main-Funktionen, die wir verwendet haben, () zurückgegeben. Die main-Funktion ist speziell, weil sie der Einstiegspunkt und Ausstiegspunkt eines ausführbaren Programms ist, und es gibt Einschränkungen für ihren Rückgabetyp, damit das Programm wie erwartet funktioniert.

Zum Glück kann main auch ein Result<(), E> zurückgeben. Listing 9-12 hat den Code aus Listing 9-10, aber wir haben den Rückgabetyp von main geändert, um Result<(), Box<dyn Error>> zu sein, und am Ende einen Rückgabewert Ok(()) hinzugefügt. Dieser Code wird jetzt kompilieren.

Dateiname: src/main.rs

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

Listing 9-12: Ändern von main, um Result<(), E> zurückzugeben, ermöglicht die Verwendung des ?-Operators auf Result-Werten.

Der Typ Box<dyn Error> ist ein Trait-Objekt, über das wir in "Using Trait Objects That Allow for Values of Different Types" sprechen werden. Für jetzt können Sie Box<dyn Error> lesen als "irgendein Fehler". Das Verwenden von ? auf einem Result-Wert in einer main-Funktion mit dem Fehlertyp Box<dyn Error> ist erlaubt, weil es ermöglicht, dass jeder Err-Wert frühzeitig zurückgegeben wird. Auch wenn der Körper dieser main-Funktion nur Fehler vom Typ std::io::Error zurückgeben wird, indem man Box<dyn Error> angibt, bleibt diese Signatur auch dann korrekt, wenn mehr Code hinzugefügt wird, der andere Fehler zurückgibt, in den Körper von main.

Wenn eine main-Funktion ein Result<(), E> zurückgibt, wird das ausführbare Programm mit einem Wert von 0 beendet, wenn main Ok(()) zurückgibt, und wird mit einem nicht nullen Wert beendet, wenn main einen Err-Wert zurückgibt. Ausführbare Programme, die in C geschrieben sind, geben beim Beenden ganze Zahlen zurück: Programme, die erfolgreich beenden, geben die ganze Zahl 0 zurück, und Programme, die einen Fehler haben, geben eine andere ganze Zahl als 0 zurück. Rust gibt auch ganze Zahlen aus ausführbaren Programmen zurück, um mit dieser Konvention kompatibel zu sein.

Die main-Funktion kann beliebige Typen zurückgeben, die das std::process::Termination-Trait implementieren, das eine Funktion report enthält, die einen ExitCode zurückgibt. Lesen Sie die Standardbibliotheksdokumentation, um weitere Informationen zur Implementierung des Termination-Traits für Ihre eigenen Typen zu erhalten.

Jetzt, nachdem wir die Details des Aufrufs von panic! oder des Rückgebens von Result diskutiert haben, kehren wir zum Thema zurück, wie man entscheidet, welches in welchen Fällen geeignet ist.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Recoverable Errors With Result" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.