Как правильно создавать и использовать пользовательские типы ошибок в Rust?
Правильный дизайн пользовательских ошибок в Rust
Вы на верном пути! Проблема в том, что ваш ConfigError не реализует преобразование из std::io::Error, поэтому оператор ? не может автоматически конвертировать типы. Давайте разберем правильный подход.
Шаг 1: Структурированный enum с данными
Вместо простых вариантов, добавляйте вложенные данные:
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum ConfigError {
Io(io::Error),
Parse { line: usize, message: String },
UnknownParam(String),
}
Шаг 2: Реализация трейтов Display и Error
Трейт Display нужен для пользовательского сообщения, а Error — для совместимости со стандартной обработкой ошибок:
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "Ошибка ввода-вывода: {}", err),
ConfigError::Parse { line, message } => {
write!(f, "Синтаксическая ошибка в строке {}: {}", line, message)
}
ConfigError::UnknownParam(param) => {
write!(f, "Неизвестный параметр: {}", param)
}
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(err) => Some(err), // Позволяет получить цепочку ошибок
_ => None,
}
}
}
Шаг 3: Реализация From для автоматической конвертации
Чтобы оператор ? работал, реализуйте From для стандартных типов:
impl From for ConfigError {
fn from(err: io::Error) -> ConfigError {
ConfigError::Io(err)
}
}
Теперь ваш код будет работать:
fn read_config(path: &str) -> Result<String, ConfigError> {
let mut file = File::open(path)?; // Автоматически конвертируется в ConfigError::Io
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Шаг 4: Использование в функциях с парсингом
Добавим реальный парсинг с ошибками:
fn parse_line(line: &str, line_num: usize) -> Result<(), ConfigError> {
if line.trim().is_empty() {
return Ok(());
}
if !line.contains('=') {
return Err(ConfigError::Parse {
line: line_num,
message: "Ожидается формат 'ключ=значение'".into(),
});
}
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts[0].trim().is_empty() {
return Err(ConfigError::Parse {
line: line_num,
message: "Пустое имя параметра".into(),
});
}
Ok(())
}
fn parse_config(path: &str) -> Result<(), ConfigError> {
let contents = read_config(path)?;
for (i, line) in contents.lines().enumerate() {
parse_line(line, i + 1)?;
}
Ok(())
}
Шаг 5: Использование крейта thiserror (рекомендуется для продакшна)
Хотя вы просили без крейтов, вот как это упрощается с thiserror:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Ошибка ввода-вывода: {0}")]
Io(#[from] io::Error), // Автоматически реализует From и source()
#[error("Синтаксическая ошибка в строке {line}: {message}")]
Parse { line: usize, message: String },
#[error("Неизвестный параметр: {0}")]
UnknownParam(String),
}
Этот код делает то же самое, что и ручная реализация выше, но в 10 раз короче.
Ключевые принципы:
- Всегда реализуйте Display и Debug — это минимальное требование для ошибок
- Реализуйте Error — для совместимости с
dyn Errorи цепочками ошибок - Используйте From — для автоматической конвертации через
? - Храните контекст — номер строки, имя файла и т.д.
- Для вложенных ошибок — используйте Box
или enum с source()
Альтернативный подход: использование обобщенного типа ошибки
Иногда не хочется создавать полноценный enum для каждой библиотеки. Можно использовать обобщенный тип с динамической ошибкой:
use std::fmt;
use std::error::Error as StdError;
#[derive(Debug)]
pub struct MyError {
kind: String,
message: String,
source: Option>,
}
impl MyError {
pub fn new(kind: &str, message: &str) -> Self {
Self {
kind: kind.to_string(),
message: message.to_string(),
source: None,
}
}
pub fn with_source(mut self, source: impl StdError + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.kind, self.message)
}
}
impl StdError for MyError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &(dyn StdError + 'static))
}
}
// Использование:
fn read_file(path: &str) -> Result<String, MyError> {
std::fs::read_to_string(path)
.map_err(|e| MyError::new("IO", "Не удалось прочитать файл")
.with_source(e))
}
Этот подход удобен для прототипов или когда типы ошибок меняются часто. Недостаток — теряется строгая типизация, и пользователь вашей библиотеки не сможет легко обработать конкретные варианты ошибок через match. Для библиотек лучше использовать enum, как описано в первом ответе.
Ответов пока нет.
Войдите чтобы оставить ответ