Odwracalne Błędy z Result

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

Przypomnijmy z „Obsługa potencjalnych błędów z użyciem Result w rozdziale 2, że enum Result jest zdefiniowane jako posiadające 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 są parametrami typów generycznych: omówimy je bardziej szczegółowo w rozdziale 10. W tym momencie należy wiedzieć, ż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 ogólne parametry typu, możemy użyć typu Result i funkcji zdefiniowanych na nim w wielu różnych sytuacjach, o różnych zwracanych wartościach sukcesu i błędu.

Wywołajmy funkcję, która zwraca wartość Result, ponieważ funkcja może się nie powieść. Na listingu 9-3 próbujemy otworzyć plik.

Plik: src/main.rs

use std::fs::File;

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

Listing 9-3: Otwarcie pliku

Typem zwracanym przez File::open jest Result<T, E>. Ogólny parametr T został wypełniony przez implementację File::open typem wartości sukcesu, std::fs::File, który jest uchwytem pliku. Typ E użyty w wartości błędu to std::io::Error. Ten typ zwrotu oznacza, że wywołanie funkcji File::open może się powieść 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ń dostępu do pliku. Funkcja File::open musi mieć sposób na poinformowanie nas o powodzeniu lub niepowodzeniu i jednocześnie przekazać nam uchwyt pliku lub informacje o błędzie. Te informacje są dokładnie tym, co przekazuje wyliczenie Result.

W przypadku gdy File::open się powiedzie, wartością w zmiennej greeting_file_result będzie instancja Ok zawierająca uchwyt pliku. W przypadku niepowodzenia, wartość w greeting_file_result będzie instancją Err, która zawiera więcej informacji o rodzaju błędu, który wystąpił.

Musimy dodać do kodu z listingu 9-3 różne akcje w zależności od wartości zwracanej przez File::open. Listing 9-4 pokazuje jeden ze sposobów obsługi Result przy użyciu podstawowego narzędzia, wyrażenia match, które omówiliśmy w rozdziale 6.

Plik: 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: Użycie wyrażenia match do obsługi wariantów Result, które mogą zostać zwrócone

Zauważ, że podobnie jak wyliczenie Option, wyliczenie Result i jego warianty zostały wprowadzone do zakresu przez preludium, więc nie musimy podawać Result:: przed wariantami Ok i Err w elementach match.

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

Drugie pole match obsługuje przypadek, w którym otrzymujemy wartość Err z File::open. W tym przykładzie wybraliśmy wywołanie makra 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 [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Jak zwykle, te dane wyjściowe mówią nam dokładnie, co poszło nie tak.

Dopasowanie na Podstawie Różnych Błędów

Kod z listingu 9-4 zgłosi panic! bez względu na przyczynę niepowodzenia File::open. Chcemy jednak podjąć różne działania dla różnych przyczyn niepowodzenia: jeśli File::open nie powiodło się, ponieważ plik nie istnieje, chcemy utworzyć plik i zwrócić uchwyt do nowego pliku. Jeśli File::open nie powiodło się z jakiegokolwiek innego powodu - na przykład, ponieważ nie mieliśmy uprawnień do otwarcia pliku - nadal chcemy, aby kod zgłosił panic! w taki sam sposób, jak na listingu 9-4. W tym celu dodajemy wewnętrzne wyrażenie match, pokazane na listingu 9-5.

Plik: 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: Obsługa różnych rodzajów błędów na różne sposoby

Typ wartości zwracanej przez File::open wewnątrz wariantu Err to io::Error, który jest strukturą dostarczaną przez bibliotekę standardową. Struktura ta posiada metodę kind, którą możemy wywołać, aby uzyskać wartość io::ErrorKind. Enum io::ErrorKind jest dostarczane przez bibliotekę standardową i posiada 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. Dopasowujemy się więc do greeting_file_result, ale mamy też wewnętrzne dopasowanie do error.kind().

Warunkiem, który chcemy sprawdzić w wewnętrznym dopasowaniu jest to, czy wartość zwrócona przez error.kind() jest wariantem NotFound wyliczenia ErrorKind. Jeśli tak, próbujemy utworzyć plik za pomocą File::create. Jednakże, ponieważ File::create może również zawieść, potrzebujemy drugiego argumentu w wewnętrznym wyrażeniu match. Jeśli plik nie może zostać utworzony, drukowany jest inny komunikat o błędzie. Drugie dopasowanie zewnętrznego wyrażenia match pozostaje takie samo, więc program panikuje przy każdym błędzie poza błędem braku pliku.

Alternatywy do Użycia match z Result<T, E>

Tak dużo tych match! Wyrażenie match jest bardzo użyteczne, ale także bardzo prymitywne. W rozdziale 13 dowiesz się o domknięciach, które są używane z wieloma metodami zdefiniowanymi w Result<T, E>. Metody te mogą być bardziej zwięzłe niż użycie match podczas obsługi wartości Result<T, E> w kodzie.

Na przykład, oto inny sposób na napisanie tej samej logiki, jak pokazano na listingu 9-5, tym razem przy użyciu 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 creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Chociaż ten kod zachowuje się tak samo jak z listingu 9-5, nie zawiera żadnych wyrażeń match i jest bardziej czytelny. Wróć do tego przykładu po przeczytaniu rozdziału 13 i poszukaj metody unwrap_or_else w dokumentacji biblioteki standardowej. Wiele z tych metod może oczyścić ogromne zagnieżdżone wyrażenia match, gdy masz do czynienia z błędami.

Skróty do Obsłużenia Paniki z Błędu: unwrap and expect

Używanie match działa wystarczająco dobrze, ale może być nieco rozwlekłe i nie zawsze dobrze komunikuje intencje. Typ Result<T, E> ma zdefiniowanych wiele metod pomocniczych do wykonywania różnych, bardziej specyficznych zadań. Metoda unwrap jest metodą skrótu zaimplementowaną podobnie jak wyrażenie match, które napisaliśmy na listingu 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 użycia unwrap:

Plik: src/main.rs

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 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49

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

Plik: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Używamy expect w taki sam sposób jak unwrap: by zwrócić uchwyt pliku lub wywołać makro panic!. Komunikat błędu użyty przez expect w wywołaniu panic! będzie parametrem, który przekażemy do expect, zamiast domyślnego komunikatu panic!, którego używa unwrap. Oto jak to wygląda:

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

W kodzie produkcyjnym, większość Rustowców wybiera expect zamiast unwrap i podaje więcej kontekstu na temat tego, dlaczego operacja ma się zawsze udać. W ten sposób, jeśli twoje założenia okażą się błędne, masz więcej informacji do wykorzystania podczas debugowania.

Propagowanie Błędów

Gdy implementacja funkcji wywołuje coś, co może się nie powieść, zamiast obsługiwać błąd w samej funkcji, można zwrócić błąd do kodu wywołującego, aby mógł zdecydować, co zrobić. Jest to znane jako propagowanie błędu i daje większą kontrolę kodowi wywołującemu, gdzie może być więcej informacji lub logiki, która dyktuje sposób obsługi błędu niż to, co jest dostępne w kontekście twojego 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 ta zwróci te błędy do kodu, który ją wywołał.

Plik: src/main.rs

#![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),
    }
}
}

Listing 9-6: Funkcja zwracająca błędy do kodu wywołującego przy użyciu match

Ta funkcja może być napisana w znacznie krótszy sposób, ale zaczniemy od zrobienia wielu z nich ręcznie, aby zbadać obsługę błędów; na koniec pokażemy krótszy sposób. Przyjrzyjmy się najpierw typowi zwracanemu funkcji: Result<String, io::Error>. Oznacza to, że funkcja zwraca wartość typu Result<T, E>, gdzie parametr generyczny T został wypełniony konkretnym typem String, a typ generyczny E został wypełniony konkretnym typem io::Error.

Jeśli ta funkcja powiedzie się bez żadnych problemów, kod wywołujący tę funkcję otrzyma wartość Ok, która zawiera String - nazwę użytkownika, którą ta funkcja odczytała z pliku. Jeśli ta funkcja napotka jakiekolwiek problemy, kod wywołujący otrzyma wartość Err, która zawiera instancję io::Error, która zawiera więcej informacji o problemach. Wybraliśmy io::Error jako typ zwracany tej funkcji, ponieważ tak się składa, że jest to typ wartości błędu zwracanej z obu operacji, które wywołujemy w ciele tej funkcji, a które mogą się nie powieść: funkcji File::open i metody read_to_string.

Ciało funkcji rozpoczyna się od wywołania funkcji File::open. Następnie obsługujemy wartość Result za pomocą match podobnego do match z listingu 9-4. Jeśli File::open się powiedzie, uchwyt pliku w zmiennej wzorca file staje się wartością w zmiennej mutowalnej username_file i funkcja jest kontynuowana. W przypadku Err, zamiast wywoływać panic!, używamy słowa kluczowego return, aby całkowicie powrócić 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.

Jeśli więc mamy uchwyt pliku w username_file, funkcja 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 się nie powieść, nawet jeśli File::open się powiedzie. Potrzebujemy więc kolejnego match do obsługi tego Result: jeśli read_to_string się powiedzie, to nasza funkcja się powiedzie i zwrócimy nazwę użytkownika z pliku, która jest teraz w username zawinięta w Ok. Jeśli read_to_string się nie powiedzie, zwracamy wartość błędu w ten sam sposób, w jaki zwróciliśmy wartość błędu w match, który obsłużył wartość zwracaną File::open. Nie musimy jednak wyraźnie używać wyrażenia return, ponieważ jest to ostatnie wyrażenie w funkcji.

Kod, który wywołuje ten kod będzie następnie obsługiwał otrzymanie wartości Ok zawierającej nazwę użytkownika lub wartości Err zawierającej io::Error. Do kodu wywołującego należy decyzja, co zrobić z tymi wartościami. Jeśli kod wywołujący otrzyma wartość Err, może na przykład wywołać panic! i zawiesić program, użyć domyślnej nazwy użytkownika lub wyszukać nazwę użytkownika z innego miejsca niż plik. Nie mamy wystarczających informacji na temat tego, co kod wywołujący faktycznie próbuje zrobić, więc propagujemy wszystkie informacje o powodzeniu lub błędzie w górę, aby mógł je odpowiednio obsłużyć.

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

Skrót do Propagawania Błędów: Operator ?

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

Plik: src/main.rs

#![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)
}
}

Listing 9-7: Funkcja zwracająca błędy do kodu wywołującego przy użyciu operatora ?

Wyrażenie ? umieszczone po wartości Result jest zdefiniowane tak, aby działało w prawie taki sam sposób jak wyrażenia match, które zdefiniowaliśmy do obsługi wartości Result na listingu 9-6. Jeśli wartością Result jest Ok, wartość wewnątrz Ok zostanie zwrócona z tego wyrażenia, a program będzie kontynuowany. Jeśli wartością jest Err, Err zostanie zwrócone z całej funkcji, tak jakbyśmy użyli słowa kluczowego return, więc wartość błędu zostanie rozpropagowana do kodu wywołującego.

Istnieje różnica między tym, co robi wyrażenie match z listingu 9-6, a tym, co robi operator ?: wartości błędów, które mają wywołany operator ? przechodzą przez funkcję from, zdefiniowaną w cechach From w bibliotece standardowej, która jest używana do konwersji 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 jej części mogą zawieść z wielu różnych powodów.

Na przykład, możemy zmienić funkcję read_username_from_file z listingu 9-7, 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, wtedy wywołania operatora ? w ciele read_username_from_file wywołają from i przekonwertują typy błędów bez potrzeby dodawania dodatkowego kodu do funkcji.

W kontekście Listingu 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 ? powróci wcześniej 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 szablonów i upraszcza implementację tej funkcji. Moglibyśmy nawet jeszcze bardziej skrócić ten kod, łącząc wywołania metod bezpośrednio po ?, jak pokazano na listingu 9-8.

Plik: src/main.rs

#![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)
}
}

Listing 9-8: Łączenie wywołań metod po operatorze ?

Przenieśliśmy tworzenie nowego String w username 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 się powiodą, zamiast zwracać błędy. Funkcjonalność jest ponownie taka sama jak na listingu 9-6 i listingu 9-7; jest to po prostu inny, bardziej ergonomiczny sposób zapisu.

Listing 9-9 pokazuje sposób na uczynienie tego jeszcze krótszym przy użyciu fs::read_to_string.

Plik: src/main.rs

#![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")
}
}

Listing 9-9: Użycie fs::read_to_string zamiast otwierania i odczytywania pliku

Odczytywanie pliku do łańcucha jest dość powszechną operacją, więc biblioteka standardowa dostarcza 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, więc najpierw zrobiliśmy to w dłuższy sposób.

Gdzie Operator ? Może Być Używany

Operator ? może być używany tylko w funkcjach, których typ zwracany jest zgodny z wartością, na której ? jest używany. Dzieje się tak dlatego, że operator ? jest zdefiniowany do wczesnego zwracania wartości z funkcji, w taki sam sposób jak wyrażenie match, które zdefiniowaliśmy na listingu 9-6. Na listingu 9-6, wyrażenie match używało wartości Result, a element wczesnego powrotu zwracał wartość Err(e). Typ zwracany funkcji musi być Result, aby był kompatybilny z tym return.

Na Listingu 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 ?:

Plik: src/main.rs

use std::fs::File;

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

Listing 9-10: Próba użycia ? w funkcji main zwracającej () nie zostanie skompilowana

Ten kod otwiera plik, co może się nie udać. Operator ? podąża za wartością Result zwracaną 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: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

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

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

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

Komunikat o błędzie wspomina, że ? może być również używany z wartościami Option<T>. Podobnie jak w przypadku użycia ? na Result, możesz użyć ? na Option tylko w funkcji, która zwraca Option. Zachowanie operatora ? po wywołaniu na Option<T> jest podobne do jego zachowania po wywołaniu na Result<T, E>: jeśli wartością jest None, to None zostanie zwrócone wcześniej z funkcji w tym momencie. Jeśli wartością jest Some, wartość wewnątrz Some jest wartością wynikową wyrażenia i funkcja jest kontynuowana. Listing 9-11 zawiera przykład funkcji, która znajduje ostatni znak pierwszej linii w podanym 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("Hello, world\nHow are you today?"),
        Some('d')
    );

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

Listing 9-11: Użycie operatora ? na wartości Option<T>

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

Znak ? wyodrębnia wycinek łańcucha i możemy wywołać chars na tym wycinku łańcucha, 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 zawiera znaki w innych liniach, jak w „\nhi”. Jednakże, jeśli w pierwszej linii znajduje się ostatni znak, zostanie on zwrócony w wariancie Some. Operator ? w środku daje nam zwięzły sposób na wyrażenie tej logiki, pozwalając nam zaimplementować funkcję w jednej linii. Gdybyśmy nie mogli użyć operatora ? w Option, musielibyśmy zaimplementować tę logikę używając więcej wywołań metod lub wyrażenia match.

Zauważ, że możesz użyć operatora ? na Result w funkcji, która zwraca Result i możesz użyć operatora ? na Option w funkcji, która zwraca Option, ale nie można ich dowolnie mieszać. Operator ? nie konwertuje automatycznie Result na Option lub odwrotnie; w takich przypadkach można użyć metod takich jak ok na Result lub ok_or na Option, aby dokonać konwersji w sposób jawny.

Jak dotąd, wszystkie funkcje main, których używaliśmy zwracały (). Funkcja main jest wyjątkowa, ponieważ jest punktem wejścia i wyjścia programów wykonywalnych i istnieją ograniczenia dotyczące tego, jaki może być jej typ zwracany, aby programy zachowywały się zgodnie z oczekiwaniami.

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

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: Zmiana main by zwracał Result<(), E> pozwala na użycie operatora ? na wartościach Result.

Typ Box<dyn Error> jest trait object, o którym będziemy mówić w sekcji „Using Trait Objects that Allow for Values of Different Types” w rozdziale 17. Na razie można odczytać 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 na wczesne zwrócenie dowolnej wartości Err. Nawet jeśli ciało tej funkcji main będzie zwracać tylko błędy typu std::io::Error, poprzez określenie Box<dyn Error>, ta sygnatura będzie nadal poprawna, nawet jeśli więcej kodu zwracającego inne błędy zostanie dodane do ciała main.

Gdy funkcja main zwraca Result<(), E>, program wykonywalny zakończy działanie z wartością 0 jeśli main zwróci Ok()) i zakończy działanie z wartością niezerową jeśli main zwróci Err. Programy wykonywalne napisane w C zwracają liczby całkowite, gdy kończą działanie: programy, które zakończą działanie pomyślnie zwracają liczbę całkowitą 0, a programy, które zwrócą błąd zwracają 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, które implementują cechę std::process::Termination trait, który zawiera funkcję report zwracającą ExitCode. Więcej informacji na temat implementacji cechy Termination dla własnych typów można znaleźć w dokumentacji biblioteki standardowej.

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