Wyjątki są mechanizmem występującym w wielu językach programowani. Służą one do obsługi sytuacji nietypowych, w szczególności sytuacji błędnych. Źródło błędu może być różne:
Mechanizm wyjątków umożliwia obsługę wszystkich typów błędów w ujednolicony sposób. Dodatkowo, w sytuacji, w której błąd wystąpi, pozwala szybko zorientować się jakie jest źródło problemu (w przeciwieństwie do kompunikatu Segmentation fault
znanego z języka C).
Jeśli w wyniku wykonania programu spróbujemy podzielić przez zero, to wystąpi błąd ZeroDivisionError
:
a = 1/0
Opis błędu składa się z nazwy błędu: ZeroDivisionError
, komunikatu: divided by 0
oraz tzw. stacktrace'a, czyli ciągu wywołań, które doprowadziły do błędu. Ponieważ Ruby nie jest kompilowany, stacktrace zawiera wskazanie plików i numerów linii, w których występuje wywołanie, które doprowadziło do wystąpienia błędu. Śledząc stacktrace, zwykle można dość szybko zidentyfikować miejsce, które wymaga naprawy.
Jeśli np. próbujemy rozwiązać niebanalny problem polegający na określeniu ile potrzeba trabantów do przewiezienia grupy słoni (a liczba słoni mieszczących się w trabancie podawana jest parametrem programu) możemy napisać następujący kod:
def liczba_miejsc_w_trabancie
4
end
def oblicz_liczbe_pelnych_samochodow(liczba_sloni)
liczba_sloni / liczba_miejsc_w_trabancie
end
def oblicz_liczbe_niepelnych_samochodow(liczba_sloni)
if liczba_sloni % liczba_miejsc_w_trabancie == 0
0
else
1
end
end
def oblicz_liczbe_wszystkich_samochodow(liczba_sloni)
oblicz_liczbe_pelnych_samochodow(liczba_sloni) + oblicz_liczbe_niepelnych_samochodow(liczba_sloni)
end
def ile_potrzeba_samochodow?(liczba_sloni)
puts "Aby przewieźć #{liczba_sloni} słoni potrzeba #{oblicz_liczbe_wszystkich_samochodow(liczba_sloni)} samochodów."
end
ile_potrzeba_samochodow?(10)
Jeśli teraz ktoś zmieni definicję funkcji liczba_miejsc_w_trabancie
:
# wszystkie trabanty są zajmowane przez myszy, dlatego nie można już w nich umieszczać słoni
def liczba_miejsc_w_trabancie
0
end
i zapyta o liczbę trabantów:
ile_potrzeba_samochodow?(10)
otrzyma błąd:
Przykład ten jest oczywiście bardzo rozbudowany, ale jeśli przyjrzymy się stacktrace'owi, to szybko możemy odnaleźć miejsce, które doprowadziło do wystąpienia błędu - przynajmniej pośrednio.
Możliwość zidentyfikowania źródła błędu jest bardzo cenna, niemniej znacznie częściej będziemy chcieli, aby w takiej sytuacji program nadal działał. Co najwyżej dopuścimy specjalny komunikat dla użytkownika informujący o błędzie.
Aby zabezpieczyć się przed wyjątkiem otaczamy "kod specjalnej troski" słowami kluczowymi begin
, rescue
i end
:
def oblicz_liczbe_pelnych_samochodow(liczba_sloni)
begin
liczba_sloni / liczba_miejsc_w_trabancie
rescue ZeroDivisionError
puts "Nie dziel przez zero..."
0
end
end
ile_potrzeba_samochodow?(10)
Istotne pytanie, które pojawia się w tym kontekście dotyczy miejsca, w którym ma nastąpić obsługa błędu. Tak jak widzimy w powyższym przykładzie, obsługa wprost w funkcji oblicz_liczbe_pelnych_samochodow
nie wystarczyła, ponieważ ten sam błąd występuje również w funkcji obliczb_liczbe_niepelnych_samochodow
. Dlatego zdecydowanie lepszym miejscem obsługi błędu jest funkcja ile_potrzeba_samochodow?
.
# przywracamy poprzednią definicję tej funkcji
def oblicz_liczbe_pelnych_samochodow(liczba_sloni)
liczba_sloni / liczba_miejsc_w_trabancie
end
# oraz dodajemy obsługę błędu na wyższym poziomie
def ile_potrzeba_samochodow?(liczba_sloni)
begin
puts "Aby przewieźć #{liczba_sloni} słowni potrzeba #{oblicz_liczbe_wszystkich_samochodow(liczba_sloni)} samochodów."
rescue ZeroDivisionError
puts "W trabancie nie można umieścić żadnego słownia, bo jest on już zajmowany przez myszy."
end
end
ile_potrzeba_samochodow?(10)
Udało nam się uporać z trabantami, które zostały już zajęte przez myszy. To nie są jednak wszystkie możliwe błędy, które mogą się pojawić. Jakiś mało rozgarnięty programista może np. wywołać funkcję w następujący sposób:
ile_potrzeba_samochodow?(nil)
Oczywiste jest, że nie potrzeba żadnego samochodu, jeśli w ogóle nie mamy słoni do przewiezienia. Wywołanie powyższego kodu skutkuje jednak innym błędem niż wcześniej: NoMethodError
. Oznacza on, że obiekt nil
nie ma metody pozwalającej na jego dzielenie (zarówno przez zero, jak przez dowolną inną wartość). Problem ten rozwiążemy również bezpośrednio w funkcji ile_potrzeba_samochodow?
dodając kolejną klauzulę rescue
:
def ile_potrzeba_samochodow?(liczba_sloni)
begin
puts "Aby przewieźć #{liczba_sloni} słowni potrzeba #{oblicz_liczbe_wszystkich_samochodow(liczba_sloni)} samochodów."
rescue ZeroDivisionError
puts "W trabancie nie można umieścić żadnego słownia, bo jest on już zajmowany przez myszy."
rescue NoMethodError
puts "Trzeba podać liczbę słoni a nie jakąś dziwną wartość: '#{liczba_sloni}' typu #{liczba_sloni.class}."
end
end
ile_potrzeba_samochodow?(nil)
ile_potrzeba_samochodow?("5")
ile_potrzeba_samochodow?(/aaaa/)
Wiemy już jak obsługiwać wyjątki. A jak je zgłaszać? Służy do tego polecenie raise
:
raise "Rzucam wyjątek bo tak."
Jeśli nie podamy nazwy wyjątku, to domyślnie będzie to błąd RuntimeError
. Aby rzucić wyjątek określonego typu, musimy użyć konstruktora, który przyjmuje komunikat wyjątki jako parametr wywołania:
raise ArgumentError.new("Jakiś dziwny ten argument")
Błąd tego rodzaju obsługujemy tak jak inne wyjątki:
begin
raise ArgumentError.new("Jakiś dziwny ten argument")
rescue ArgumentError => ex
puts ex
end
W powyższym kodzie została zostosowana dodatkowa możliwość przy obsłudze wyjątków - przechwycenie ich do zmiennej, w tym wypadku ex
. Zmienna taka zawiera wszystkie istotne informacje dotyczące wyjątku, m.in. komunikat i stos wywołań:
begin
raise ArgumentError.new("Jakiś dziwny ten argument")
rescue ArgumentError => ex
puts ex
puts ex.backtrace[0..5]
end
Zmodyfikuj funkcje oblicz_liczbe_pelnych_samochodow
oraz oblicz_liczbe_niepelnych_samochodow
tak by rzucały wyjątek ArgumentError
, jeśli liczba słoni jest mniejsza od 0. Następnie zmodyfikuj funkcję ile_potrzeba_samochodow?
, tak by obsługiwała ten wyjątek. W następnej kolejności zmodyfikuj funkcję liczba_miejsc_w_trabancie
, tak by ponownie można było wozić słonie. Przetestuj główną funkcję przekazując jej różne parametry (również mało oczywiste). Czy tak zdefiniowana funkcja jest niezawodna?
Dwie podstawowe operacje na plikach to odczytywanie i zapisywanie danych do pliku.
Obie operacje wymagają aby plik został najpierw otwarty. Służy do tego metoda File.open
, która wymaga podania ścieżki do pliku:
plik = File.open("data/authors.csv")
puts plik.readline
plik.close
Po otwarciu pliku, można na nim wykonywać operacje. W powyższym przykładzie metoda readline
wczytała pierwszą linię pliku. Po jej wyświetleniu plik został zamknięty poleceniem close
.
Ponieważ po otwarciu pliku trzeba go zawsze zamknąć (choć programiści i programistki często o tym zapominają), Ruby oferuje alternatywną składnie do obsługi plików:
File.open("data/authors.csv") do |plik|
puts plik.readline
end
Jej przewaga nad poprzednią metodą jest taka, że po opuszczeniu bloku do
... end
plik zostanie automatycznie zamknięty. A jeśli zapomnielibyśmy słowa end
, to program się po prostu nie wykona (wystąpi błąd składni).
Wyświetlenie 10 pierwszych wierszy wygląda następująco:
File.open("data/authors.csv") do |plik|
10.times do
puts plik.readline
end
end
Ponieważ linie w pliku zakończone są znakiem przejścia do nowej linii, warto pamiętać o zastosowaniu funkcji chomp
przy wyświetlaniu poszczególnych wierszy:
File.open("data/authors.csv") do |plik|
10.times do
puts plik.readline.chomp
end
end
Poszczególne wiersze wczytywane za pomocą polecenia readline
są po prostu łańcuchami znaków. Jeśli chcemy np. wyświetlić pierwsze pole w pliku, w którym stosuje się przecinek do oddzielenia pól (format CSV), to możemy zrobić to następująco:
File.open("data/authors.csv") do |plik|
10.times do
puts plik.readline.chomp.split(",").first
end
end
Do obsługi tego formatu istnieje osobna klasa CSV
, która np. bierzę pod uwagę, to, że przecinek może również być treścią pola. Ponadto obsługa plików csv
za pomocą tej klasy jest znacznie szybsza. Klasa ta nie jest jednak domyślnie dostępna, dlatego trzeba załadować ją za pomocą polecenia require
. Ponadto wczytywanie kolejnych wierszy odbywa się za pomocą metody shift
:
require 'csv'
CSV.open("data/authors.csv") do |plik|
10.times do
puts plik.shift.first
end
end
Zapisywanie do pliku jest bardzo podobne do wyświetlania treści na ekranie. Zapis do kolejnych linii realizowany jest za pomocą polecenia puts
. Jednakże próba zapisu do nieistniejącego pliku kończy się porażką:
File.open("data/moj_plik.txt") do |plik|
1.upto(10) do |indeks|
plik.puts "To jest #{indeks} linia"
end
end
W powyższym wywołaniu wystąpił wyjątek Errno::ENOENT
, oznaczający, że plik, który chcieliśmy otworzyć nie isnieje. Aby rozwiązać ten problem, konieczne jest zmodyfikowanie wywołania funkcji open
, przez dodanie flagi "w" informującej, że plik otwierany jest w trybie do zapisu:
File.open("data/moj_plik.txt","w") do |plik|
1.upto(10) do |indeks|
plik.puts "To jest #{indeks} linia"
end
end
To że kod faktycznie zadziałał można zweryfikować zaglądając do pliku data/moj_plik.txt
.
Napisz program, który przepisze wszystkie pierwsze pola (czyli nazwiska autorów) z pliku data/authors.csv
do pliku data/names.txt
. W nazwiskach znaki podkreślenia mają zostać zastąpione spacjami. Aby odwiedzić wszystkie wiersze w pliku użyj metody each
. Zwróć uwagę, że plik do którego zapisujesz i plik który odczytujesz muszą być dostępne za pomocą innych zmiennych (przujmując, że pracujemy na obu plikach jednocześnie) oraz, że w pliku authors.csv
jest więcej pozycji niż 10.
Wykonaj to samo zadanie co w poprzednim ćwiczeniu, uwzględniając następujące różnice:
data/sorted_names.txt
,Możesz pomiąć fakt, że w pliku na pierwszym miejscu pojawia się imię, a nie nazwisko autora.