сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Основы Rust

match и Option в Rust

match, Option и комбинаторы

Здесь проверяют, умеешь ли ты работать с отсутствием значения без null. Спрашивают, почему match обязан покрыть все варианты, чем if let отличается от let else, когда брать map, а когда and_then. Это код, который пишется каждый день, поэтому небрежность видно сразу.

Стержень: пустота выражена типом, разбор обязателен и исчерпывающ, а комбинаторы это тот же разбор, только короче.

// Формулировки: «зачем Option вместо null?», «что делает let ... else?», «чем map отличается от and_then?»

Исчерпывающий разбор это фича

match требует покрыть все варианты, иначе сборка падает. Смысл не в строгости ради строгости: добавил новый вариант в enum, и все места, где его забыли обработать, перестали компилироваться. Компилятор превращается в список задач по рефакторингу.

Отсюда практический совет: в разборе своего enum старайся не ставить ветку _. Она глушит ровно ту диагностику, ради которой всё и затевалось: новый вариант молча уедет в общий случай, и баг всплывёт в проде.

// Для чужих enum с #[non_exhaustive] ветка _ обязательна - там она защищает от поломки при обновлении библиотеки.

исчерпывающий разбор
требование покрыть все варианты типа
non_exhaustive
атрибут, обязывающий внешний код держать ветку _

if let, let else и ранний выход

Когда интересен один вариант, полный match избыточен: if let Some(x) = opt { ... } читается лучше. Но у него есть неприятная особенность - вложенность: три проверки подряд превращаются в лестницу.

Для этого есть let ... else: let Some(x) = opt else { return; }. Значение привязывается к остатку функции, а ветка else обязана расходиться - return, continue, break или panic. Счастливый путь остаётся на верхнем уровне, а все выходы стоят рядом с проверками.

// Тот же приём в мире Result - оператор ?. Разница в том, что ? возвращает ошибку наверх, а else-ветка решает сама, что делать.

let Some(user) = find(id) else {
    return Err(NotFound);
};
println!("{}", user.name);
let ... else
разбор с обязательным расходящимся else
расходящаяся ветка
код, который не возвращает управление: return, break, panic

Комбинаторы и перемещение

map меняет значение внутри Option, and_then нужен, когда замыкание само возвращает Option, иначе получится Option<Option<T>>. unwrap_or подставляет запасное значение, но вычисляет его всегда; unwrap_or_else берёт замыкание и трогает его только при None. Для дорогих значений разница принципиальна.

И отдельная ловушка про владение: match opt { Some(s) => ... } перемещает содержимое, если оно не Copy - дальше переменная недоступна. Спасает as_ref(), который превращает &Option<T> в Option<&T>, или разбор по ссылке: match &opt.

// Ещё есть take(): он забирает значение из Option, оставляя на его месте None. Незаменим, когда поле структуры надо вынуть, не разбирая всю структуру.

let len = opt.as_ref().map(|s| s.len());
let name = opt.unwrap_or_else(|| default());
and_then
склейка уровней, когда замыкание возвращает Option
as_ref
Option<T> → Option<&T>: разбор без перемещения

Как отвечать: «Чем Option лучше null?»

Отсутствие значения становится частью типа, и компилятор не даёт добраться до содержимого, не разобрав его: match, if let, let else или комбинаторы. То есть целый класс ошибок «забыл проверить на null» просто не доходит до рантайма. При этом накладных расходов почти нет: благодаря niche-оптимизации Option<Box<T>> и Option<&T> занимают столько же, сколько сам указатель, None кодируется невозможным нулевым значением.

Ты называешь и гарантию, и её цену. Упоминание niche-оптимизации показывает, что ты понимаешь представление типов, а не только синтаксис.

На чём валятся

  • Разбирают Option по значению и теряют переменную: содержимое перемещено, спасает as_ref.
  • Ставят _ в match по своему enum и лишаются проверки при добавлении варианта.
  • Пишут unwrap_or(дорогой_вызов()) - аргумент вычисляется даже когда значение есть.
  • Путают map и and_then и получают Option<Option<T>>.
  • Считают, что Option это указатель. Это обычный enum, просто с оптимизацией представления.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_match_option1 / 5
    Для чего нужна конструкция let ... else?
    A)Разобрать паттерн и в else-ветке уйти из функции ранним выходом
    B)Заменить if let там, где нужны обе ветки с одинаковым результатом
    C)Задать значение по умолчанию, если паттерн не совпал
    D)Объявить переменную, видимую только внутри else-ветки
    показать ответ и разбор
    +A)Разобрать паттерн и в else-ветке уйти из функции ранним выходом

    // разбор: let Some(x) = opt else { return; } привязывает x к остатку функции, а несовпадение уводит в else, который обязан расходиться — return, continue, break или panic. Это убирает лестницу вложенных if let: счастливый путь остаётся на верхнем уровне, а выходы из функции стоят рядом с проверками.

  2. #rs_match_option2 / 5
    Чем unwrap_or(expensive()) отличается от unwrap_or_else(|| expensive())?
    A)Первый вернёт ссылку, второй — владеющее значение
    B)Разницы нет: компилятор вносит вызов внутрь ветки сам
    C)Первый вычислит expensive() всегда, второй — только при None
    D)Первый паникует при None, второй подставляет значение из замыкания
    показать ответ и разбор
    +C)Первый вычислит expensive() всегда, второй — только при None

    // разбор: Аргумент unwrap_or — обычное выражение: оно считается до вызова метода, даже если значение есть и запасной вариант не понадобится. unwrap_or_else берёт замыкание и вызывает его только в ветке None. Для дешёвых констант разницы нет, а для запроса в базу или аллокации она принципиальна.

  3. #rs_match_option3 / 5
    Когда для Option берут and_then вместо map?
    A)Когда Option может быть None и требуется значение по умолчанию
    B)Когда замыкание само возвращает Option и вложенность не нужна
    C)Когда замыкание может паниковать и нужен перехват
    D)Когда нужно поменять тип значения внутри Option
    показать ответ и разбор
    +B)Когда замыкание само возвращает Option и вложенность не нужна

    // разбор: map оборачивает результат замыкания: если оно возвращает Option, получится Option<Option<T>>. and_then (он же flat_map по смыслу) ждёт замыкание, возвращающее Option, и склеивает уровни — так цепочка «найти, потом разобрать, потом проверить» остаётся плоской и обрывается на первом None.

  4. #rs_match_option4 / 5
    Есть let opt: Option<String>. Почему match opt { Some(s) => ..., None => ... } ломает последующее использование opt, а as_ref() — нет?
    A)as_ref клонирует строку, поэтому оригинал остаётся нетронутым
    B)match забирает владение разбираемым значением
    C)opt после match становится None: разбор опустошает Option
    D)match по значению перемещает String из opt, as_ref даёт Option<&String>
    показать ответ и разбор
    +D)match по значению перемещает String из opt, as_ref даёт Option<&String>

    // разбор: Разбор по значению перемещает содержимое: String не Copy, поэтому opt считается частично перемещённым и дальше недоступен. as_ref превращает &Option<String> в Option<&String> — внутрь match уезжает ссылка, владение остаётся. Тот же эффект даёт match &opt: привязки становятся ссылками.

  5. #rs_match_option5 / 5
    Что напечатает код?
    let mut v = vec![1, 2, 3];
    while let Some(x) = v.pop() {
        print!("{} ", x);
    }
    println!("{:?}", v);
    A)1 2 3 и пустой вектор
    B)Не соберётся: v заимствована циклом
    C)3 2 1 и пустой вектор
    D)3 2 1 и вектор [1, 2, 3]
    показать ответ и разбор
    +C)3 2 1 и пустой вектор

    // разбор: pop снимает элемент с конца и отдаёт Some, пока вектор не опустеет, а на пустом возвращает None — и while let на этом выходит. Отсюда обратный порядок и пустой вектор в конце. Заимствование не мешает: каждая итерация берёт &mut на время вызова pop, и оно заканчивается до тела цикла. Это стандартная идиома «разобрать коллекцию до конца».

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.