Nieodwracalne Błędy z panic!
Czasami w kodzie dzieją się złe rzeczy i nic nie można na to poradzić.
W takich przypadkach Rust posiada makro panic!
. Istnieją dwa sposoby na wywołanie go
w praktyce: podejmując działanie, które powoduje, że nasz kod wpada w panikę (np.
dostęp do tablicy poza jej końcem) lub poprzez jawne wywołanie makra panic!
.
W obu przypadkach wywołujemy panikę w naszym programie. Domyślnie, te paniki
wypisują komunikat o niepowodzeniu, zwijają, czyszczą stos i kończą działanie. Poprzez
zmienną środowiskową, można również nakazać Rustowi wyświetlanie stosu wywołań, gdy wystąpi
panika aby ułatwić odnalezienie źródła paniki.
Zwijanie Stosu lub Przerwanie w Odpowiedzi na Panikę
Domyślnie, gdy wystąpi panika, program rozpoczyna zwijanie, co oznacza, że Rust cofa się w górę stosu i czyści dane każdej napotkanej funkcji. Jednak cofanie się i czyszczenie to dużo pracy. Rust dlatego pozwala wybrać alternatywę natychmiastowego przerwania, co kończy program bez czyszczenia.
Pamięć, z której korzystał program, będzie musiała zostać wyczyszczona przez system operacyjny. Jeśli w swoim projekcie chcesz, aby wynikowy plik binarny był tak mały jak to tylko możliwe, możesz przełączyć się ze zwijania na przerywanie w przypadku paniki poprzez dodanie
panic = 'abort'
do odpowiednich sekcji[profile]
w pliku Cargo.toml. Na przykład, jeśli chcesz przerwać po panice w trybie zwijania, dodaj to:[profile.release] panic = 'abort'
Spróbujmy wywołać panic!
w prostym programie:
Filename: src/main.rs
fn main() { panic!("crash and burn"); }
Po uruchomieniu programu zobaczysz coś takiego:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Wywołanie panic!
powoduje wyświetlenie komunikatu o błędzie zawarty w dwóch ostatnich wierszach.
Pierwsza linia pokazuje komunikat paniki i miejsce w naszym kodzie źródłowym, w którym
wystąpiła panika: src/main.rs:2:5 wskazuje, że jest to druga linia,
piąty znak w pliku src/main.rs.
W tym przypadku wskazana linia jest częścią naszego kodu, a jeśli do niej przejdziemy
zobaczymy wywołanie makra panic!
. W innych przypadkach wywołanie panic!
może znajdować
się w kodzie wywoływanym przez nasz kod, a nazwa pliku i numer linii zgłoszone przez
komunikat o błędzie będą kodem innej osoby, w którym wywoływane jest makro panic!
,
a nie linią naszego kodu, która ostatecznie doprowadziła do wywołania panic!
.
Możemy użyć stosu wywołań funkcji, z których pochodzi wywołanie panic!
,
aby dowiedzieć się, która część naszego kodu powoduje problem. Stosy wywołań omówimy
bardziej szczegółowo w następnej części.
Używanie Stosu Wywołań panic!
Spójrzmy na inny przykład, aby zobaczyć, jak to jest, gdy wywołanie panic!
pochodzi z biblioteki z powodu błędu w naszym kodzie, a nie z naszego kodu
wywołującego makro bezpośrednio.
Listing 9-1 zawiera kod, który próbuje uzyskać
dostęp do indeksu w wektorze poza zakresem prawidłowych indeksów.
Filename: src/main.rs
fn main() { let v = vec![1, 2, 3]; v[99]; }
Listing 9-1: Próba uzyskania dostępu do elementu poza
końcem wektora, co spowoduje wywołanie panic!
.
Tutaj próbujemy uzyskać dostęp do setnego elementu naszego wektora (który
znajduje się w indeksie 99, ponieważ indeksowanie zaczyna się od zera),
ale wektor ma tylko 3 elementy. W tej sytuacji Rust wpadnie w panikę.
Użycie []
powinno zwrócić element, ale jeśli podasz nieprawidłowy indeks,
Rust nie będzie w stanie zwrócić żadnego elementu, który byłby poprawny.
W języku C próba odczytu poza końcem struktury danych jest niezdefiniowanym zachowaniem. Można uzyskać to, co znajduje się w miejscu w pamięci, które odpowiadałoby temu elementowi w strukturze danych, nawet jeśli 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 za strukturą danych.
Aby chronić swój program przed tego rodzaju lukami, jeśli spróbujesz odczytać element w indeksie, który nie istnieje, Rust zatrzyma wykonywanie i odmówi kontynuowania. Wypróbujmy to i zobaczmy:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Ten błąd wskazuje na linię 4 naszego pliku main.rs
, gdzie próbujemy uzyskać
dostęp do indeksu 99. Następna linia notatki mówi nam, że możemy ustawić
zmienną środowiskową RUST_BACKTRACE
, aby uzyskać stos wysołań tego,
co dokładnie spowodowało błąd. Stos wywołań to lista wszystkich funkcji,
które zostały wywołane, aby dotrzeć do tego punktu. Stos wywołań w Ruście działa
podobnie jak w innych językach: kluczem do odczytania stosu wywołań jest
rozpoczęcie od góry i czytanie, aż zobaczysz pliki, które napisałeś. Jest to miejsce,
w którym pojawił się problem. Linie powyżej tego miejsca to kod, który został wywołany
przez twój kod; linie poniżej to kod, który wywołał twój kod. Te linie przed i po mogą
zawierać kod rdzenia Rusta, kod biblioteki standardowej lub używane skrzynki.
Spróbujmy uzyskać stos wywołań, ustawiając zmienną środowiskową RUST_BACKTRACE
na dowolną wartość z wyjątkiem 0. Listing 9-2 pokazuje dane wyjściowe podobne do tego, co zobaczysz.
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Listing 9-2: Stos wywołań wygenerowany przez wywołanie panic!
wyświetlany,
gdy ustawiona jest zmienna środowiskowa RUST_BACKTRACE
.
To bardzo 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ć stosy wywołań
z tymi informacjami, symbole debugowania muszą być włączone. Symbole debugowania
są domyślnie włączone podczas korzystania z cargo build
lub cargo run
bez flagi --release
, tak jak tutaj.
W danych wyjściowych na listingu 9-2, linia 6 stosu wywołań wskazuje na linię w naszym projekcie, która powoduje problem: linia 4 pliku src/main.rs. Jeśli nie chcemy, aby nasz program wpadł w panikę, powinniśmy rozpocząć nasze dochodzenie w miejscu wskazywanym przez pierwszą linię wspominającą o napisanym przez nas pliku. Na listingu 9-1, gdzie celowo napisaliśmy kod, który spowodowałby panikę, sposobem na naprawienie paniki jest nie żądanie elementu spoza zakresu indeksów wektora. Gdy kod będzie wpadał w panikę w przyszłości, trzeba będzie dowiedzieć się, jakie działania kod wykonuje z jakimi wartościami, aby wywołać panikę i co kod powinien zrobić zamiast tego.
Wrócimy do panic!
i omówimy kiedy powinniśmy, a kiedy nie powinniśmy używać panic!
do obsługi warunków błędu w sekcji "To panic!
or Not topanic!
"
w dalszej części tego rozdziału. Następnie przyjrzymy się, jak odzyskać
dane po błędzie przy użyciu Result
.