Im Idealfall funktioniert alles perfekt. Aber in der realen Welt kann etwas schiefgehen, und wie wir mit diesen Situationen umgehen, ist entscheidend, damit unsere Software robust und zuverlässig ist. Ausnahmen sind ein wichtiges Konzept in der Programmierung, das es uns ermöglicht, Fehler und unerwartete Situationen elegant zu behandeln.
Wenn ein Fehler ausgelöst und nicht behandelt wird, hält das Programm an und gibt eine Fehlermeldung aus. In den meisten Fällen möchtest du nicht, dass dein Programm anhält, wenn ein Fehler auftritt. Stattdessen möchtest du den Fehler behandeln und das Programm weiterlaufen lassen.
In Crystal werden Ausnahmen mit dem Schlüsselwort raise ausgelöst und ihnen kann entweder ein String oder ein Exception-Objekt übergeben werden.
Wenn sie nicht behandelt werden, hält das Programm an und gibt die Fehlermeldung aus.
raise "This is an error"
Es gibt mehrere eingebaute Ausnahmen in Crystal, wie ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError und viele mehr.
Bei diesen musst du eine Nachricht an die Ausnahme übergeben.
raise ArgumentError.new("This is an argument error")
Wir wollen nicht, dass unser Programm abstürzt, wenn eine Ausnahme ausgelöst wird.
Wenn wir also wissen, dass ein Stück Code fehleranfällig ist, können wir es in einen begin-Block packen und die Ausnahme mit einem rescue-Block abfangen.
Der begin-Block markiert den Anfang des Codes, der eine Ausnahme auslösen könnte, und der rescue-Block fängt die Ausnahme ab.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
Der rescue-Block kann auch mit einer Variablen angegeben werden, um das Ausnahmeobjekt zu erhalten.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
Der rescue-Block kann auch mit einem bestimmten Ausnahmetyp angegeben werden, um nur diese Ausnahme abzufangen.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
Es können mehrere rescue-Blöcke verwendet werden, um verschiedene Arten von Ausnahmen zu behandeln.
Im folgenden Beispiel fängt der erste rescue-Block einen ArgumentError ab, und der zweite rescue-Block fängt jede andere Ausnahme ab.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
Der begin-Block kann auch einen else-Block haben, der ausgeführt wird, wenn keine Ausnahme ausgelöst wird.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
Schließlich gibt es einen ensure-Block, der immer ausgeführt wird, unabhängig davon, ob eine Ausnahme ausgelöst wurde.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
Manche Methoden haben zwei Versionen, eine mit ! und eine ohne.
Das kann zwei verschiedene Dinge bedeuten: Entweder verändert die Methode das Objekt, oder sie kann eine Ausnahme auslösen.
Aber es gibt noch eine weitere Konvention, eine Methode mit ? enden zu lassen, die im Konzept zu booleschen Werten erwähnt wird.
Manche Methoden lösen standardmäßig eine Ausnahme aus, haben aber auch eine Version, die mit ? endet und nil zurückgibt, statt eine Ausnahme auszulösen.
Das ist ideal, wenn du vermeiden willst, dass ein Fehler ausgelöst wird. Das kann die Leistung verbessern, da kein Stacktrace erstellt werden muss, und bei richtiger Einrichtung den Code sicherer machen.
du kannst auch eigene Ausnahmen erstellen, indem du von der Klasse Exception erbst.
Dabei kannst du optional die Methode initialize überschreiben, um die Ausnahmenachricht festzulegen.
Das kannst du tun, indem du einer Instanzvariable namens @message die Nachricht zuweist.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
Der Tag, auf den du so lange gewartet hast, ist endlich gekommen: Du bist jetzt der stolze Besitzer einer wunderschönen Farm in den Alpen.
Es gefällt dir immer noch nicht, morgens zu früh aufzustehen, um deine Kühe zu füttern.
Weil du ein ausgezeichneter Ingenieur bist, baust du einen Futterspender, den FEED-M-ALL.
Das Letzte, was noch fehlt, um dein Projekt abzuschließen, ist ein Stück Code, das die Futtermenge berechnet, die jede Kuh bekommen soll. Es ist wichtig, dass jede Kuh die gleiche Menge bekommt, du musst Konflikte vermeiden. Kühe sind sehr sensibel.
Zum Glück musst du nicht alle Formeln zur Berechnung der Futtermenge selbst herleiten. Du verwendest eine geheimnisvolle externe Bibliothek, die du im Internet gefunden hast. Sie soll für die glücklichsten Kühe sorgen. Die Bibliothek stellt eine Klasse bereit, die die folgende Schnittstelle erfüllt. Darauf wirst du dich in dem Code verlassen, den du selbst schreibst.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
Während du an deinem Code arbeitest, wirst du die Fehlerbehandlung verbessern, damit sie robuster und später leichter zu debuggen ist, wenn du sie in deinem täglichen Farmleben einsetzt.
Zuerst konzentrierst du dich darauf, den Code zu schreiben, der benötigt wird, um die Futtermenge pro Kuh zu berechnen.
Implementiere eine Methode TheFarm.divide_food, die ein Objekt vom Typ FodderCalculator und eine Anzahl Kühe als Ganzzahl als Argumente entgegennimmt.
Für diese Aufgabe gehst du davon aus, dass die übergebene Anzahl Kühe immer größer als null ist.
Die Methode sollte die Futtermenge pro Kuh zurückgeben.
Um die Berechnung durchzuführen, musst du zuerst die Gesamtfuttermenge für alle Kühe abrufen.
Das machst du, indem du die Methode fodder_amount! aufrufst und die Anzahl der Kühe übergibst.
Außerdem brauchst du einen Faktor, mit dem diese Menge multipliziert werden muss.
Diesen Faktor erhältst du, indem du die Methode fattening_factor! aufrufst.
Mit diesen beiden Werten und der Anzahl der Kühe kannst du nun die Futtermenge pro Kuh berechnen (als float64).
Wenn eine der Methoden, die du aufrufst, einen Fehler zurückgibt, sollte die Ausführung stoppen und 0 zurückgeben.
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
Während du an der ersten Aufgabe oben gearbeitet hast, hast du gemerkt, dass die externe Bibliothek, die du verwendest, nicht so hochwertig ist, wie du dachtest. Zum Beispiel kann sie ungültige Eingaben nicht richtig verarbeiten. Du möchtest diese Einschränkung umgehen, indem du in deinem eigenen Code eine Prüfung des Eingabewerts hinzufügst.
Schreibe eine Funktion TheFarm.validate_input_and_divide_food, die dieselbe Signatur hat wie TheFarm.divide_food oben.
TheFarm.divide_food aufrufen und die Ergebnisse dieses Aufrufs zurückgeben."Number of cows must be greater than 0" auslösen.TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
Die Anzahl der Kühe zu prüfen, bevor du sie weitergibst, war ein guter Schachzug, aber mit der unspezifischen Fehlermeldung bist du nicht ganz zufrieden.
Du beschließt, es besser zu machen, indem du eine eigene Fehlerklasse namens InvalidCowsError erstellst.
Der eigene Fehler sollte die Anzahl der Kühe (Int32) und eine eigene Meldung (String) enthalten, und die Methode Expection sollte die Daten im folgenden Format serialisieren:
{number of cows} cows are invalid: {custom message}
Ausgestattet mit deinem eigenen Fehler, implementiere eine Funktion TheFarm.validate_number_of_cows, die die Anzahl der Kühe als Ganzzahl entgegennimmt und einen Fehler (oder nil) zurückgibt.
InvalidCowsError zurück, bei dem die eigene Meldung auf "there are no negative cows" gesetzt ist.InvalidCowsError zurück, bei dem die eigene Meldung auf "no cows don't need food" gesetzt ist.nil zurück, um anzuzeigen, dass die Validierung erfolgreich war.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
Nach der harten Arbeit, diese Validierungsfunktion aufzusetzen, fällt dir auf, dass es bereits Abend ist, und du verlässt deinen Schreibtisch, um den Sonnenuntergang über den Bergen zu genießen. Die Aufgabe, die neue Validierungsfunktion tatsächlich in deinen Code einzubauen, verschiebst du auf einen anderen Tag.
Melde dich bei Exercism an, um Crystal mit 26 Konzepte142 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.