Перейти к содержимому

Паника (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 backtrace

Rust сообщает, что поток main запаниковал в файле src/main.rs, в строке 2, столбце 5, показывает твоё сообщение и подсказывает, что для трассировки стека (backtrace) нужно задать переменную окружения RUST_BACKTRACE=1. Число в скобках, здесь (12345), — это идентификатор потока. При каждом запуске оно будет другим, а старые версии Rust его вообще не выводят.

▶ Попробуй сам

При запуске программа выводит это сообщение и останавливается — ничего после 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 (о нём — на предыдущей странице) — для неудач, которых вызывающий код вполне может ожидать и захотеть обработать: неправильный ввод пользователя, отсутствующий файл, неудачный сетевой запрос.
// Неправильный ввод от пользователя — от этого можно восстановиться, используем Result
fn 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() и у 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» («ещё не реализовано»). Он удобен как временное тело функции, пока ты набрасываешь её и собираешься дописать позже.

Есть значение из настроек: 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}");
}

Вывод:

Слушаем порт 8080
▶ Попробуй сам

Напиши 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}");
}

Вывод:

Каждый платит 30
▶ Попробуй сам
  • panic! немедленно останавливает программу с сообщением об ошибке.
  • Некоторые стандартные операции (неправильный индекс массива, .unwrap() у None, деление на ноль) паникуют автоматически.
  • Используй panic! для ошибок в программе и инвариантов, нарушение которых невозможно; Result — для неудач, которые вызывающий код может разумно обработать.
  • .expect("сообщение") похож на .unwrap(), но с сообщением, которое упрощает отладку паники.
  • assert!, assert_eq! и unreachable! намеренно паникуют, когда то, что ты считал правдой, оказывается ложью.

Проверь себя

1. Что делает вызов panic!?

2. Когда, по простому правилу с этой страницы, стоит использовать panic!, а не Result?

3. Чем .unwrap() отличается от .expect('сообщение')?

Результат: 0 / 3