Ruby-Programmierung: Exceptions

Zurück zum Inhaltsverzeichnis

In vielen modernen Programmiersprachen gibt es Konstrukte, die es erlauben auf Fehler bei der Programmierung des Programms hinzuweisen. Diese Fehler werden in Ruby Exceptions genannt, dabei handelt es sich um einen alternativen Programmablauf, wenn ein Fehler auftritt.

Exceptions

Bearbeiten

Exceptions werden durch das Schlüsselwort raise angezeigt. Die dadurch erzeugten Fehler werden im Rubyprogramm an die aufrufende Methode weitergereicht. Dadurch ist es Möglich an jeder Stelle des Programms den aufgetretenen Fehler abzufangen und ihn zu behandeln. Diese Fehlerbehandlung erfolgt mit den Schlüsselwörtern begin, rescue und ensure. Dabei wird versucht den Teil des Programms innerhalb des begin auszuführen, falls dabei ein Fehler auftritt wird der mit rescue eigeleitete Block ausgeführt, während Code innerhalb von ensure in jedem Fall ausgeführt wird.

def exception_if(bool)
  raise ArgumentError if bool
end

[true, false].each do |b|
  begin
    puts "Running with b=#{ b }"
    exception_if(b)
    puts "After possible exception"
  rescue ArgumentError => e
    puts "An error occurred: #{ e }!"
  ensure
    puts "Always executed, no matter what."
  end
end

Das Skript verdeutlicht den oben geschilderten Sachverhalt. Es ist möglich rescue auf bestimmte Fehler zu beschränken und diese Fehler in einer Variablen zu speichern.

Da das auftreten von Exceptions im Program in den meisten Fällen auf einen Programmierfehler zurückzuführen ist, sollte das Einfangen aller Fehler durch ein einfaches rescue verhindert werden. Es gibt Bibliotheken, zum Beispiel für Netzwerkzugriff, die einen Timeout als Exception anzeigen. An dieser Stelle sollte nur die bestimmte Timeoutexception eingefangen werden. Exceptions sollten nicht den Programmablauf beeinflussen, sondern die sollte durch normale Verzweigungen geschehen.

def div(a, b)
  return a / b
  rescue 
    "Can't divide by zero"
end

puts div(1, 0)
puts div(6, 3)
puts div(1.0, 0.0)
puts div("a", 2)

Dieses Skript ist ein Negativbeispiel für den oben erwähnten Punkt, soll aber an dieser Stelle dazu dienen den Nachteil aufzuzeigen.

Zum einen wird jede mögliche Exception abgefangen, auch wenn sie mit der Division durch Null gar nichts zu tun hat, das ist im Zweifelsfall verwirrend. So führt die fehlerhafte Benutzung der Methode mit einem String als Parameter trotzdem zur Ausgabe, das man nicht mit Null dividieren könne.

Außerdem ist die Ausgabe der dritten Zeile Unendlich, statt der korrekten Fehlerbehandlung. Dies hat mit der binären Darstellung von gebrochenen Zahlen zu tun und wird nicht korrekt abgefangen.

Die gezeigte Methode ist also aus mehreren Gründen schlecht: zum einen fängt sie nicht jeden Fehler ab und manche Fehler führen zudem zu einer falschen Ausgabe und damit zu Konfusion entweder bei Ihnen als Programmierer oder beim Benutzer. Auf die Verwendung von Exceptions zur Änderung des Programmablaufs sollte man unbedingt verzichten und stattdessen auf throw oder normale Verzweigungen zurückgreifen. Die oben gezeigte Methode sollte daher besser so aussehen:

def div(a, b)
  if b == 0
    "Can't divide by zero."
  else
    a / b
  end  
end

puts div(1, 0)
puts div(6, 3)
puts div("a", 2)

StandardError

Bearbeiten

In manchen Fällen reichen die verfügbaren Fehlerklassen nicht aus, um sinnvoll auszudrücken, was im jeweiligen Fall passiert ist. Es ist einfach möglich neue Fehlerklassen durch Vererbung einzuführen und diese zu benutzen. Die Klasse StandardError stellt dabei insbesondere zwei interessante Methoden zur verfügung, nämlich den Backtrace des Fehlers, sowie eine übergebene Nachricht, die den Fehler genauer beschreiben kann.

class CustomError < StandardError; end

def test
  raise CustomError, "Something is wrong"
end

begin
  test
rescue CustomError => e
  puts "An Error occurred: #{ e.message } in #{ e.backtrace }"
end

Rückblick: Threads

Bearbeiten

Um diesem Kapitel eine praktische Relevanz zu verleihen wird auf ein Problem bei der Verwendung von Threads eingegangen. Falls Sie mit diesen schon ein bisschen experimentiert haben, ist Ihnen vielleicht eine Situation unterlaufen, die ähnlich zu der folgenden ist und Sie verwundert hat.

Thread.new do
  1/0
end

sleep(1)
puts "Everything is fine."

In diesem sehr einfachen Beispiel ist der Fehler einfach zu erkennen. Eine Division durch Null ist nicht möglich und wirft eine Exception, wenn Sie diesen Code allerdings ausführen werden Sie keine Exception erkennen. Der Interpreter weiß von der Exception in dem neuen Thread nichts und kann Sie daher auch nicht wie oben erklärt anzeigen. Es ist jedoch möglich das Anzeigen des Fehlers in Threads manuell durchzuführen.

def thread(&block)
  Thread.new do
    begin
      block.()
    rescue StandardError => e
      puts "Error:#{ e } occurred\n#{ e.backtrace.join("\n") }"
    end
  end
end

thread { 1/0 }
sleep(1)
puts "Nothing is fine ):"

Catch und Throw

Bearbeiten

In anderen Programmiersprachen werden Exceptions manchmal verwendet um den Programmablauf zu beeinflussen, um zum Beispiel aus geschachtelten Schleifen herauszukommen. Darauf sollte man in Ruby verzichten, da raise sehr langsam ist und daher nur in Ausnahmefällen sinnvoll ist.

In Ruby gibt es stattdessen eine andere Konstruktion, die für diesen Fall geeignet ist. catch leitet einen Block ein und erhält eine Sprungmarke auf die man mit throw zugreifen kann und damit die Ausführung des Blockes beenden kann. Ein optionaler zweiter Parameter von throw führt dazu, dass catch diesen zurückgibt.

i = catch :test  do
  (0..10).each do |i|
    throw(:test, i) if i == 5
    puts i
  end
end

puts "after loop: #{ i }"

throw ist im Gegensatz zu raise sehr leichtgewichtig, sollte aber in Anbetracht der eventuell schlechteren Lesbarkeit des Programms nur mit Vorsicht angewendet werden.