Ruby/Przetwarzanie wyjątków: rescue

Przetwarzanie wyjątków: rescue

edytuj

Wykonujący się program może napotkać na niespodziewane problemy. Plik, które chce odczytać może nie istnieć, dysk może być pełny, gdy trzeba zapisać dane, a użytkownik wprowadza niepoprawny rodzaj danych wejściowych.

irb(main):001:0> plik = open('jakis_plik')
Errno::ENOENT: No such file or directory - jakis_plik
        from (irb):1:in `initialize'
        from (irb):1:in `open'
        from (irb):1

Solidny program powinien radzić sobie z takimi sytuacjami sensownie i wdzięcznie. Sprostanie temu wymaganiu może być irytującym zadaniem. Od programistów języka C oczekuje się sprawdzania wyniku każdego wywołania systemowego które potencjalnie mogło się nie powieść oraz natychmiastowego zdecydowania, co należy zrobić:

FILE *plik = fopen("jakis_plik", "r");
if (plik == NULL) {
  fprintf( stderr, "Plik nie istnieje.\n" );
  exit(1);
}
bajty_przeczytane = fread( buf, 1, bajty_zadane, file );
if (bajty_przeczytane != bajty_zadane) {
  /* tutaj wiecej kodu obslugi bledow... */
}
...

Jest to bardzo męcząca praktyka, którą programiści mają w zwyczaju traktować niedbale i pomijać, czego rezultatem jest to, że program źle sobie radzi z wyjątkami. Z drugiej strony, porządne wykonanie tej pracy czyni programy trudnymi do czytania, ponieważ duża ilość kodu obsługi wyjątków przesłania właściwą logikę programu.

W Rubim, tak jak w wielu współczesnych językach programowania, możemy radzić sobie z wyjątkami poszczególnych bloków kodu oddzielnie, co jednak skutecznie acz nie nadmiernie obciąża programistę lub każdego, kto będzie potem czytał kod. Blok kodu oznaczony słowem begin wykonuje się dopóki nie napotka na wyjątek, który powoduje przekierowanie kontroli do bloku zarządzania błędami, rozpoczynającego się od rescue. Jeżeli nie wystąpi żaden wyjątek, kod z bloku rescue nie jest używany. Następująca metoda zwraca pierwszą linię z pliku tekstowego lub nil jeżeli napotka wyjątek:

def pierwsza_linia(nazwa_pliku)
  begin
    plik = open(nazwa_pliku)
    info = plik.gets
    plik.close
    info  # Ostatnia obliczona rzecz jest zwracana
  rescue
    nil   # Nie mozesz przeczytac pliku? więc nie zwracaj łancucha
  end
end

Będą występować sytuacje, gdy będziemy chcieli kreatywnie pracować nad problemem. Tutaj, jeśli plik, który żądamy jest niedostępny, możemy spróbować użyć standardowego wejścia:

begin
  plik = open("jakis_plik")
rescue
  plik = STDIN
end

begin
  # ... przetwarzaj wejscie ...
rescue
  # ... tutaj obsluguj wyjatki.
end

Słowo kluczowe retry może być używane w bloku rescue, by wystartować blok begin od początku. Pozwala to nam przepisać poprzedni przykład nieco zwięźlej:

nazwap = "jakis_plik"
begin
  plik = open(nazwap)
  # ... przetwarzaj wejscie ...
rescue
  nazwap = "STDIN"
  retry
end

Jednakże, mamy tutaj pewną wadę. Nieistniejący plik sprawi, że pętla ta będzie powtarzana w nieskończoność. Musisz zwracać uwagę na tego rodzaju pułapki podczas przetwarzania wyjątków.

Każda biblioteka Rubiego podnosi wyjątek jeśli wystąpi jakiś błąd. Ty również możesz podnosić wyjątki jawnie w kodzie. By podnieść wyjątek użyj słowa kluczowego raise. Przyjmuje ono jeden argument, którym powinien być łańcuch znakowy opisujący wyjątek. Argument jest wprawdzie opcjonalny, jednak nie powinien być pomijany. Będzie on mógł być później dostępny za pomocą specjalnej zmiennej globalnej $!.

begin
  raise "test2"
rescue
  puts "Wystapil blad: #{$!}"
end
#=> Wystapil blad: test2

Zmienna $! zwraca konkretny obiekt który jest podklasą klasy Exception. Klasa Exception jest nadklasą (niekoniecznie wprost) wszystkich wyjątków. Cechę tę można efektywnie wykorzystać podczas definiowania różnych bloków obsługujących poszczególne typy wyjątków.

 
begin
  plik = open("jakis_plik")
rescue SystemCallError
  puts "Blad WE/WY: #{$!}"
rescue Exception
  puts "Blad: #{$!}"
end
#=> Blad WE/WY: No such file or directory - jakis_plik

Operacja otwarcia pliku generuje wyjątek będący podklasą SystemCallError więc zostanie wykonany blok obsługujący ten wyjątek. Interpreter po kolei sprawdza wszystkie bloki rescue i wykonuje pierwszy pasujący. Z tego też powodu nie należy umieszczać rescue Exception jako pierwszego, gdyż Exception jako nadklasa wszystkich wyjątków będzie tu zawsze pasować i blok obsługujący Exception będzie zawsze wykonywany.

Jeżeli podmienimy plik = open("jakis_plik") na np. raise "jakis blad" wykonany zostanie blok rescue obsługujący Exception:

 
begin
  raise "jakis blad"
rescue SystemCallError
  puts "Blad WE/WY: #{$!}"
rescue Exception
  puts "Blad: #{$!}"
end
#=> Blad: jakis blad

Zamiast zmiennej $! można używać zmiennych nazwanych stosując operator => i składnię przypominającą definiowanie wpisu tablicy asocjacyjnej.

begin
  # ... jakis kod ...
rescue SystemCallError => e
  puts "Blad we/wy: #{e}"
rescue Exception => e
  puts "Blad: #{e}"
end