Как вы определяете пользовательские типы ошибок в Rust?

Я пишу функцию, которая может вернуть несколько ошибок.

fn foo(...) -> Result<..., MyError> {}

Мне, вероятно, нужно будет определить свой собственный тип ошибки для представления таких ошибок. Я предполагаю, что это будет enum возможных ошибок, причем некоторые из вариантов enum имеют связанные с ними диагностические данные:

enum MyError {
    GizmoError,
    WidgetNotFoundError(widget_name: String)
}

Это самый идиоматический способ этого? И как мне реализовать признак Error?

Ответ 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 было обязательным методом.

Ответ 2

Это самый идиоматический способ этого? И как мне реализовать признак ошибки?

Это обычный способ, да. "Идиоматический" зависит от того, насколько сильно вы набрали нужные вам ошибки, и как вы хотите, чтобы это взаимодействовало с другими вещами.

И как мне реализовать признак ошибки?

Строго говоря, вам здесь не нужно. Вы могли бы взаимодействовать с другими вещами, для которых требуется Error, но так как вы определили свой тип возвращаемого значения как это перечисление напрямую, ваш код должен работать без него.

Ответ 3

Ящик custom_error позволяет определять пользовательские типы ошибок с меньшим количеством образцов, чем было предложено выше:

custom_error!{MyError
     Io{source: io::Error}             = "input/output error",
     WidgetNotFoundError{name: String} = "could not find widget '{name}'",
     GizmoError                        = "A gizmo error occurred!"
}

Отказ от ответственности: я автор этого ящика.