Option и Result вместо исключений
В Rust ошибка — это не исключение, а обычное значение, которое компилятор не даст молча выбросить.
Option<T> — тип «значение либо есть, либо его нет»; Result<T, E> — тип «операция либо дала результат, либо провалилась с ошибкой E».
Вопрос: «Почему в Rust нет исключений и как тогда сообщать об ошибке?»
Что на самом деле проверяет интервьюер
Хочет услышать не «в Rust так решили», а понимание цены исключений: невидимый поток управления. В языке с try/catch любая строка потенциально прыгает наружу функции, а сигнатура об этом молчит. Rust переносит эту информацию в тип возвращаемого значения — ошибка становится частью контракта, который читается глазами и проверяется компилятором.
Ошибка живёт в сигнатуре
use std::num::ParseIntError;
fn parse_port(raw: &str) -> Result<u16, ParseIntError> {
match raw.parse::<u16>() {
Ok(port) => Ok(port),
Err(e) => Err(e),
}
}
fn main() {
println!("{:?}", parse_port("8080"));
println!("{:?}", parse_port("8o8o"));
}Вывод:
Ok(8080)
Err(ParseIntError { kind: InvalidDigit })Никакого стека раскрутки: вызывающий получает обычное перечисление и обязан решить, что с ним делать. Это тот же enum и тот же match, что вы уже разбирали, — отдельного механизма исключений в языке просто нет.
Проигнорировать Result нельзя молча
Result помечен атрибутом #[must_use], поэтому «забытая» ошибка — это диагностика компилятора, а не тихий баг в проде.
use std::fs::File;
fn main() {
File::create("log.txt");
}Предупреждение компилятора:
warning: unused `Result` that must be used
--> src/main.rs:4:5
|
4 | File::create("log.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = File::create("log.txt");
| +++++++
В командах, где включён #![deny(warnings)] или -D warnings в CI, это уже не предупреждение, а красная сборка.
Вопрос: «В чём разница между Option и Result?»
Что на самом деле проверяет интервьюер
Различаете ли вы «отсутствие значения» и «сбой». Option<T> — про допустимое пустое состояние: в словаре нет ключа, у пользователя не заполнено отчество, итератор кончился. Result<T, E> — про операцию, которая могла не получиться, и тогда важно, почему: в E лежит причина.
Ноль стоимости за null safety
Option заменяет null, но не платит за это памятью: для указателей и ссылок работает niche optimization — компилятор использует заведомо невозможное значение (нулевой адрес) как метку None.
use std::mem::size_of;
fn main() {
println!("{}", size_of::<&i32>());
println!("{}", size_of::<Option<&i32>>());
println!("{}", size_of::<Box<i32>>());
println!("{}", size_of::<Option<Box<i32>>>());
}Вывод:
8 8 8 8
Как разбирать: match, if let, let else
fn greet(name: Option<&str>) -> String {
let Some(name) = name else {
return String::from("Привет, гость");
};
format!("Привет, {name}")
}
fn log_port(port: Option<u16>) {
if let Some(p) = port {
println!("слушаем {p}");
}
}let else хорош тем, что убирает лестницу вложенности: «плохая» ветка обязана разойтись (return, continue, panic!), а дальше код пишется на уже развёрнутом значении.
Комбинаторы вместо ручного match
fn port_or_default(raw: Option<&str>) -> u16 {
raw.and_then(|s| s.parse::<u16>().ok())
.filter(|p| *p != 0)
.unwrap_or(8080)
}Полезный минимум: map меняет значение внутри, and_then — когда функция сама возвращает Option/Result, unwrap_or подставляет готовую заглушку, unwrap_or_else — вычисляет её лениво, unwrap_or_default берёт Default::default(), ok_or превращает Option в Result, а ok() — наоборот.
Вопрос: «Чем unwrap отличается от expect и почему их ругают в code review?»
Что на самом деле проверяет интервьюер
Понимаете ли вы, что unwrap — это не «получить значение», а «утверждаю: ошибки быть не может, иначе роняй процесс».
fn main() {
let raw = "8o8o";
let port: u16 = raw.parse().expect("PORT должен быть числом");
println!("{port}");
}Вывод:
thread 'main' panicked at src/main.rs:3:32:
PORT должен быть числом: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceС unwrap() в этой же строке было бы безликое called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit } — по такому логу дежурный не поймёт, какой из десяти unwrap сработал. Отсюда правило ревью: если уж утверждаете инвариант, объясните его через expect. Уместны оба в прототипах, тестах, примерах и там, где невозможность ошибки доказана рядом стоящим кодом.
Типичные ошибки кандидатов
- Говорить «Result — это как исключение, только вручную», не упомянув главное: он виден в сигнатуре и проверяется компилятором.
- Считать, что
Option— это обёртка с накладными расходами; про niche optimization не слышали. - Использовать
Optionтам, где нужна причина сбоя: вызывающий получаетNoneи не знает, файл не найден или прав не хватило. - Путать
unwrap_orиunwrap_or_else— первый вычисляет запасное значение всегда, даже на успешном пути. - Утверждать, что
unwrap«просто нельзя»: в тестах и прототипах это нормальный инструмент, вопрос в контексте. - Забывать, что игнорирование
Result— предупреждение, а не ошибка, и в CI с-D warningsоно валит сборку.
Как ответить кратко
В Rust ошибка — обычное значение, а не скрытый прыжок по стеку: функция объявляет Result<T, E>, и обработка становится частью контракта. Option<T> отвечает на вопрос «значение вообще есть?» и заменяет null бесплатно за счёт niche optimization, а Result<T, E> — на вопрос «операция удалась и если нет, то почему?». Разбирать их можно через match, if let, let else или комбинаторы вроде map, and_then, unwrap_or. unwrap и expect паникуют; expect хотя бы оставляет сообщение, поэтому в библиотечном коде вместо них возвращают ошибку вызывающему.