panic, unwrap и expect в Rust
Про панику спрашивают, чтобы понять твоё чувство границы: что считать багом, а что нормальным сбоем. Плюс механика - unwind против abort, поведение в потоках, отравление мьютексов.
Стержень: паника это сигнал о нарушенном инварианте программы, а не способ вернуть ошибку.
// Формулировки: «когда допустим unwrap?», «что происходит при панике?», «что увидит join, если поток паниковал?»
unwrap, expect и граница применимости
unwrap разворачивает значение, а при Err или None паникует со стандартным текстом. expect делает то же с вашим сообщением, и это почти всегда лучше: в лог попадает нарушенное ожидание, а не «called unwrap on a None value».
Граница простая. Файл, сеть, пользовательский ввод - ожидаемые сбои, им место в Result. Индекс за границей, невозможное состояние, отсутствие обязательного конфига на старте - нарушенный инвариант, и продолжать работу опаснее, чем упасть. Поэтому библиотеки почти всегда возвращают Result: решать, падать ли, должен вызывающий.
// В сообщении expect пишут ожидание, а не диагноз: «конфиг должен быть прочитан на старте» полезнее, чем «ошибка чтения».
- unwrap
- развернуть значение или паниковать
- expect
- то же со своим сообщением о нарушенном ожидании
Unwind, abort и границы потоков
По умолчанию паника разворачивает стек: идёт вверх по кадрам, вызывая деструкторы, файлы закрываются, блокировки снимаются. Дойдя до границы потока, она его завершает. Профиль с panic = abort обрывает процесс мгновенно: бинарник меньше, сборка быстрее, но деструкторы не отрабатывают.
Через границу потока паника не проходит: join() возвращает Err с упакованной причиной, а процесс продолжает жить. Поэтому пул воркеров переживает падение одной задачи. Побочный эффект - отравление: мьютекс, удерживаемый упавшим потоком, помечается, и следующий lock() возвращает Err.
// catch_unwind ловит панику внутри потока это страховка на границе сервера, чтобы один запрос не уронил процесс. Механизмом обработки ошибок он не является: состояние после паники под вопросом, а в режиме abort ловить вообще нечего.
- unwind
- разворачивание стека с вызовом деструкторов
- отравление
- пометка мьютекса после паники под блокировкой
Как отвечать: «Когда unwrap в проде допустим?»
Когда нарушение условия означает баг, а не внешний сбой. Разбор зашитой в код константы, обязательный конфиг на старте сервиса, инварианты внутри модуля, тесты - там паника уместна, потому что продолжать работу с испорченным состоянием опаснее, чем упасть. Всё, что зависит от внешнего мира - ввод пользователя, сеть, файлы, база, обязано возвращать Result. И даже там, где паника уместна, я пишу expect с сообщением про нарушенное ожидание: по логу сразу видно, какое условие сломалось.
Ты даёшь критерий вместо списка правил и добавляешь практическую деталь про expect. Это ответ, по которому видно, что человек читал свои логи в проде.
На чём валятся
- −Ставят unwrap на пользовательский ввод и сетевые вызовы.
- −Считают catch_unwind заменой обработке ошибок.
- −Думают, что паника валит процесс. Она валит поток; процесс умирает, только если это main.
- −Забывают, что при panic = abort деструкторы не отработают и буферы не сбросятся.
- −Не знают про отравление мьютекса и глушат его unwrap, не подумав о целостности данных.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Что происходит при панике по умолчанию (panic = "unwind")?A)Стек разворачивается, деструкторы отрабатываютB)Поток засыпает, а обработчик паники решает, продолжать лиC)Процесс немедленно завершается без вызова деструкторовD)Паника превращается в Err и возвращается вызывающей функции
показать ответ и разбор
+A)Стек разворачивается, деструкторы отрабатывают// разбор: Разворачивание идёт по кадрам стека вверх и вызывает Drop для всех живых значений — файлы закрываются, блокировки снимаются. Дойдя до границы потока, паника его завершает. Альтернатива panic = abort экономит код и время сборки, но обрывает процесс мгновенно, деструкторы не отрабатывают.
- Рабочий поток паникует. Что увидит поток, который его ждёт через join()?A)Панику в своём потоке: она пробрасывается через границуB)Ok со значением по умолчанию для типа результатаC)Err с упакованной причиной паники — процесс при этом живD)Ничего: join заблокируется навсегда, поток не завершился штатно
показать ответ и разбор
+C)Err с упакованной причиной паники — процесс при этом жив// разбор: Паника не переходит границу потока: она разворачивает свой стек и превращается в Err(Box<dyn Any>) на стороне join. Главный поток решает, что делать — повторить работу, залогировать, упасть самому. Именно поэтому пул воркеров переживает панику отдельной задачи, а мьютекс, захваченный упавшим потоком, помечается отравленным.
- Веб-сервер ловит панику обработчика через catch_unwind. Почему это не замена обработке ошибок?A)catch_unwind не компилируется в многопоточном кодеB)Перехват паники стоит дороже, чем возврат ResultC)После перехвата процесс всё равно завершится через несколько секундD)Состояние после паники не гарантировано, а abort не поймать
показать ответ и разбор
+D)Состояние после паники не гарантировано, а abort не поймать// разбор: catch_unwind — страховка на границе (не уронить весь сервер из-за одного запроса), а не механизм управления потоком. Инварианты после паники могли поплыть, замок мог остаться отравленным, а сборка с panic = abort вообще не даст ничего поймать. Ожидаемые сбои по-прежнему возвращают Result.
- Где unwrap в проде обычно допустим?A)В обработчиках HTTP-запросов: паника вернёт клиенту 500B)Там, где нарушение условия — это баг: константа, старт сервисаC)При разборе пользовательского ввода: неверные данные — вина клиентаD)В коде, покрытом тестами: тесты поймают ошибку заранее
показать ответ и разбор
+B)Там, где нарушение условия — это баг: константа, старт сервиса// разбор: unwrap — заявление «этого не может быть, а если случилось, продолжать нельзя»: разбор зашитой в код константы, старт сервиса без обязательного конфига, тесты. Всё, что зависит от внешнего мира — ввод, сеть, файлы, — обязано возвращать Result: там сбой это не баг, а обычный сценарий.
- Что произойдёт при выполнении?
let v: Vec<i32> = Vec::new(); println!("{:?}", v.first()); println!("{}", v[0]);A)Паника уже на first(): пустой вектор так не опрашиваютB)None, затем 0: пустой вектор отдаёт значение по умолчаниюC)None, затем паника: индекс за границейD)Не соберётся: компилятор видит индексацию пустого векторапоказать ответ и разбор
+C)None, затем паника: индекс за границей// разбор: first возвращает Option и на пустом векторе честно отдаёт None — отсутствие элемента здесь ожидаемый исход, а не ошибка. Индексация же обещает элемент, поэтому при выходе за границу паникует: index out of bounds, the len is 0 but the index is 0. Длину компилятор в общем случае не знает, а безопасный аналог индексации — get(0), который тоже вернёт None.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.