Wdrażasz Error
dokładnie tak, jak robiłbyś any other trait; nic o niej bardzo szczególny:
pub trait Error: Debug + Display {
fn description(&self) -> &str;
fn cause(&self) -> Option<&Error> { ... }
}
description
metoda jest wymagane, cause
ma domyślną implementację oraz preferowany typ musi także wdrożyć Debug
i Display
, ponieważ są one supertraits.
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct Thing;
impl Error for Thing {
fn description(&self) -> &str {
"Something bad happened"
}
}
impl fmt::Display for Thing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Oh no, something bad went down")
}
}
fn main() {}
Oczywiście, co Thing
zawiera, a tym samym implementacje metod, w dużym stopniu zależy od tego, jakie błędy chcesz mieć. Być może chcesz umieścić w nim nazwę pliku, a może jakąś liczbę całkowitą. Być może chcesz mieć enum
zamiast struct
, aby reprezentować wiele typów błędów.
Jeśli skończy się zawijanie istniejących błędów, zalecane jest wdrożenie From
, aby przekonwertować te błędy i błąd. Dzięki temu można używać try!
i ?
i mieć dość ergonomiczne rozwiązanie.
Czy to jest najbardziej idiomatyczny sposób na zrobienie tego?
Idiomatycznie, powiedziałbym, że biblioteka będzie mieć małą (może 1-3) liczbę głównych typów błędów, które są narażone. Prawdopodobnie są to wyliczenia innych typów błędów. Dzięki temu konsumenci skrzyni nie mogą poradzić sobie z eksplozją typów. Oczywiście zależy to od interfejsu API i od tego, czy ma sens scalanie niektórych błędów razem.
Inną rzeczą, na którą należy zwrócić uwagę, jest to, że jeśli zdecydujesz się osadzić dane w błędzie, może to mieć daleko idące konsekwencje.Na przykład biblioteka standardowa nie zawiera nazwy pliku w błędach związanych z plikami. Takie działanie spowodowałoby dodatkowy napływ do każdego błędu pliku. Wywoływacz metody zwykle ma odpowiedni kontekst i może zdecydować, czy ten kontekst musi zostać dodany do błędu, czy nie.
Polecam zrobić to ręcznie kilka razy, aby zobaczyć, jak wszystkie elementy idą w parze. Kiedy już to zrobisz, będziesz zmęczony robieniem tego ręcznie. Następnie możesz sprawdzić skrzynie, takie jak quick-error lub error-chain, które udostępniają makra, aby zmniejszyć wartość zadaną.
Moja preferowana biblioteka jest szybkie błędów, więc oto przykład użycia że z oryginalnego typu błędu:
#[macro_use]
extern crate quick_error;
quick_error! {
#[derive(Debug)]
enum MyError {
Gizmo {
description("Refrob the Gizmo")
}
WidgetNotFound(widget_name: String) {
description("The widget could not be found")
display(r#"The widget "{}" could not be found"#, widget_name)
}
}
}
fn foo() -> Result<(), MyError> {
Err(MyError::WidgetNotFound("Quux".to_string()))
}
fn main() {
println!("{:?}", foo());
}
Uwaga Usunąłem zbędne Error
przyrostek na każdej wartości typu wyliczeniowego. Często można też wywołać typ Error
i zezwolić konsumentowi na prefiks typu (mycrate::Error
) lub zmienić jego nazwę podczas importowania (use mycrate::Error as FooError
).
Przypuszczam, że już przeczytałeś [odpowiednią sekcję z książki] (https://doc.rust-lang.org/stable/book/error-handling.html#defining-your-own-error-type). :) Ale biorąc pod uwagę wiele błędów w tym zakresie, odpowiedzi mogą chcieć rozwinąć więcej niż tylko ten wpis. –