Оператор ? и когда panic уместен
Оператор ? — это не магия, а сахар над ранним возвратом Err с автоматическим преобразованием типа ошибки.
panic — аварийное завершение из-за нарушенного инварианта программы, а не способ сообщить вызывающему коду об ожидаемом сбое.
Вопрос: «Что делает оператор ? и во что он разворачивается?»
Что на самом деле проверяет интервьюер
Не заучили ли вы ? как «сокращение для unwrap». Разница принципиальная: unwrap роняет процесс, а ? отдаёт ошибку наверх, оставляя решение вызывающему.
use std::io;
fn read_config() -> Result<String, io::Error> {
let text = std::fs::read_to_string("config.toml")?;
Ok(text)
}Эквивалент без сахара:
fn read_config() -> Result<String, io::Error> {
let text = match std::fs::read_to_string("config.toml") {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
Ok(text)
}Тот же оператор работает и для Option: на None функция немедленно возвращает None.
fn first_char_upper(s: &str) -> Option<char> {
let c = s.chars().next()?;
Some(c.to_ascii_uppercase())
}Где ? не работает
fn main() {
let text = std::fs::read_to_string("config.toml")?;
println!("{text}");
}Ошибка компиляции:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
--> src/main.rs:2:53
|
1 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
2 | let text = std::fs::read_to_string("config.toml")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
Лечится сменой сигнатуры:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let text = std::fs::read_to_string("config.toml")?;
println!("{text}");
Ok(())
}Если такой main вернёт Err, рантайм напечатает ошибку через Debug (не Display) и завершит процесс с кодом 1:
Вывод:
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }Вопрос: «Как ? умеет превращать одну ошибку в другую?»
Что на самом деле проверяет интервьюер
Знаете ли вы, что за конверсией стоит обычный трейт From. Один ? собирает в одну функцию ошибки ввода-вывода и парсинга — при условии, что вы написали два impl From.
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Io(std::io::Error),
BadPort(ParseIntError),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "не удалось прочитать конфиг: {e}"),
ConfigError::BadPort(e) => write!(f, "порт не является числом: {e}"),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
ConfigError::BadPort(e) => Some(e),
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(e: std::io::Error) -> Self {
ConfigError::Io(e)
}
}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::BadPort(e)
}
}
fn load_port(path: &str) -> Result<u16, ConfigError> {
let raw = std::fs::read_to_string(path)?;
let port = raw.trim().parse::<u16>()?;
Ok(port)
}Реализованный std::error::Error с методом source даёт цепочку причин — по ней инструменты логирования разворачивают «порт не является числом → invalid digit found in string».
Библиотека или приложение
В приложении удобно стирать тип: Box<dyn Error> принимает любую ошибку, реализующую Error, и вам не нужен собственный enum — всё равно всё закончится логом и кодом возврата. В библиотеке так делать нельзя: пользователь должен уметь отличить «файла нет» от «файл битый» и обработать варианты через match. Индустриальный стандарт ровно такой: thiserror для типизированных ошибок библиотек и anyhow для приложений, но обе крейта — это генераторы того кода, который выше написан руками.
Вопрос: «Когда panic! оправдан, а когда надо возвращать Result?»
Что на самом деле проверяет интервьюер
Проведёте ли вы границу «ожидаемый сбой против бага». Файл отсутствует, сеть отвалилась, пользователь ввёл ерунду — это ожидаемо, тут Result. Индекс вышел за границы среза, unwrap на None, целочисленное переполнение в debug-сборке, нарушенный инвариант структуры — это баг программиста, и продолжать работу бессмысленно.
fn percent(part: u32, total: u32) -> u32 {
assert!(total > 0, "total не может быть нулём");
part * 100 / total
}unwind против abort
По умолчанию panic раскручивает стек (unwind): вызываются деструкторы, память освобождается, поток аккуратно умирает. В Cargo.toml это переключается на мгновенное завершение процесса:
[profile.release]
panic = "abort"
abort даёт меньший бинарник и убирает код раскрутки, но лишает вас std::panic::catch_unwind. Сам catch_unwind — не замена try/catch: он нужен на границе FFI (раскрутка через чужой ABI — UB) и в пулах потоков, чтобы упавшая задача не уронила воркер.
Отдельно помните про unwrap в библиотеке: паника внутри чужого кода отбирает у пользователя право решать. Библиотека возвращает Result, а паникует только там, где контракт нарушен явно, — и это документируется секцией «Panics».
Типичные ошибки кандидатов
- Называть
?«сокращением unwrap» — на самом деле это раннийreturn Err, а не паника. - Не знать про
From: «оно само как-то конвертирует» вместо «нуженimpl From<IoError> for MyError». - Пытаться писать
?вmainбезResultи не узнавать ошибку E0277. - Тащить
Box<dyn Error>в публичный API библиотеки, лишая пользователя разбора вариантов. - Считать
catch_unwindаналогомtry/catchи строить на нём бизнес-логику. - Забывать, что при
panic = "abort"никакого перехвата не будет вовсе. - Паниковать в библиотечной функции на пользовательском вводе.
Как ответить кратко
Оператор ? разворачивается в match: на Ok достаёт значение, на Err делает return Err(From::from(e)), то есть попутно конвертирует тип ошибки — для этого нужен impl From. Он же работает с Option, возвращая None, и требует, чтобы функция возвращала подходящий тип: в () это ошибка E0277, поэтому main объявляют как Result<(), Box<dyn Error>>. В приложениях удобно стирать тип ошибки, в библиотеках — держать свой enum с Display и std::error::Error (на практике через thiserror и anyhow). А panic — сигнал о баге и недостижимом состоянии, а не способ сообщить об ожидаемом сбое: библиотека не должна паниковать за пользователя.