programowanieProgramista systemowy

Wyjaśnij, jak odbywa się zarządzanie błędami za pomocą Result<T, E> i jak prawidłowo używać operatora zapytania (?), nie naruszając bezpieczeństwa i czytelności kodu.

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Rust opiera się na jawnej kontroli błędów: wyjątki są nieobecne, zamiast tego używa się wartości zwracanej Result<T, E>. Zapewnia to bezpieczeństwo i przewidywalność kodu.

Historia zagadnienia:

Wiele języków poszło drogą wyjątków, co prowadziło do nieoczekiwanych sytuacji i konieczności jawnego obsługiwania wyjątków w czasie działania. Rust od samego początku postawił na kontrolę przez typy, wszystkie błędy muszą być częścią sygnatury funkcji.

Problem:

Głównym zadaniem jest pisanie kodu, w którym błędy nie są ignorowane i nie są ukrywane, nie ma „padających” funkcji, ale kod pozostaje kompaktowy i czytelny. Należy prawidłowo propagować błędy wyżej, nie tracąc informacji o ich typie, i nie mylić logiki.

Rozwiązanie:

Kluczowym narzędziem jest typ Result<T, E> i operator ?, który automatycznie „rozwija” wynik: w przypadku błędu następuje natychmiastowe wyjście z funkcji z zwróceniem błędu, a w przypadku sukcesu zwracana jest wartość.

Przykład kodu:

fn read_number(file: &str) -> Result<i32, std::io::Error> { let content = std::fs::read_to_string(file)?; let num: i32 = content.trim().parse()?; Ok(num) }

Kluczowe cechy:

  • Jawna deklaracja wszystkich potencjalnych błędów w sygnaturze
  • Operator ? pozwala upraszczać zagnieżdżenie (eliminując if-let/unwrap/expect)
  • błędy mogą być łatwo „opakowane” lub przekształcone (poprzez .map_err() i inne metody)

Pytania z podchwytliwymi odpowiedziami.

Czy można używać ? w funkcjach zwracających Unit (void)?

Nie, operator ? jest dozwolony tylko wewnątrz funkcji, które zwracają Result lub Option. Jeśli funkcja zwraca (), nie można używać ?.

Co się stanie, jeśli typy błędów w kilku wywołaniach z ? są różne?

Powstaje błąd kompilacji: typ błędu musi być jednoznacznie określony. Należy albo sprowadzić wszystkie błędy do jednego typu poprzez .map_err(), albo używać thiserror, albo opisać typ enum-opakowanie na poziomie API. Przykład:

fn foo() -> Result<_, MyError> { let a = bar()?; let b = baz().map_err(MyError::Baz)?; Ok() }

Jakie niebezpieczeństwo niesie ze sobą .unwrap() w wewnętrznej logice?

Powszechne jest błędne przekonanie, że w „głównym” kodzie można swobodnie używać unwrap(), bo „na pewno nie spadnie”. W rzeczywistości nawet mały niewidoczny błąd doprowadzi do paniki czasu wykonania — naruszy to bezpieczeństwo programu.

Typowe błędy i antywzorce

  • Przechwytywanie wszystkich błędów za pomocą unwrap/expect, szczególnie w wewnętrznej logice
  • Utrata kontekstu przy map_err(|_| …), gdy błąd jest zamykany bez zachowania pierwotnych informacji
  • Nadmierna zagnieżdżenie przy ręcznym przetwarzaniu każdego błędu bez ?

Przykład z życia

Negatywny przypadek

W kodzie produkcyjnym pozostawiono wiele unwrap() po szybkim debugowaniu. W rezultacie aplikacja padała przy każdym błędzie parsowania z powodu niepoprawnego wejścia od użytkownika.

Zalety:

  • Szybkie debugowanie

Wady:

  • Potencjalna awaria aplikacji, trudno znaleźć przyczynę

Pozytywny przypadek

Użyto pełnego stosu Result<T, E> z explicite opisanymi błędami i stosowano tylko ? oraz .map_err(), zachowując wszystkie informacje o błędzie.

Zalety:

  • Łatwość debugowania, przewidywalne zachowanie

Wady:

  • Nieco więcej „szumowych” typów błędów