match и Option в Rust
Здесь проверяют, умеешь ли ты работать с отсутствием значения без 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, остальные разбираются в тренажёре.
- Для чего нужна конструкция 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: счастливый путь остаётся на верхнем уровне, а выходы из функции стоят рядом с проверками.
- Чем unwrap_or(expensive()) отличается от unwrap_or_else(|| expensive())?A)Первый вернёт ссылку, второй — владеющее значениеB)Разницы нет: компилятор вносит вызов внутрь ветки самC)Первый вычислит expensive() всегда, второй — только при NoneD)Первый паникует при None, второй подставляет значение из замыкания
показать ответ и разбор
+C)Первый вычислит expensive() всегда, второй — только при None// разбор: Аргумент unwrap_or — обычное выражение: оно считается до вызова метода, даже если значение есть и запасной вариант не понадобится. unwrap_or_else берёт замыкание и вызывает его только в ветке None. Для дешёвых констант разницы нет, а для запроса в базу или аллокации она принципиальна.
- Когда для 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.
- Есть let opt: Option<String>. Почему match opt { Some(s) => ..., None => ... } ломает последующее использование opt, а as_ref() — нет?A)as_ref клонирует строку, поэтому оригинал остаётся нетронутымB)match забирает владение разбираемым значениемC)opt после match становится None: разбор опустошает OptionD)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: привязки становятся ссылками.
- Что напечатает код?
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, и оно заканчивается до тела цикла. Это стандартная идиома «разобрать коллекцию до конца».
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.