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
matchzResult<T, E>Tak dużo tych
match! Wyrażeniematchjest 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 wResult<T, E>. Metody te mogą być bardziej zwięzłe niż użyciematchpodczas obsługi wartościResult<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ń
matchi jest bardziej czytelny. Wróć do tego przykładu po przeczytaniu rozdziału 13 i poszukaj metodyunwrap_or_elsew dokumentacji biblioteki standardowej. Wiele z tych metod może oczyścić ogromne zagnieżdżone wyrażeniamatch, 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.