Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Błędy odzyskiwalne za pomocą Result

Większość błędów nie jest na tyle poważna, aby wymagać całkowitego zatrzymania programu. Czasami, gdy funkcja zawodzi, dzieje się tak z powodu, który można łatwo zinterpretować i na niego zareagować. Na przykład, jeśli próbujesz otworzyć plik, a ta operacja kończy się niepowodzeniem, ponieważ plik nie istnieje, możesz chcieć utworzyć plik zamiast zakończyć proces.

Przypomnij sobie z sekcji „Obsługa potencjalnych awarii za pomocą Result w Rozdziale 2, że enum Result jest zdefiniowany tak, aby miał dwa warianty, Ok i Err, w następujący sposób:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T i E to generyczne parametry typu: bardziej szczegółowo omówimy generyki w Rozdziale 10. To, co musisz teraz wiedzieć, to to, że T reprezentuje typ wartości, która zostanie zwrócona w przypadku sukcesu w wariancie Ok, a E reprezentuje typ błędu, który zostanie zwrócony w przypadku niepowodzenia w wariancie Err. Ponieważ Result ma te generyczne parametry typu, możemy używać typu Result i zdefiniowanych na nim funkcji w wielu różnych sytuacjach, gdy wartość sukcesu i wartość błędu, którą chcemy zwrócić, mogą się różnić.

Wywołajmy funkcję, która zwraca wartość Result, ponieważ funkcja może zawieść. W Listing 9-3 próbujemy otworzyć plik.

use std::fs::File;

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

Typ zwracany przez File::open to Result<T, E>. Generyczny parametr T został wypełniony przez implementację File::open typem wartości sukcesu, std::fs::File, który jest uchwytem pliku. Typ E używany w wartości błędu to std::io::Error. Ten typ zwracany oznacza, że wywołanie File::open może zakończyć się sukcesem i zwrócić uchwyt pliku, z którego możemy czytać lub do którego możemy pisać. Wywołanie funkcji może również zakończyć się niepowodzeniem: na przykład plik może nie istnieć lub możemy nie mieć uprawnień do dostępu do pliku. Funkcja File::open musi mieć sposób, aby poinformować nas, czy zakończyła się sukcesem, czy niepowodzeniem, i jednocześnie podać nam uchwyt pliku lub informacje o błędzie. Te informacje to dokładnie to, co przekazuje enum Result.

W przypadku, gdy File::open zakończy się sukcesem, wartość w zmiennej greeting_file_result będzie instancją Ok, która zawiera uchwyt pliku. W przypadku, gdy zawiedzie, wartość w greeting_file_result będzie instancją Err, która zawiera więcej informacji o rodzaju błędu, który wystąpił.

Musimy uzupełnić kod w Listing 9-3, aby podejmować różne działania w zależności od wartości zwracanej przez File::open. Listing 9-4 pokazuje jeden ze sposobów obsługi Result za pomocą podstawowego narzędzia, wyrażenia match, które omówiliśmy w Rozdziale 6.

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 z otwarciem pliku: {error:?}"),
    };
}

Zauważ, że, podobnie jak enum Option, enum Result i jego warianty zostały wprowadzone do zasięgu przez preludium, więc nie musimy określać Result:: przed wariantami Ok i Err w ramionach match.

Kiedy wynik jest Ok, ten kod zwróci wewnętrzną wartość file z wariantu Ok, a następnie przypisujemy tę wartość uchwytu pliku do zmiennej greeting_file. Po match możemy użyć uchwytu pliku do odczytu lub zapisu.

Drugie ramię match obsługuje przypadek, w którym otrzymujemy wartość Err z File::open. W tym przykładzie zdecydowaliśmy się wywołać makro panic!. Jeśli w naszym bieżącym katalogu nie ma pliku o nazwie hello.txt i uruchomimy ten kod, zobaczymy następujące dane wyjściowe z makra panic!:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Jak zwykle, to wyjście mówi nam dokładnie, co poszło nie tak.

Dopasowywanie różnych błędów

Kod w Listing 9-4 spowoduje panic! niezależnie od tego, dlaczego File::open zakończyło się niepowodzeniem. My jednak chcemy podejmować różne działania z różnych powodów awarii. Jeśli File::open zakończyło się niepowodzeniem, ponieważ plik nie istnieje, chcemy utworzyć plik i zwrócić uchwyt do nowego pliku. Jeśli File::open zakończyło się niepowodzeniem z jakiegokolwiek innego powodu — na przykład, ponieważ nie mieliśmy uprawnień do otwarcia pliku — nadal chcemy, aby kod spowodował panic! w ten sam sposób, jak w Listing 9-4. W tym celu dodajemy wewnętrzne wyrażenie match, pokazane w Listing 9-5.

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 z tworzeniem pliku: {e:?}"),
            },
            _ => {
                panic!("Problem z otwarciem pliku: {error:?}");
            }
        },
    };
}

Typ wartości zwracanej przez File::open w wariancie Err to io::Error, który jest strukturą dostarczaną przez standardową bibliotekę. Ta struktura posiada metodę kind, którą możemy wywołać, aby uzyskać wartość io::ErrorKind. Enum io::ErrorKind jest dostarczany przez standardową bibliotekę i ma warianty reprezentujące różne rodzaje błędów, które mogą wynikać z operacji io. Wariant, którego chcemy użyć, to ErrorKind::NotFound, który wskazuje, że plik, który próbujemy otworzyć, jeszcze nie istnieje. Zatem dopasowujemy greeting_file_result, ale mamy również wewnętrzne dopasowanie na error.kind().

Warunkiem, który chcemy sprawdzić w wewnętrznym dopasowaniu, jest to, czy wartość zwrócona przez error.kind() jest wariantem NotFound enum ErrorKind. Jeśli tak, próbujemy utworzyć plik za pomocą File::create. Jednakże, ponieważ File::create również może zakończyć się niepowodzeniem, potrzebujemy drugiego ramienia w wewnętrznym wyrażeniu match. Kiedy plik nie może zostać utworzony, wyświetlany jest inny komunikat o błędzie. Drugie ramię zewnętrznego match pozostaje takie samo, więc program panikuje przy każdym błędzie innym niż błąd braku pliku.

Alternatywy dla używania match z Result<T, E>

To dużo match! Wyrażenie match jest bardzo przydatne, ale także bardzo prymitywne. W Rozdziale 13 poznasz domknięcia, które są używane z wieloma metodami zdefiniowanymi dla Result<T, E>. Te metody mogą być bardziej zwięzłe niż używanie match podczas obsługi wartości Result<T, E> w Twoim kodzie.

Na przykład, oto inny sposób napisania tej samej logiki, jak pokazano w Listing 9-5, tym razem używając domknięć i metody unwrap_or_else:

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 z tworzeniem pliku: {error:?}");
            })
        } else {
            panic!("Problem z otwarciem pliku: {error:?}");
        }
    });
}

Chociaż ten kod ma takie samo zachowanie jak Listing 9-5, nie zawiera żadnych wyrażeń match i jest czytelniejszy. Wróć do tego przykładu po przeczytaniu Rozdziału 13 i wyszukaj metodę unwrap_or_else w dokumentacji standardowej biblioteki. Wiele innych z tych metod może uporządkować ogromne, zagnieżdżone wyrażenia match, gdy masz do czynienia z błędami.

Skróty do paniki przy błędzie: unwrap i expect

Użycie match działa wystarczająco dobrze, ale może być nieco gadatliwe i nie zawsze dobrze komunikuje intencje. Typ Result<T, E> ma wiele pomocniczych metod zdefiniowanych na nim do wykonywania różnych, bardziej specyficznych zadań. Metoda unwrap jest skrótem zaimplementowanym tak samo jak wyrażenie match, które napisaliśmy w Listing 9-4. Jeśli wartość Result jest wariantem Ok, unwrap zwróci wartość wewnątrz Ok. Jeśli Result jest wariantem Err, unwrap wywoła dla nas makro panic!. Oto przykład unwrap w działaniu:

use std::fs::File;

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

Jeśli uruchomimy ten kod bez pliku hello.txt, zobaczymy komunikat o błędzie z wywołania panic!, które wykonuje metoda unwrap:

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

Podobnie, metoda expect pozwala nam również wybrać komunikat o błędzie panic!. Użycie expect zamiast unwrap i dostarczenie dobrych komunikatów o błędach może przekazać twoje intencje i ułatwić śledzenie źródła paniki. Składnia expect wygląda tak:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt powinien być dołączony do tego projektu");
}

Używamy expect w ten sam sposób co unwrap: aby zwrócić uchwyt pliku lub wywołać makro panic!. Komunikat o błędzie używany przez expect w jego wywołaniu panic! będzie parametrem, który przekazujemy do expect, zamiast domyślnego komunikatu panic!, którego używa unwrap. Oto jak to wygląda:

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

W kodzie produkcyjnym większość Rustacean preferuje expect zamiast unwrap i podaje więcej kontekstu, dlaczego operacja ma zawsze zakończyć się sukcesem. W ten sposób, jeśli twoje założenia kiedykolwiek okażą się błędne, masz więcej informacji do wykorzystania w debugowaniu.

Propagacja błędów

Kiedy implementacja funkcji wywołuje coś, co może zawieść, zamiast obsługiwać błąd w samej funkcji, możesz zwrócić błąd do kodu wywołującego, aby ten mógł zdecydować, co zrobić. Jest to znane jako propagacja błędu i daje większą kontrolę kodowi wywołującemu, gdzie może być więcej informacji lub logiki, która dyktuje, jak błąd powinien być obsłużony, niż to, co masz dostępne w kontekście swojego kodu.

Na przykład, Listing 9-6 pokazuje funkcję, która odczytuje nazwę użytkownika z pliku. Jeśli plik nie istnieje lub nie można go odczytać, funkcja zwróci te błędy do kodu, który wywołał funkcję.

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

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

    let mut username = String::new();

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

Ta funkcja może być napisana w znacznie krótszy sposób, ale zaczniemy od jej ręcznego implementowania, aby zbadać obsługę błędów; na końcu pokażemy krótszy sposób. Spójrzmy najpierw na typ zwracany przez funkcję: Result<String, io::Error>. Oznacza to, że funkcja zwraca wartość typu Result<T, E>, gdzie generyczny parametr T został wypełniony konkretnym typem String, a generyczny typ E został wypełniony konkretnym typem io::Error.

Jeśli ta funkcja zakończy się sukcesem bez żadnych problemów, kod, który ją wywołuje, otrzyma wartość Ok, która zawiera Stringusername, który ta funkcja odczytała z pliku. Jeśli ta funkcja napotka jakiekolwiek problemy, kod wywołujący otrzyma wartość Err, która zawiera więcej informacji o tym, jakie były problemy. Wybraliśmy io::Error jako typ zwracany tej funkcji, ponieważ jest to typ wartości błędu zwracany przez obie operacje, które wywołujemy w treści tej funkcji i które mogą zawieść: funkcja File::open i metoda read_to_string.

Ciało funkcji zaczyna się od wywołania funkcji File::open. Następnie obsługujemy wartość Result za pomocą match, podobnego do match w Listing 9-4. Jeśli File::open zakończy się sukcesem, uchwyt pliku w zmiennej wzorca file staje się wartością w mutowalnej zmiennej username_file, a funkcja kontynuuje. W przypadku Err, zamiast wywoływać panic!, używamy słowa kluczowego return, aby natychmiast wyjść z funkcji i przekazać wartość błędu z File::open, teraz w zmiennej wzorca e, z powrotem do kodu wywołującego jako wartość błędu tej funkcji.

Zatem, jeśli mamy uchwyt pliku w username_file, funkcja następnie tworzy nowy String w zmiennej username i wywołuje metodę read_to_string na uchwycie pliku w username_file, aby odczytać zawartość pliku do username. Metoda read_to_string również zwraca Result, ponieważ może zawieść, nawet jeśli File::open zakończyło się sukcesem. Musimy więc użyć kolejnego match do obsługi tego Result: Jeśli read_to_string zakończy się sukcesem, to nasza funkcja zakończyła się sukcesem, i zwracamy nazwę użytkownika z pliku, która jest teraz w username, zawiniętą w Ok. Jeśli read_to_string zawiedzie, zwracamy wartość błędu w taki sam sposób, w jaki zwróciliśmy wartość błędu w match, które obsługiwało wartość zwracaną przez File::open. Nie musimy jednak jawnie mówić return, ponieważ jest to ostatnie wyrażenie w funkcji.

Kod wywołujący ten kod będzie następnie obsługiwał otrzymanie wartości Ok, która zawiera nazwę użytkownika, lub wartości Err, która zawiera io::Error. To od kodu wywołującego zależy, co zrobić z tymi wartościami. Jeśli kod wywołujący otrzyma wartość Err, mógłby wywołać panic! i zniszczyć program, użyć domyślnej nazwy użytkownika lub wyszukać nazwę użytkownika gdzie indziej niż w pliku, na przykład. Nie mamy wystarczających informacji o tym, co kod wywołujący faktycznie próbuje zrobić, więc propagujemy wszystkie informacje o sukcesie lub błędzie w górę, aby zostały odpowiednio obsłużone.

Ten wzorzec propagacji błędów jest tak powszechny w Rust, że Rust udostępnia operator znak zapytania ?, aby to ułatwić.

Skrót operatora ? do propagacji błędów

Listing 9-7 pokazuje implementację read_username_from_file, która ma tę samą funkcjonalność co w Listing 9-6, ale ta implementacja używa operatora ?.

#![allow(unused)]
fn main() {
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)
}
}

Operator ? umieszczony po wartości Result działa prawie tak samo jak wyrażenia match, które zdefiniowaliśmy do obsługi wartości Result w Listing 9-6. Jeśli wartość Result to Ok, wartość wewnątrz Ok zostanie zwrócona z tego wyrażenia, a program będzie kontynuował. Jeśli wartość to Err, Err zostanie zwrócone z całej funkcji tak, jakbyśmy użyli słowa kluczowego return, tak aby wartość błędu została propagowana do kodu wywołującego.

Istnieje różnica między tym, co robi wyrażenie match z Listing 9-6, a tym, co robi operator ?: wartości błędów, na których wywołano operator ?, przechodzą przez funkcję from, zdefiniowaną w cesze From w standardowej bibliotece, która jest używana do konwertowania wartości z jednego typu na inny. Gdy operator ? wywołuje funkcję from, otrzymany typ błędu jest konwertowany na typ błędu zdefiniowany w typie zwracanym bieżącej funkcji. Jest to przydatne, gdy funkcja zwraca jeden typ błędu, aby reprezentować wszystkie sposoby, w jakie funkcja może zawieść, nawet jeśli części mogą zawieść z wielu różnych powodów.

Na przykład, moglibyśmy zmienić funkcję read_username_from_file w Listing 9-7 tak, aby zwracała niestandardowy typ błędu o nazwie OurError, który zdefiniujemy. Jeśli zdefiniujemy również impl From<io::Error> for OurError, aby skonstruować instancję OurError z io::Error, to wywołania operatora ? w ciele read_username_from_file wywołają from i przekonwertują typy błędów bez potrzeby dodawania do funkcji żadnego dodatkowego kodu.

W kontekście Listing 9-7, ? na końcu wywołania File::open zwróci wartość wewnątrz Ok do zmiennej username_file. Jeśli wystąpi błąd, operator ? natychmiast wyjdzie z całej funkcji i przekaże dowolną wartość Err do kodu wywołującego. To samo dotyczy ? na końcu wywołania read_to_string.

Operator ? eliminuje wiele szablonowego kodu i upraszcza implementację tej funkcji. Moglibyśmy nawet skrócić ten kod, łącząc wywołania metod bezpośrednio po ?, jak pokazano w Listing 9-8.

#![allow(unused)]
fn main() {
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)
}
}

Tworzenie nowego String w username przenieśliśmy na początek funkcji; ta część nie uległa zmianie. Zamiast tworzyć zmienną username_file, połączyliśmy wywołanie read_to_string bezpośrednio z wynikiem File::open("hello.txt")?. Nadal mamy ? na końcu wywołania read_to_string i nadal zwracamy wartość Ok zawierającą username, gdy zarówno File::open, jak i read_to_string zakończą się sukcesem, zamiast zwracać błędy. Funkcjonalność jest ponownie taka sama jak w Listing 9-6 i Listing 9-7; jest to po prostu inny, bardziej ergonomiczny sposób zapisu.

Listing 9-9 pokazuje sposób na jeszcze większe skrócenie tego za pomocą fs::read_to_string.

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

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

Odczytywanie pliku do ciągu znaków jest dość powszechną operacją, dlatego standardowa biblioteka udostępnia wygodną funkcję fs::read_to_string, która otwiera plik, tworzy nowy String, odczytuje zawartość pliku, umieszcza zawartość w tym String i zwraca go. Oczywiście, użycie fs::read_to_string nie daje nam możliwości wyjaśnienia całej obsługi błędów, dlatego najpierw zrobiliśmy to w dłuższy sposób.

Gdzie można używać operatora ?

Operator ? może być używany tylko w funkcjach, których typ zwracany jest zgodny z wartością, na której użyto ?. Dzieje się tak, ponieważ operator ? jest zdefiniowany do wykonania wczesnego zwrócenia wartości z funkcji, w ten sam sposób, co wyrażenie match, które zdefiniowaliśmy w Listing 9-6. W Listing 9-6 match używał wartości Result, a ramię wczesnego zwrócenia zwracało wartość Err(e). Typ zwracany funkcji musi być Result, aby był zgodny z tym return.

W Listing 9-10 przyjrzyjmy się błędowi, który otrzymamy, jeśli użyjemy operatora ? w funkcji main z typem zwracanym niezgodnym z typem wartości, na której używamy ?.

use std::fs::File;

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

Ten kod otwiera plik, co może zakończyć się niepowodzeniem. Operator ? następuje po wartości Result zwracanej przez File::open, ale ta funkcja main ma typ zwracany (), a nie Result. Kiedy skompilujemy ten kod, otrzymamy następujący komunikat o błędzie:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
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() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

Ten błąd wskazuje, że możemy używać operatora ? tylko w funkcji, która zwraca Result, Option lub inny typ implementujący FromResidual.

Aby naprawić błąd, masz dwie możliwości. Jedna to zmiana typu zwracanego przez funkcję, aby był zgodny z wartością, na której używasz operatora ?, o ile nie ma żadnych ograniczeń, które by to uniemożliwiały. Druga to użycie match lub jednej z metod Result<T, E>, aby obsłużyć Result<T, E> w dowolny odpowiedni sposób.

Komunikat o błędzie wspominał również, że ? może być używany z wartościami Option<T>. Podobnie jak w przypadku użycia ? na Result, możesz używać ? na Option tylko w funkcji, która zwraca Option. Zachowanie operatora ? wywoływanego na Option<T> jest podobne do jego zachowania wywoływanego na Result<T, E>: Jeśli wartość to None, None zostanie zwrócone z funkcji w tym momencie. Jeśli wartość to Some, wartość wewnątrz Some jest wartością wynikową wyrażenia, a funkcja kontynuuje. Listing 9-11 zawiera przykład funkcji, która znajduje ostatni znak pierwszej linii w danym tekście.

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

fn main() {
    assert_eq!(
        last_char_of_first_line("Witaj, świecie\nJak się masz dzisiaj?"),
        Some('e')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}

Ta funkcja zwraca Option<char>, ponieważ możliwe jest, że tam jest znak, ale możliwe jest również, że go nie ma. Ten kod pobiera argument wycinka ciągu text i wywołuje na nim metodę lines, która zwraca iterator po liniach w ciągu. Ponieważ ta funkcja chce zbadać pierwszą linię, wywołuje next na iteratorze, aby uzyskać pierwszą wartość z iteratora. Jeśli text jest pustym ciągiem, to wywołanie next zwróci None, w którym to przypadku używamy ?, aby zatrzymać i zwrócić None z last_char_of_first_line. Jeśli text nie jest pustym ciągiem, next zwróci wartość Some zawierającą wycinek ciągu pierwszej linii w text.

Operator ? wyodrębnia wycinek ciągu znaków, a my możemy wywołać chars na tym wycinku ciągu znaków, aby uzyskać iterator jego znaków. Interesuje nas ostatni znak w tej pierwszej linii, więc wywołujemy last, aby zwrócić ostatni element w iteratorze. Jest to Option, ponieważ możliwe jest, że pierwsza linia jest pustym ciągiem; na przykład, jeśli text zaczyna się od pustej linii, ale ma znaki w innych liniach, jak w "\nhi". Jednakże, jeśli istnieje ostatni znak w pierwszej linii, zostanie on zwrócony w wariancie Some. Operator ? w środku daje nam zwięzły sposób wyrażenia tej logiki, pozwalając nam zaimplementować funkcję w jednej linii. Gdybyśmy nie mogli użyć operatora ? na Option, musielibyśmy zaimplementować tę logikę, używając większej liczby wywołań metod lub wyrażenia match.

Zauważ, że możesz używać operatora ? na Result w funkcji, która zwraca Result, i możesz używać operatora ? na Option w funkcji, która zwraca Option, ale nie możesz ich mieszać. Operator ? nie przekonwertuje automatycznie Result na Option ani na odwrót; w tych przypadkach możesz użyć metod, takich jak metoda ok na Result lub metoda ok_or na Option, aby wykonać konwersję jawnie.

Do tej pory wszystkie używane przez nas funkcje main zwracały (). Funkcja main jest specjalna, ponieważ jest punktem wejścia i wyjścia programu wykonywalnego, a istnieją ograniczenia dotyczące jej typu zwracanego, aby program działał zgodnie z oczekiwaniami.

Na szczęście, main może również zwrócić Result<(), E>. Listing 9-12 zawiera kod z Listing 9-10, ale zmieniliśmy typ zwracany main na Result<(), Box<dyn Error>> i dodaliśmy wartość zwracaną Ok(()) na końcu. Ten kod teraz się skompiluje.

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

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

    Ok(())
}

Typ Box<dyn Error> to obiekt cechy, o którym będziemy mówić w sekcji „Używanie obiektów cech do abstrakcji wspólnego zachowania” w Rozdziale 18. Na razie możesz czytać Box<dyn Error> jako „dowolny rodzaj błędu”. Użycie ? na wartości Result w funkcji main z typem błędu Box<dyn Error> jest dozwolone, ponieważ pozwala to na wczesne zwrócenie dowolnej wartości Err. Mimo że ciało tej funkcji main zawsze będzie zwracać błędy typu std::io::Error, poprzez określenie Box<dyn Error>, ta sygnatura będzie nadal poprawna, nawet jeśli do ciała main zostanie dodany więcej kodu, który zwraca inne błędy.

Kiedy funkcja main zwraca Result<(), E>, plik wykonywalny zakończy działanie z wartością 0, jeśli main zwróci Ok(()), a zakończy działanie z wartością różną od zera, jeśli main zwróci wartość Err. Pliki wykonywalne napisane w C zwracają liczby całkowite po zakończeniu działania: programy, które zakończyły działanie pomyślnie, zwracają liczbę całkowitą 0, a programy, które zakończyły się błędem, zwracają jakąś liczbę całkowitą inną niż 0. Rust również zwraca liczby całkowite z plików wykonywalnych, aby być zgodnym z tą konwencją.

Funkcja main może zwracać dowolne typy implementujące cechę std::process::Termination, która zawiera funkcję report zwracającą ExitCode. Zapoznaj się z dokumentacją standardowej biblioteki, aby uzyskać więcej informacji na temat implementacji cechy Termination dla własnych typów.

Teraz, gdy omówiliśmy szczegóły wywoływania panic! lub zwracania Result, wróćmy do tematu, jak zdecydować, który z nich jest odpowiedni do użycia w jakich przypadkach.