← Свитки rust

Result и Option: Коробка Шредингера

Почему Rust не даст унести результат поиска, не открыв его

Ты уже что-то искал раньше - в словаре, в телефонной книге, в базе данных. Иногда успешно, а иногда нет. В большинстве языков ты просто пожимаешь плечами и идёшь дальше: получаешь значение - или получаешь null, undefined, nil - и язык доверяет тебе не перепутать одно с другим, прежде чем ты пойдёшь дальше. Rust не доверяет на слово. Там, где можно проверить - он проверяет.

Архив

Представь городской архив - равнодушный каменный фасад, архивариус, повидавший любые запросы и ни одним не впечатлённый. Ты подаёшь заявку на книгу. Архивариус не отдаёт книгу. Архивариус отдаёт запечатанную коробку.

Внутри - одно из двух, и узнать какое можно только открыв: либо сама книга, либо записка с объяснением, почему книги нет. Если книгу просто никогда не заказывали, записка говорит об этом прямо - не значится. Если случилось что-то похуже - архив горел в 89-м, гроссбух съела моль

У архива одно правило, и оно не гнётся: что внутри - не угадывают. Коробку открывают. Каждый раз. Спешка архивариуса не волнует.

Rust называет первый вид коробки Option. Второй - Result. Обе запечатаны до открытия; расходятся они только в том, что позволено написать в записке. У Option записка всегда одна фраза: не значится (None). У Result записка обязана объясниться (Err, с причиной внутри).

Привычка из прошлой жизни

В большинстве других языков ты просто тянешься в коробку - found.title - и веришь, что там что-то разумное. Если поиск не удался, назад прилетает null, и либо проблема всплывает сразу, в момент выполнения, либо - что ещё хуже - язык молча подсовывает мусорное значение, а узнаёшь ты об этом тремя файлами позже. Привычка не глупая. Она же и причина, по которой человек, придумавший null, сам называет его своей ошибкой на миллиард долларов.

struct Book {
    id: u32,
    title: String,
}

fn request_book(id: u32, archive: &[Book]) -> Option<&Book> {
    archive.iter().find(|book| book.id == id)
}

fn main() {
    let archive = vec![Book { id: 1, title: String::from("Reaper Man") }];

    let found = request_book(2, &archive);
    println!("Book: {}", found.title); // тянешься в коробку, не открыв её
}
error[E0609]: no field `title` on type `Option<&Book>`
  --> src/main.rs:12:32
   |
12 |     println!("Book: {}", found.title);
   |                                ^^^^^ unknown field
   |
   = note: see the API docs for `Option<T>` for methods that unwrap or inspect its contents

Открыть коробку

Компилятор не вредничает. Это архивариус, который не даёт унести запечатанную коробку под мышкой. Option<&Book> - не Book, которая, возможно, отсутствует. Это сама коробка, и Rust не даст добраться до содержимого, не открыв её.

Открой её так, как ожидает архив: сначала прочитай записку, потом действуй по ней.

match request_book(2, &archive) {
    Some(book) => println!("Book: {}", book.title),
    None => println!("No book with that id - not held"),
}

Result открывается так же, но его записка несёт причину вместо пожатия плечами:

fn parse_priority(input: &str) -> Result<u8, String> {
    input.parse().map_err(|e| format!("not a priority: {e}"))
}

match parse_priority("urgent") {
    Ok(priority) => println!("Priority: {priority}"),
    Err(reason) => println!("Bad priority - {reason}"), // записка объясняется сама
}

Что меняется

Ты перестаёшь тянуться в коробку на веру. Каждый match заставляет ответить сразу на оба случая - книга нашлась или нет - и компилятор не даст оставить один из них без ответа. Не остаётся пути, на котором пропавшая книга молча превращается в null-указатель тремя файлами дальше.