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 хотя бы оставляет сообщение, поэтому в библиотечном коде вместо них возвращают ошибку вызывающему.

Проверьте себя
1. Что произойдёт, если вызвать функцию, возвращающую Result, и никак не использовать её значение?
AПрограмма упадёт с panic в момент вызова
BКомпилятор выдаст предупреждение unused `Result` that must be used
CОшибка будет автоматически напечатана в stderr
DНичего, компилятор такие ситуации не отслеживает
2. Сколько байт на 64-битной платформе занимает Option от Box с i32 внутри?
A8 — работает niche optimization, None кодируется нулевым указателем
B16 — восемь байт под указатель и восемь под тег варианта
C9 — указатель плюс один байт тега
DЗависит от того, инициализирован ли Box
3. Чем expect отличается от unwrap?
Aexpect возвращает Result, а unwrap — значение
Bexpect не паникует, а завершает программу с кодом 1
Cexpect паникует с вашим сообщением, unwrap — со стандартным
Dexpect работает только с Option, unwrap — только с Result