Правильная обработка ошибок Rust (автоматическое преобразование из одного типа ошибки в другое с вопросительным знаком)

Я хочу узнать, как правильно бороться с ошибками в Rust. Я прочитал книгу и этот пример; Теперь я хотел бы знать, как мне следует устранять ошибки в этой функции:

fn get_synch_point(&self) -> Result<pv::synch::MeasPeriods, reqwest::Error> {
    let url = self.root.join("/term/pv/synch"); // self.root is url::Url
    let url = match url {
        Ok(url) => url,
        // ** this err here is url::ParseError and can be converted to Error::Kind https://docs.rs/reqwest/0.8.3/src/reqwest/error.rs.html#54-57 **//
        Err(err) => {
            return Err(Error {
                kind: ::std::convert::From::from(err),
                url: url.ok(),
            })
        }
    };

    Ok(reqwest::get(url)?.json()?) //this return reqwest::Error or convert to pv::sych::MeasPeriods automaticly
}      

этот код неправильный; это вызывает ошибку компиляции:

error[E0451]: field 'kind' of struct 'reqwest::Error' is private
  --> src/main.rs:34:42
   |
34 |             Err(err) => return Err(Error{kind: ::std::convert::From::from(err), url: url.ok()})
   |                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field 'kind' is private

error[E0451]: field 'url' of struct 'reqwest::Error' is private
  --> src/main.rs:34:81
   |
34 |             Err(err) => return Err(Error{kind: ::std::convert::From::from(err), url: url.ok()})
   |                                                                                 ^^^^^^^^^^^^^ field 'url' is private

Каков правильный порядок действий в этом случае? Для меня reqwest::Error в данном случае является хорошим решением, поэтому я бы хотел избежать определения собственного типа ошибки:

enum MyError {
    Request(reqwest::Error),
    Url(url::ParseError) // this already a part of request::Error::Kind!!!
} 

Ответ 1

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

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

#[macro_use] extern crate custom_error;

custom_error!{ MyError
    Request{source: reqwest::Error} = "request error",
    Url{source: url::ParseError}    = "invalid url"
}

Ответ 2

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

  1. Объявите свое собственное пользовательское перечисление со всеми ошибками, с которыми работает ваше приложение (или с одной подсистемой вашего приложения; степень детализации сильно зависит от проекта), и объявите преобразования From из всех ошибок, с которыми вы работаете, в этот тип перечисления.

    В качестве расширения этого подхода вы можете использовать error-chain (или quick-error, на котором в основном основана цепочка ошибок) для генерации таких пользовательских типов и преобразований в полу -автоматический способ.

  2. Используйте специальный общий тип ошибки. В основном их два:

    а. Box<Error> где Error определено в стандартной библиотеке.

    б. Используйте тип Error, определенный в ящике failure.

    Тогда оператор вопросительного знака сможет преобразовать любую совместимую ошибку в один из этих типов из-за различных реализаций черт Into и From.

Обратите внимание, что ящик failure предназначен для определения ошибок, возникающих в сообществе Rust. Он не только предоставляет общий тип ошибки и черту (которая устраняет различные проблемы с чертой std::error::Error; см., Например, здесь), но также имеет средства для определения собственных типов ошибок (например, с помощью failure_derive), а также для отслеживания контекста ошибки, причин и создания трассировки. Кроме того, он старается быть максимально совместимым с существующими подходами к обработке ошибок, поэтому его можно легко использовать для интеграции с библиотеками, которые используют другие, более старые подходы (std::error::Error, error-chain, quick-error). Поэтому я настоятельно рекомендую вам сначала использовать этот ящик, а не другие варианты.

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

  1. Определите тип Result:

    type Result<T> = std::result::Result<T, failure::Error>;
    
  2. Используйте Result<Something> везде, где можно вернуть ошибку, используя оператор вопросительного знака (?) для преобразования между ошибками и функциями, такими как err_msg или format_err! или bail! для создания собственных сообщений об ошибках.

Мне еще предстоит написать библиотеку с использованием failure, но я думаю, что для библиотек было бы важно создать более конкретные ошибки, объявленные как enum, что можно сделать с помощью ящика failure_derive. Для приложений, однако, тип failure::Error более чем достаточно.

Ответ 3

Как уже говорилось Владимир Матвеев, провал ящик должен быть отправной точкой. Вот мое решение:

use std::io;
use std::result;

use failure::{Backtrace, Fail};

/// This is a new error type manged by Oxide library.
/// The custom derive for Fail derives an impl of both Fail and Display.
#[derive(Debug, Fail)]
pub enum OxideError {
    #[fail(display = "{}", message)]
    GeneralError { message: String },

    #[fail(display = "{}", message)]
    IoError {
        message: String,
        backtrace: Backtrace,
        #[cause]
        cause: io::Error,
    },
}

/// Create general error
pub fn general(fault: &str) -> OxideError {
    OxideError::GeneralError {
        message: String::from(fault),
    }
}

/// Create I/O error with cause and backtrace
pub fn io(fault: &str, error: io::Error) -> OxideError {
    OxideError::IoError {
        message: String::from(fault),
        backtrace: Backtrace::new(),
        cause: error,
    }
}

Это перечисление ошибок является расширяемым для будущих нужд.

Ответ 4

Обновление 10.10.2019

Ржавчина развивается быстро, поэтому можно добавить новый ответ! Мне очень нравится custom_error, но я думаю, что этот ящик теперь будет моим любимым человеком!

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[source] io::Error),
    #[error("the data for key '{0}' is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

Это позволяет изменить io::Error на DataStoreError::Disconnect со знаком вопроса ?.

источники: crates.io или GitHub