Ты уже что-то искал раньше - в словаре, в телефонной книге, в базе данных. Иногда успешно, а иногда
нет. В большинстве языков ты просто пожимаешь плечами и идёшь дальше: получаешь значение - или
получаешь 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-указатель тремя файлами дальше.
Option<T>- архив отвечает на вопрос “оно вообще есть?” (Some/None)Result<T, E>- архив отвечает на вопрос “что пошло не так?” (Ok/Err(reason))- Ни одна коробка не открывается по предположению. Компилятор проверяет за тебя, на этапе компиляции, каждый раз.