Паника (panic) в Rust
Паника (panic) — это способ Rust немедленно остановить программу, когда что-то пошло не так и исправить это уже нельзя. Если ты повторял примеры с прошлых страниц, ты, скорее всего, уже вызывал панику, сам того не желая: обращение за конец массива или .unwrap() у None — оба паникуют.
Вызываем панику вручную
Заголовок раздела «Вызываем панику вручную»fn main() { panic!("что-то пошло совсем не так");}thread 'main' (12345) panicked at src/main.rs:2:5:что-то пошло совсем не такnote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceRust сообщает, что поток main запаниковал в файле src/main.rs, в строке 2, столбце 5, показывает твоё сообщение и подсказывает, что для трассировки стека (backtrace) нужно задать переменную окружения RUST_BACKTRACE=1. Число в скобках, здесь (12345), — это идентификатор потока. При каждом запуске оно будет другим, а старые версии Rust его вообще не выводят.
При запуске программа выводит это сообщение и останавливается — ничего после panic! не выполняется.
Что паникует без явного вызова panic!
Заголовок раздела «Что паникует без явного вызова panic!»Некоторые операции, с которыми ты уже знаком, паникуют сами, если что-то не так. В каждом примере ниже опасное значение приходит через параметр функции, а не записано прямо в коде. Если индекс или 0 стоят прямо в коде, компилятор часто ещё при компиляции понимает, что операция всегда завершится неудачей, и вообще отказывается собирать программу, вместо того чтобы дать ей запуститься и запаниковать.
fn get(numbers: &[i32], index: usize) -> i32 { numbers[index] // паника: index out of bounds (индекс за пределами)}
fn main() { let numbers = [1, 2, 3]; println!("{}", get(&numbers, 10));}fn main() { let value: Option<i32> = None; println!("{}", value.unwrap()); // паника: called `Option::unwrap()` on a `None` value}fn divide(a: i32, b: i32) -> i32 { a / b // паника: attempt to divide by zero (попытка деления на ноль)}
fn main() { let a = 10; let b = 0; println!("{}", divide(a, b));}Во всех этих случаях Rust отказывается продолжать программу в состоянии, в котором нет смысла, вместо того чтобы молча вернуть мусор.
panic! или Result
Заголовок раздела «panic! или Result»Это настоящее решение, которое тебе придётся принимать часто: должна ли эта неудача обрушить программу, или вызывающий код должен получить шанс её обработать?
panic!— для ошибок в программе и ситуаций, которые никогда не должны случаться, если код правильный. Разумного способа восстановиться нет, поэтому самое безопасное — остановиться сразу, пока не стало хуже.Result(о нём — на предыдущей странице) — для неудач, которых вызывающий код вполне может ожидать и захотеть обработать: неправильный ввод пользователя, отсутствующий файл, неудачный сетевой запрос.
// Неправильный ввод от пользователя — от этого можно восстановиться, используем Resultfn parse_age(input: &str) -> Result<u32, String> { input.parse().map_err(|_| String::from("некорректный возраст"))}
// Инвариант, нарушение которого невозможно, если код правильный, — паникаfn get_first(numbers: &[i32]) -> i32 { if numbers.is_empty() { panic!("get_first вызвана с пустым срезом"); } numbers[0]}unwrap() и expect() — короткий путь к панике
Заголовок раздела «unwrap() и expect() — короткий путь к панике»Ты уже видел .unwrap() и у Option, и у Result. На самом деле это просто сокращение для «дай мне значение или запаникуй, если его нет». .expect("сообщение") делает то же самое, но позволяет приложить более полезное сообщение:
fn main() { let input = "42"; let number: i32 = input.parse().expect("во входных данных должно быть число"); println!("{number}");}Если разбор не удастся, в сообщение паники попадёт твой текст. Отлаживать так гораздо легче, чем с общим сообщением, которое даёт .unwrap().
Проверяем свои предположения: assert!, assert_eq! и unreachable!
Заголовок раздела «Проверяем свои предположения: assert!, assert_eq! и unreachable!»Иногда ты знаешь, что в определённом месте кода что-то обязательно должно быть правдой. В Rust есть несколько маленьких макросов, которые превращают такую уверенность в проверку. Если проверка не проходит, они паникуют с понятным сообщением, и ошибка проявляется прямо там, где возникла, а не приводит к странным результатам позже.
assert!(условие, "сообщение")паникует, если условиеfalse.assert_eq!(left, right)паникует, если два значения не равны, и выводит оба.unreachable!("сообщение")отмечает код, который никогда не должен выполняться, например веткуmatchдля невозможного значения.
fn average(numbers: &[i32]) -> i32 { assert!(!numbers.is_empty(), "для среднего нужно хотя бы одно число");
let mut total = 0; for n in numbers { total += n; } total / numbers.len() as i32}
fn day_name(day: u8) -> &'static str { match day { 1 => "понедельник", 2 => "вторник", 3 => "среда", 4 => "четверг", 5 => "пятница", 6 => "суббота", 7 => "воскресенье", _ => unreachable!("день должен быть от 1 до 7, а пришло {day}"), }}
fn main() { let avg = average(&[70, 80, 90]); assert_eq!(avg, 80); println!("среднее: {avg}");
println!("день 3 — {}", day_name(3));}Вывод:
среднее: 80день 3 — средаЗдесь все проверки проходят, и программа работает как обычно. А теперь поменяй в Playground список на [70, 80, 95]. Среднее станет 81, и assert_eq! остановит программу, показав оба значения рядом:
thread 'main' (12345) panicked at src/main.rs:26:5:assertion `left == right` failed left: 81 right: 80Сообщение значит «проверка left == right не прошла»: слева 81, справа 80. Попробуй ещё average(&[]) или day_name(9) — и увидишь в панике свои собственные сообщения.
Эти проверки работают всегда, даже в release-сборке, так что пусть они будут дешёвыми. С assert_eq! ты снова встретишься на странице Тестирование: там это главный способ, которым тест говорит «это должно быть равно тому». Есть ещё todo!(), который паникует с сообщением «not yet implemented» («ещё не реализовано»). Он удобен как временное тело функции, пока ты набрасываешь её и собираешься дописать позже.
Практика
Заголовок раздела «Практика»1. Номер порта и expect
Заголовок раздела «1. Номер порта и expect»Есть значение из настроек: let port_text = "8080";. Преврати его в u16 через .parse() и .expect(...) с сообщением, которое объясняет, что пошло не так. Затем выведи Слушаем порт 8080. Подсказка: переменной нужен тип, let port: u16 = ..., чтобы parse знал, что получить. Потом поменяй текст на "80a0", чтобы увидеть своё сообщение в панике.
Показать решение
fn main() { let port_text = "8080"; let port: u16 = port_text .parse() .expect("порт в настройках должен быть числом от 0 до 65535"); println!("Слушаем порт {port}");}Вывод:
Слушаем порт 80802. Делим счёт
Заголовок раздела «2. Делим счёт»Напиши fn split_bill(total: u32, people: u32) -> u32, которая возвращает долю каждого. Делить счёт на ноль человек — это ошибка в вызывающем коде, поэтому поймай её через assert! с понятным сообщением. Выведи долю для суммы 120 и 4 человек. Подсказка: поставь assert! первой строкой функции, до деления.
Показать решение
fn split_bill(total: u32, people: u32) -> u32 { assert!(people > 0, "для split_bill нужен хотя бы один человек"); total / people}
fn main() { let share = split_bill(120, 4); println!("Каждый платит {share}");}Вывод:
Каждый платит 30panic!немедленно останавливает программу с сообщением об ошибке.- Некоторые стандартные операции (неправильный индекс массива,
.unwrap()уNone, деление на ноль) паникуют автоматически. - Используй
panic!для ошибок в программе и инвариантов, нарушение которых невозможно;Result— для неудач, которые вызывающий код может разумно обработать. .expect("сообщение")похож на.unwrap(), но с сообщением, которое упрощает отладку паники.assert!,assert_eq!иunreachable!намеренно паникуют, когда то, что ты считал правдой, оказывается ложью.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что делает вызов panic!?
2. Когда, по простому правилу с этой страницы, стоит использовать panic!, а не Result?
3. Чем .unwrap() отличается от .expect('сообщение')?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- Неустранимые ошибки с panic! — книга «Язык программирования Rust»: что происходит во время паники, раскрутка стека (unwinding) и как читать трассировку (перевод на русский).
- panic! или не panic! — книга «Язык программирования Rust»: официальные рекомендации, как выбирать между
panic!иResult, с примерами (перевод на русский). - panic! — Rust на примерах: короткий пример паники, который показывает, что память всё равно освобождается при раскрутке стека (перевод на русский).
assert_eq!— документация std (на английском): как работают макросы проверок, включая собственные сообщения.panic!— документация std (на английском): справка по самомуpanic!, его связь сunwrapи когда паника — правильный инструмент.