Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nieodwracalne błędy z panic!

Czasami w twoim kodzie dzieją się złe rzeczy i nic nie możesz na to poradzić. W takich przypadkach Rust ma makro panic!. W praktyce istnieją dwa sposoby wywołania paniki: poprzez wykonanie akcji, która powoduje panikę kodu (takiej jak dostęp do tablicy poza jej zakresem) lub poprzez jawne wywołanie makra panic!. W obu przypadkach powodujemy panikę w naszym programie. Domyślnie te paniki wyświetlą komunikat o błędzie, rozwiną stos, wyczyszczą stos i zakończą działanie. Za pomocą zmiennej środowiskowej możesz również sprawić, że Rust wyświetli stos wywołań, gdy wystąpi panika, co ułatwi śledzenie źródła paniki.

Rozwijanie stosu lub przerywanie działania w odpowiedzi na panikę

Domyślnie, gdy wystąpi panika, program zaczyna się rozwijać, co oznacza, że Rust cofa się po stosie i czyści dane z każdej napotkanej funkcji. Jednak cofanie się i czyszczenie to dużo pracy. Rust pozwala zatem wybrać alternatywę natychmiastowego przerwania, które kończy program bez czyszczenia.

Pamięć używana przez program będzie musiała zostać wyczyszczona przez system operacyjny. Jeśli w twoim projekcie musisz sprawić, aby wynikowy plik binarny był jak najmniejszy, możesz przełączyć się z rozwijania na przerwanie po panice, dodając panic = 'abort' do odpowiednich sekcji [profile] w pliku Cargo.toml. Na przykład, jeśli chcesz przerwać działanie po panice w trybie wydania, dodaj to:

[profile.release]
panic = 'abort'

Spróbujmy wywołać panic! w prostym programie:

fn main() {
    panic!("awaria i spalenie");
}

Po uruchomieniu programu zobaczysz coś takiego:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Wywołanie panic! powoduje komunikat o błędzie zawarty w ostatnich dwóch liniach. Pierwsza linia pokazuje nasz komunikat paniki i miejsce w kodzie źródłowym, gdzie nastąpiła panika: src/main.rs:2:5 wskazuje, że jest to druga linia, piąty znak naszego pliku src/main.rs.

W tym przypadku wskazana linia jest częścią naszego kodu, a jeśli przejdziemy do tej linii, zobaczymy wywołanie makra panic!. W innych przypadkach wywołanie panic! może znajdować się w kodzie, który wywołuje nasz kod, a nazwa pliku i numer linii zgłoszone przez komunikat o błędzie będą kodem kogoś innego, gdzie wywołano makro panic!, a nie linią naszego kodu, która ostatecznie doprowadziła do wywołania panic!.

Możemy użyć śledzenia stosu funkcji, z których pochodzi wywołanie panic!, aby ustalić, która część naszego kodu powoduje problem. Aby zrozumieć, jak używać śledzenia stosu panic!, przyjrzyjmy się innemu przykładowi i zobaczmy, jak to wygląda, gdy wywołanie panic! pochodzi z biblioteki z powodu błędu w naszym kodzie, a nie z naszego kodu bezpośrednio wywołującego makro. Listing 9-1 zawiera kod, który próbuje uzyskać dostęp do indeksu w wektorze poza zakresem prawidłowych indeksów.

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

Tutaj próbujemy uzyskać dostęp do 100. elementu naszego wektora (który znajduje się pod indeksem 99, ponieważ indeksowanie zaczyna się od zera), ale wektor ma tylko trzy elementy. W tej sytuacji Rust wywoła panikę. Użycie [] ma zwrócić element, ale jeśli podasz nieprawidłowy indeks, Rust nie mógłby zwrócić żadnego prawidłowego elementu.

W C, próba odczytu poza końcem struktury danych jest niezdefiniowanym zachowaniem. Możesz otrzymać cokolwiek, co znajduje się w miejscu w pamięci, które odpowiadałoby temu elementowi w strukturze danych, mimo że pamięć nie należy do tej struktury. Nazywa się to przepełnieniem bufora i może prowadzić do luk w zabezpieczeniach, jeśli atakujący jest w stanie manipulować indeksem w taki sposób, aby odczytać dane, do których nie powinien mieć dostępu, a które są przechowywane po strukturze danych.

Aby chronić program przed tego rodzaju lukami, jeśli spróbujesz odczytać element o indeksie, który nie istnieje, Rust zatrzyma wykonanie i odmówi kontynuowania. Spróbujmy i zobaczmy:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Ten błąd wskazuje na linię 4 naszego main.rs, gdzie próbujemy uzyskać dostęp do indeksu 99 wektora w v.

Linia note: mówi nam, że możemy ustawić zmienną środowiskową RUST_BACKTRACE na dowolną wartość inną niż 0, aby uzyskać śledzenie stosu (backtrace) dokładnie tego, co się stało, aby spowodować błąd. Śledzenie stosu to lista wszystkich funkcji, które zostały wywołane, aby dojść do tego punktu. Śledzenia stosu w Rust działają tak samo jak w innych językach: kluczem do odczytania śledzenia stosu jest rozpoczęcie od góry i czytanie, aż zobaczysz pliki, które napisałeś. To jest miejsce, w którym problem się rozpoczął. Linie powyżej tego miejsca to kod, który wywołał Twój kod; linie poniżej to kod, który wywołał Twój kod. Te linie przed i po mogą zawierać podstawowy kod Rust, kod standardowej biblioteki lub skrzynki, których używasz. Spróbujmy uzyskać śledzenie stosu, ustawiając zmienną środowiskową RUST_BACKTRACE na dowolną wartość inną niż 0. Listing 9-2 pokazuje wyjście podobne do tego, co zobaczysz.

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
   1: core::panicking::panic_fmt
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

To dużo danych wyjściowych! Dokładne dane wyjściowe mogą się różnić w zależności od systemu operacyjnego i wersji Rusta. Aby uzyskać śledzenia stosu z tymi informacjami, symbole debugowania muszą być włączone. Symbole debugowania są domyślnie włączone przy użyciu cargo build lub cargo run bez flagi --release, jak to zrobiliśmy tutaj.

W danych wyjściowych w Listing 9-2, linia 6 śledzenia stosu wskazuje na linię w naszym projekcie, która powoduje problem: linia 4 pliku src/main.rs. Jeśli nie chcemy, aby nasz program panikował, powinniśmy rozpocząć nasze dochodzenie w miejscu wskazanym przez pierwszą linię wspomniającą o pliku, który napisaliśmy. W Listing 9-1, gdzie celowo napisaliśmy kod, który wywołałby panikę, sposobem na naprawienie paniki jest nie żądanie elementu poza zakresem indeksów wektora. Gdy Twój kod panikuje w przyszłości, będziesz musiał dowiedzieć się, jaką akcję podejmuje kod z jakimi wartościami, aby spowodować panikę, i co kod powinien zrobić zamiast tego.

Powrócimy do panic! i do tego, kiedy powinniśmy, a kiedy nie powinniśmy używać panic! do obsługi warunków błędu, w sekcji „Panikować czy nie panikować!” w dalszej części tego rozdziału. Następnie przyjrzymy się, jak odzyskać się po błędzie za pomocą Result.