|
|||
open
gibt einen bestimmten Wert zurück, um zu sagen, dass sie nicht geklappt hat. Dieser Wert wird dann über die verschiedenen Schichten von aufrufenden Routinen zurückgegeben, bis irgend jemand sich um ihn kümmert.
Das Problem dabei ist, dass die Behandlung aller dieser Fehler ziemlich übel sein kann. Wenn eine Funktion open
aufruft, dann read
und schließlich close
und alle können einen Fehler-Code zurückliefern, wie kann diese Funktion all diese Fehler-Codes unterscheiden und der zugehörigen aufgerufenen Methode zuordnen?
In großem Maße lösen Exceptions dieses Problem. Mit Exceptions kann man Informationen über einen Fehler in ein Objekt packen. Dieses Exception_Objekt wird dann automatisch über den Funktions-Stack zurückgeliefert, bis das Runtime-System Code findet, der explizit erklärt, dass er für diese Art von Exception zuständig ist.
Exception
, oder eines der Kinder der Klasse Exception
. In Ruby wird eine strenge Hierarchie von Exceptions vordefiniert, wie in Figur 8.1 auf Seite 93 zu sehen. Wie wir später noch sehen werden, macht diese Hierarchie das Behandeln von Exceptions wesentlich einfacher.
Exception
-Klassen benutzen oder man kann eine eigene erzeugen. Wenn man eine eigene erzeugt, sollte man sie als Unterklasse von
StandardError
oder einer ihrer Nachfahren machen. Falls nicht, dann wird diese Exception nicht automatisch aufgefangen.
Zu jeder Exception
gibt es einen zugehörigen Meldungs-String und einen Rückverfolgungsstapel. Wenn man eine eigene Exception definiert, kann man noch zusätzliche Informationen hinzufügen.
opFile = File.open(opName, "w") while data = socket.read(512) opFile.write(data) end |
begin
/end
-Block und benutzen rescue
, um Ruby den Typ der Exception zu sagen, den wir behandeln wollen. In diesem Fall sind wir an SystemCallError
-Exceptions interessiert (und implizit an allen Exceptions, die Unterklassen von SystemCallError
sind), also schreiben wir das in die rescue
-Zeile. Im Fehlerbehandlungs-Block merken wir uns den Fehler, schließen und vernichten die Output-Datei und dann lösen wir die Exception neu aus.
opFile = File.open(opName, "w") begin # Exceptions die von diesem Code ausgelöst werden, # werden von der folgenden Rescue-Klausel aufgefangen while data = socket.read(512) opFile.write(data) end rescue SystemCallError $stderr.print "IO failed: " + $! opFile.close File.delete(opName) raise end |
Exception
-Objektes in die globale Variable $!
(das Ausrufezeichen zeigt noch zusätzlich unsere Überraschung an, dass
unser Code überhaupt Fehler erzeugen könnte). Im vorherigen Beispiel benutzten wir diese Variable, um unsere Fehlermeldung zu formatieren.
Nachdem wir die Datei geschlossen und entfernt haben, rufen wir raise
ohne Parameter auf, das löst die Exception in $!
erneut aus. Dies ist eine nützliche Technik, sie erlaubt einem, Code zu schreiben, der Exceptions filtert und die, die man nicht selber behandeln kann, an höhere Ebenen weiterzuleiten. Das ist als wenn man eine Vererbungshierarchie für Fehlerbehandlungen implementiert.
Man kann mehrere rescue
-Klauseln in einem begin
-Block haben und jede rescue
-Klausel kann mehrere Exceptions angeben, die sie fangen will. Am Ende jeder Rescue-Klausel kann man für Ruby den Namen einer lokalen Variablen angeben, die die passende Exception aufnehmen soll. Viele Leute finden das lesbarer, als dauernd $!
zu benutzen.
begin eval string rescue SyntaxError, NameError => boom print "String doesn't compile: " + boom rescue StandardError => bang print "Error running script: " + bang end |
case
-Anweisung. Bei jeder rescue
Klausel im begin
-Block vergleicht Ruby die ausgelöste Exception mit den Rückgabeparametern. Wenn einer davon auf die ausgelöste Exception passt, führt Ruby den Rumpf von rescue
aus und hört auf zu suchen. Die Suche wird mit
$!.kind_of?(parameter)
ausgeführt und ist dann erfolgreich, wenn der Parameter die selbe Klasse wie die Exception besitzt oder einer ihrer Vorfahren ist. Wenn man eine rescue
-Klausel ohne Parameter-Liste schreibt, so ist der Default-Parameter StandardError
.
Wenn keine rescue
-Klausel passend ist oder die Exception außerhalb eines begin
/end
-Blocks ausgelöst wird, fährt Ruby den Stack aufwärts und sucht nach einer Exception-Behandlung im Aufrufer, dann im Aufrufer des Aufrufers und so weiter.
Obwohl die Parameter einer rescue
-Klausel üblicherweise die Namen von Exception
-Klassen sind, können sie auch beliebige Ausdrücke (inklusive Methoden-Aufrufe) sein, die eine Exception
-Klasse zurückgeben.
ensure
-Klausel macht genau das.
ensure
kommt nach der letzten
rescue
-Klausel und enthält ein Stück Code, das immer ausgeführt wird, wenn der Block beendet wird. Dabei kommt es nicht darauf an, ob der Block normal verlassen wird, ob er eine Exception auslöst und ein Rescue durchläuft oder ob er durch eine nicht abgefangene Exception beendet wird --- der ensure
-Block kommt zuletzt dran.
f = File.open("testfile") begin # .. Process rescue # .. Fehler- Behandlung ensure f.close unless f.nil? end |
else
-Klausel ist ein ähnliches aber weniger nützliches Konstrukt. Falls vorhanden kommt sie nach der rescue
-Klausel und vor einem eventuellen
ensure
. Der Rumpf der else
-Klausel wird nur dann ausgeführt. wenn keine Exceptions vom Hauptteil des Codes ausgelöst wurden.
f = File.open("testfile") begin # .. Process rescue # .. Fehler- Behandlung else puts "Congratulations-- no errors!" ensure f.close unless f.nil? end |
retry
-Anweisung innerhalb einer rescue
-Anweisung nutzen, um den kompletten begin
/end
Block zu wiederholen. Natürlich ist das eine hervorragende Möglichkeit, Endlos-Schleifen herzustellen, also sollte man das doch mit äußerster Vorsicht (und mit einem Finger auf dem Reset-Knopf) genießen.
Als Beispiel für Code, der nach einer Exception mit Retry wiederholt wird, sehen wir uns das Folgende aus Minero Aoki's net/smtp.rb
-Bibliothek an.
@esmtp = true begin # Versucht zuerst ein ausführliches Login. Falls das schiefgeht, weil der Server das nicht kennt, versuch ein normales Login if @esmtp then @command.ehlo(helodom) else @command.helo(helodom) end rescue ProtocolError if @esmtp then @esmtp = false retry else raise end end |
EHLO
-Kommando, die Verbindung zu einem SMTP-Server herzustellen, was aber nicht überall unterstützt wird. Falls der Verbindungsversuch fehlschlägt, setzt der Code die @esmtp
-Variable auf false
und wiederholt den Verbindungsversuch. Wenn das wieder schief geht, wird die Exception an den Aufrufer weitergereicht.
Kernel::raise
-Methode auslösen.
raise raise "bad mp3 encoding" raise InterfaceException, "Keyboard failure", caller |
RuntimeError
, falls es keine aktuelle Exception gibt). Das wird in Exception-Behandlungen benutzt, die eine Exception abfangen bevor sie sie weiterleiten.
Der zweite Fall erzeugt eine neue RuntimeError
-Exception und setzt seine Meldung in den gegebenen String. Diese Exception wird dann an den Aufruf-Stack weitergeleitet.
Im dritten Fall wird das erste Argument benutzt, um eine Exception zu erzeugen, die damit verbundene Meldung bildet das zweite Argument und die Stack-Spur das dritte. Üblicherweise ist das erste Argument entweder der Name einer Klasse aus der Exception
-Hierarchie oder eine Referenz auf eine Objekt-Instanz einer dieser Klassen.[Technisch gesehen kann dieses Argument jegliches Objekt sein, das auf die Exception-Meldung ein Objekt zurückgibt, das auf
object.kind_of?(Exception)
mit true antwortet.] Die Stack-Spur wird üblicherweise mit der Kernel::caller
-Methode erzeugt.
Jetzt einige typische Beispiele für raise
in Aktion.
raise raise "Missing name" if name.nil? if i >= myNames.size raise IndexError, "#{i} >= size (#{myNames.size})" end raise ArgumentError, "Name too big", caller |
raise ArgumentError, "Name too big", caller[1..-1] |
class RetryException < RuntimeError attr :okToRetry def initialize(okToRetry) @okToRetry = okToRetry end end |
def readData(socket) data = socket.read(512) if data.nil? raise RetryException.new(true), "transient read error" end # .. normal processing end |
begin stuff = readData(socket) # .. process stuff rescue RetryException => detail retry if detail.okToRetry raise end |
raise
und rescue
gut geeignet ist, die Ausführung abzubrechen, wenn ein Fehler aufgetreten ist, so ist es manchmal angebracht, aus einem tief verschachtelten Konstrukt ganz normal heraus zu springen. Dazu gibt es catch
und throw
.
catch (:done) do while gets throw :done unless fields = split(/\t/) songList.add(Song.new(*fields)) end songList.play end |
catch
definiert einen Block, der mit dem gegebenen Namen angesprochen wird (das kann ein Symbol
oder ein String
) sein. Der Block wird ganz normal ausgeführt, bis ein throw
auftaucht.
Wenn Ruby auf einen throw
stößt, dann läuft es zurück über den Aufrufer-Stack und sucht nach einem catch
-Block mit einem passenden Symbol. Wenn es einen gefunden hat, macht es den Stack bis dahin frei und beendet den Block. Wenn throw
mit dem optionalen zweiten Parameter aufgerufen wird, so wird dieser als Wert von catch
zurückgegeben. Falls also in den vorherigen Beispiel die Eingabe falsch formatierte Zeilen enthält, so springt
throw
an das Ende des dazugehörenden catch
und beendet dabei nicht nur die while
-Schleife sondern auch das Abspielen der Song-Liste.
Das folgende Beispiel benutzt einen throw
, um die Benutzerabfrage zu beenden, wenn ein ``!'' eingegeben wurde.
def promptAndGet(prompt) print prompt res = readline.chomp throw :quitRequested if res == "!" return res end catch :quitRequested do name = promptAndGet("Name: ") age = promptAndGet("Age: ") sex = promptAndGet("Sex: ") # .. # process information end |
throw
nicht innerhalb des statischen Bereichs von catch
auftauchen.