Обработка ошибок
В Rust нет исключений. Ошибка — обычное значение, которое функция возвращает, а вызывающий код обязан что-то с ним сделать.
Option: значения может не быть
Вместо null в языке есть тип Option<T> с двумя вариантами: Some(значение) и None. Забыть про пустой случай не получится — код просто не скомпилируется.
fn first_word(text: &str) -> Option<&str> {
text.split_whitespace().next()
}
fn main() {
match first_word("привет мир") {
Some(word) => println!("первое слово: {word}"),
None => println!("строка пустая"),
}
}
Result и оператор ?
Если операция может не удаться, функция возвращает Result<T, E>: либо Ok(результат), либо Err(ошибка). Разбирать каждый вызов через match утомительно, поэтому есть оператор ?. Он достаёт значение из Ok, а при Err сразу возвращает ошибку из текущей функции.
use std::error::Error;
use std::fs;
fn read_port(path: &str) -> Result<u16, Box<dyn Error>> {
let text = fs::read_to_string(path)?; // может вернуть io::Error
let port = text.trim().parse::<u16>()?; // может вернуть ParseIntError
Ok(port)
}
fn main() {
match read_port("port.txt") {
Ok(port) => println!("порт: {port}"),
Err(err) => eprintln!("не удалось прочитать порт: {err}"),
}
}
Тип Box<dyn Error> означает «любая ошибка». Для небольших программ и прототипов этого достаточно: оператор ? сам упакует в него и ошибку ввода-вывода, и ошибку разбора числа.
Собственный тип ошибки
В библиотеке лучше дать вызывающему коду возможность различать причины. Для этого ошибку описывают перечислением и реализуют для него From — тогда ? продолжит работать без единой лишней строки.
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
BadPort(ParseIntError),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "ошибка чтения: {err}"),
ConfigError::BadPort(err) => write!(f, "некорректный порт: {err}"),
}
}
}
impl std::error::Error for ConfigError {}
impl From<io::Error> for ConfigError {
fn from(err: io::Error) -> Self {
ConfigError::Io(err)
}
}
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> Self {
ConfigError::BadPort(err)
}
}
Писать это руками быстро надоедает. В реальных проектах шаблонный код обычно генерирует крейт thiserror, а в приложениях, где различать ошибки не нужно, удобен anyhow: он добавляет к ошибке контекст и цепочку причин.
Полезные методы
| Метод | Что делает |
|---|---|
unwrap_or(x) | Возвращает значение или запасной вариант x. |
unwrap_or_default() | Значение или значение по умолчанию для типа. |
map(f) | Преобразует содержимое Some или Ok, не трогая пустой случай и ошибку. |
and_then(f) | Цепочка операций, каждая из которых может не удаться. |
ok_or(err) | Превращает Option в Result. |
map_err(f) | Преобразует ошибку, например в собственный тип. |
А как же panic?
Паника — для ситуаций, когда продолжать бессмысленно: нарушен инвариант, в программе ошибка. Методы unwrap() и expect("…") паникуют, если значения нет. Они уместны в тестах, примерах и там, где отсутствие значения действительно невозможно. В остальном коде возвращайте Result: решать, что делать с ошибкой, должен тот, кто вызвал функцию.