Короче говоря: нет, это не хорошо. String as error отбрасывает информацию о деталях и причинах, делая эту ошибку бесполезной для вызывающей стороны, так как она не может проверить ее и, возможно, восстановить после нее.
В случае, если вам просто нужно заполнить параметр Error чем-то, создайте структуру модуля. Это не очень полезно, но и не так сильно, как строка. И вы можете легко отличить foo::SomeError
от bar::SomeError
.
#[derive(Debug)]
pub struct SomeError; // No fields.
Если вы можете перечислить варианты ошибок, используйте enum
.
Иногда полезно также включить в него другие ошибки.
#[derive(Debug)]
pub enum PasswordError {
Empty,
ToShort,
NoDigits,
NoLetters,
NoSpecials
}
#[derive(Debug)]
pub enum ConfigLoadError {
InvalidValues,
DeserializationError(serde::de::Error),
IoError(std::io::Error),
}
Никто не мешает вам использовать struct
с.
Они особенно полезны, когда вы намеренно хотите скрыть некоторую информацию от вызывающей стороны (в отличие от enum
, чьи варианты всегда имеют публичную видимость). Например. вызывающий не имеет ничего общего с сообщением об ошибке, но может использовать kind
для его обработки:
pub enum RegistrationErrorKind {
InvalidName { wrong_char_idx: usize },
NonUniqueName,
WeakPassword,
DatabaseError(db::Error),
}
#[derive(Debug)]
pub struct RegistrationError {
message: String, // Private field
pub kind: RegistrationErrorKind, // Public field
}
impl Ошибка - экзистенциальный тип - здесь не имеет смысла. Вы не можете возвращать разные типы ошибок с ним в месте ошибки, если это было вашим намерением. И непрозрачные ошибки не очень полезны, как строки.
std::error::Error
черта гарантирует, что ваш тип SomeError
имеет реализацию для std::fmt::{Display, Debug}
(для отображения ошибки пользователю и разработчику, соответственно) и предоставляет некоторые полезные методы, такие как source
(Это возвращает причину этой ошибки); is
, downcast
, downcast_ref
, downcast_mut
. Последние 4 относятся к стиранию типа ошибки.
Ошибка типа стирания
У стирания типа ошибки есть свои недостатки, но также стоит упомянуть.
Это также особенно полезно при написании кода приложения высокого уровня. Но в случае библиотек вы должны дважды подумать, прежде чем использовать этот подход, потому что это сделает вашу библиотеку непригодной для использования с no_std.
Допустим, у вас есть какая-то функция с нетривиальной логикой, которая может возвращать значения некоторых типов ошибок, а не точно одной. В этом случае вы можете использовать (но не злоупотреблять) тип ошибки:
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::Error as IoError;
use std::net::AddrParseError;
use std::net::Ipv4Addr
use std::path::Path;
// Error for case where file contains '127.0.0.1'
#[derive(Debug)]
pub struct AddressIsLocalhostError;
// Display implementation is required for std::error::Error.
impl fmt::Display for AddressIsLocalhostError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Address is localhost")
}
}
impl Error for AddresIsLocalhostError {} // Defaults are okay here.
// Now we have a function that takes a path and returns
// non-localhost Ipv4Addr on success.
// On fail it can return either of IoError, AddrParseError or AddressIsLocalhostError.
fn non_localhost_ipv4_from_file(path: &Path) -> Result<Ipv4Addr, Box<dyn Error + 'static>> {
// Opening and reading file may cause IoError.
// ? operator will automatically convert it to Box<dyn Error + 'static>.
// (via From trait implementation)
// This way concrete type of error is "erased": we don't know what's
// in a box, in fact it's kind of black box now, but we still can call
// methods that Error trait provides.
let content = fs::read_to_string(path)?;
// Parsing Ipv4Addr from string [slice]
// may cause another error: AddressParseError.
// And ? will convert it to to the same type: Box<dyn Error + 'static>
let addr: Ipv4Addr = content.parse()?;
if addr == Ipv4Add::new(127, 0, 0, 1) {
// Here we perform manual conversion
// from AddressIsLocalhostError
// to Box<dyn Error + 'static> and return error.
return Err(AddressIsLocalhostError.into());
}
// Everyhing is okay, returning addr.
Ok(Ipv4Addr)
}
fn main() {
// Let's try to use our function.
let maybe_address = non_localhost_ipv4_from_file(
"sure_it_contains_localhost.conf"
);
// Let's see what kind of magic Error trait provides!
match maybe_address {
// Print address on success.
Ok(addr) => println!("File was containing address: {}", addr),
Err(err) => {
// We sure can just print this error with.
// println!("{}", err.as_ref());
// Because Error implementation implies Display implementation.
// But let's imagine we want to inspect error.
// Here deref coercion implicitly converts
// `&Box<dyn Error>` to `&dyn Error`.
// And downcast_ref tries to convert this &dyn Error
// back to &IoError, returning either
// Some(&IoError) or none
if Some(err) = err.downcast_ref::<IoError>() {
println!("Unfortunately, IO error occured: {}", err)
}
// There's also downcast_mut, which does the same, but gives us
// mutable reference.
if Some(mut err) = err.downcast_mut::<AddressParseError>() {
// Here we can mutate err. But we'll only print it.
println!(
"Unfortunately, what file was cantaining, \
was not in fact an ipv4 address: {}",
err
);
}
// Finally there's 'is' and 'downcast'.
// 'is' comapres "erased" type with some concrete type.
if err.is::<AddressIsLocalhostError>() {
// 'downcast' tries to convert Box<dyn Error + 'static>
// to box with value of some concrete type.
// Here - to Box<AddressIsLocalhostError>.
let err: Box<AddressIsLocalhostError> =
Error::downcast(err).unwrap();
}
}
};
}
Подводя итог: ошибки должны (я бы сказал - должен) предоставлять полезную информацию вызывающей стороне, кроме возможности просто отображать их, поэтому они не должны быть строками. И ошибки должны реализовывать Error как минимум, чтобы сохранить более-менее последовательный опыт обработки ошибок во всех ящиках. Все остальное зависит от ситуации.
Кайо уже упомянул Книгу ржавчины.
Но эти ссылки могут быть также полезны:
std :: любая документация API уровня модуля
std :: error :: Ошибка документации API