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
matchzResult<T, E>To dużo
match! Wyrażeniematchjest bardzo przydatne, ale także bardzo prymitywne. W Rozdziale 13 poznasz domknięcia, które są używane z wieloma metodami zdefiniowanymi dlaResult<T, E>. Te metody mogą być bardziej zwięzłe niż używaniematchpodczas obsługi wartościResult<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ń
matchi jest czytelniejszy. Wróć do tego przykładu po przeczytaniu Rozdziału 13 i wyszukaj metodęunwrap_or_elsew dokumentacji standardowej biblioteki. Wiele innych z tych metod może uporządkować ogromne, zagnieżdżone wyrażeniamatch, 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 String – username, 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.