Ответ 1
Вы реализуете Error
точно так же, как и любую другую черту; в этом нет ничего особенного:
pub trait Error: Debug + Display {
fn description(&self) -> &str { /* ... */ }
fn cause(&self) -> Option<&Error> { /* ... */ }
fn source(&self) -> Option<&(Error + 'static)> { /* ... */ }
}
description
, cause
и source
имеют реализации по умолчанию 1 и ваш тип должен также реализовывать Debug
и Display
, так как они являются супертрейтами.
use std::{error::Error, fmt};
#[derive(Debug)]
struct Thing;
impl Error for Thing {}
impl fmt::Display for Thing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Oh no, something bad went down")
}
}
Конечно, то, что содержит Thing
, и, следовательно, реализация методов, сильно зависит от того, какие ошибки вы хотите иметь. Возможно, вы хотите включить туда имя файла или, может быть, какое-то целое число. Возможно, вы хотите иметь enum
вместо struct
для представления нескольких типов ошибок.
Если вы закончите оборачивать существующие ошибки, я бы порекомендовал реализовать From
для преобразования между этими ошибками и вашей ошибкой. Это позволяет использовать try!
а ?
и иметь довольно эргономичное решение.
Это самый идиоматический способ?
Идиоматически, я бы сказал, что библиотека будет иметь небольшое (возможно, 1-3) число основных типов ошибок, которые могут быть обнаружены. Вероятно, это перечисления других типов ошибок. Это позволяет потребителям вашего ящика не иметь дело со взрывом типов. Конечно, это зависит от вашего API и от того, имеет ли смысл объединять некоторые ошибки вместе или нет.
Еще одна вещь, которую стоит отметить, это то, что если вы решите встроить данные в ошибку, это может иметь далеко идущие последствия. Например, стандартная библиотека не включает имя файла в ошибках, связанных с файлом. Это добавит накладные расходы к каждой ошибке файла. Вызывающий метод обычно имеет соответствующий контекст и может решить, должен ли этот контекст быть добавлен к ошибке или нет.
Я бы порекомендовал сделать это вручную несколько раз, чтобы увидеть, как все части идут вместе. Как только вы это сделаете, вы устанете делать это вручную. Затем вы можете проверить ящики, которые предоставляют макросы для уменьшения шаблона:
Моя предпочтительная библиотека - SNAFU (потому что я ее написал), поэтому вот пример использования этого с вашим исходным типом ошибки:
// This example uses the simpler syntax supported in Rust 1.34
use snafu::Snafu; // 0.2.0
#[derive(Debug, Snafu)]
enum MyError {
#[snafu(display("Refrob the Gizmo"))]
Gizmo,
#[snafu(display("The widget '{}' could not be found", widget_name))]
WidgetNotFound { widget_name: String }
}
fn foo() -> Result<(), MyError> {
WidgetNotFound { widget_name: "Quux" }.fail()
}
fn main() {
if let Err(e) = foo() {
println!("{}", e);
// The widget 'Quux' could not be found
}
}
Примечание. Я удалил избыточный суффикс Error
в каждом значении перечисления. Также обычно просто вызывать тип Error
и разрешать потребителю префикс типа (mycrate::Error
) или переименовывать его при импорте (use mycrate::Error as FooError
).
1 До внедрения RFC 2504 description
было обязательным методом.