In der erste Version von Swift fehlte eine try-catch
-Konstruktion zum Umgang mit Fehlern. Diese Entscheidung ist vielfach auf Unverständnis gestossen. In Version 2 hat Apple nachgebessert: Es gibt nun eine try/catch-Syntax, die ähnlich wie in anderen Programmiersprachen aussieht, auch wenn sie intern vollkommen anders und insbesondere ohne Exceptions implementiert ist.
Update 12.9.2015: Der Beitrag berücksichtigt jetzt auch »try!« und »try?«.
Das Wichtigste in aller Kürze
Unzählige Methoden aus dem Foundation-Framework sowie aus anderern Bibliotheken wurden ursprünglich für die Verwendung durch Objective C konzipiert. Bei der Verwendung dieser Programmiersprache, aber auch unter Swift 1.n, liefern diese Methoden true
oder false
zurück, je nachdem, ob sie erfolgreich waren oder nicht. Die Details über einen eventuell aufgetretenen Fehler können einem NSError
-Objekt entnommen werden. Unter Swift 1.n sah ein typischer Aufruf einer Methode mit NSError
-Rückgabeparameter wie folgt aus:
// alter Code für Swift 1.n
var err: NSError?
var s = "Hello World!"
// Zeichenkette s in eine Textdatei schreiben
let ok = s.writeToFile("/bla/test.txt",
atomically: false,
encoding: NSUTF8StringEncoding,
error: &err)
if !ok {
print("Fehler: " + err!.localizedDescription)
}
Für Swift 2 hat Apple die Deklarationen aller derartigen Methoden so abgewandelt, dass diese nun kompatibel zum try-catch
-Konzept sind. Der Aufruf derselben Methode zum Speichern einer Textdatei sieht in aktuellen Swift-Versionen so aus:
// neuer Code ab Swift 2
var s = "Hello World!"
do {
try s.writeToFile("/bla/test.txt",
atomically: false,
encoding: NSUTF8StringEncoding)
} catch let err as NSError {
print("Fehler: " + err.localizedDescription)
}
Wenn Sie weder am Fehler an sich noch an dessen Details interessiert sind, können Sie den obigen Code wie folgt verkürzen. (Das stille Ignorieren von Fehlern ist in der Praxis natürlich selten sinnvoll.)
// neuer Code ab Swift 2
var s = "Hello World!"
do {
try s.writeToFile("/bla/test.txt",
atomically: false,
encoding: NSUTF8StringEncoding)
} catch _ { }
Syntax: do, try, catch, throw und throws
Das folgende Listing fasst die wichtigsten Elemente der try/catch-Syntax in Swift 2 zusammen:
enum MyErrors : ErrorType {
case TooSmall
case TooBig(maximum:Int)
case Missing
case Other(explanation:String)
}
// Definition von Funktionen, die Fehler auslösen können
func f1(n:Int) throws -> Int {
if n<0 { throw MyErrors.TooSmall }
if n>100 { MyErrors.TooBig(maximum: 100) }
return n+1
}
func f2(n:Int?) throws -> Int {
if n == nil { throw MyErrors.Missing }
return n! + 1
}
// Test der Funktionen
do {
let a = try f1(190)
let b = try f2(a)
let c = a / (a-6) // kann nicht abgesichert werden
try print(12 + f1(1) + f2(7))
} catch MyErrors.TooSmall {
print("Fehler: Parameter zu klein")
} catch MyErrors.TooBig(let maximum) {
print("Fehler: Parameter zu groß")
print("Maximalwert = \(maximum)")
} catch {
print("Ein anderer Fehler:")
print(error)
}
print("Hier geht es weiter, wenn kein Fehler " +
"oder ein abgesicherter Fehler aufgetreten ist.")
Nun die Kurzfassung der Syntax:
- Eigene Fehlercodes werden in der Regel als Enumeration formuliert. Grundsätzlich sind auch andere Typen möglich, diese müssen aber auf jeden Fall das leere Protokoll
ErrorType
implementieren. (ErrorType
hat einen formalen Charakter und stellt Swift-intern die Kompatibilität zur NSError-Klasse her. Das Protokoll schreibt aber keine Methoden oder andere Implementierungsdetails vor.) -
Methoden und (Init-)Funktion, die Fehler auslösen können, müssen mit
throws
deklariert werden. Anders als z.B. in Java fehlt eine Angabe, welche Arten von Fehlern ausgelöst werden können. -
Innerhalb solcher Methoden kann
throw
einen Fehler auslösen. Anthrow
muss ein Element/Objekt übergeben werden, dessen Typ dasErrorType
-Protokoll implementiert. -
Der Aufruf von Methoden, die mit
throws
deklariert sind, muss mittry
abgesichert werden.try
gilt immer für eine gesamte Anweisung, nicht nur für einen einzelnen Methodenaufruf. Wennxxx()
undyyy()
\linebreak Fehler auslösen können, lautet der korrekte Aufruf zur Berechnung der Summe dieser Funktionentry xxx() + yyy()
, nichttry xxx() + try yyy()
. Bei Zuweisungen musstry
unmittelbar nach dem Zuweisungsoperator angegeben werden, alsolet c = try xxx()
odervar v = try xxx()
. Analog lautet die korrekte Syntaxreturn try xxx()
, nichttry return xxx()
. -
try
kann nicht für sich verwendet werden, sondern nur innerhalb einesdo
-Blocks. In diesem Block isttry
beliebig oft erlaubt — aber eben nur für Anweisungen, diethrows
-Methoden aufrufen. -
Zur Reaktion auf Fehler muss es zumindest einen catch-all-Block geben, also einfach
catch
odercatch _
odercatch let error as NSError
. -
Optional sind vorher zusätzliche
catch
-Blöcke für spezifische Fehler zulässig.
Noch mehr Details können Sie schon jetzt im Swift-eBook von Apple und demnächst in meinem Swift-Buch nachlesen.
Was try/catch in Swift kann — und was nicht …
Es gibt zwei große Missverständnisse rund um try/catch in Swift:
- Erstens: try/catch basiert nicht auf Exceptions! Das
try-catch
-Modell von Swift sieht so aus, als würde es wie z.B. in Java Exceptions verarbeiten. Tatsächlich ist das aber nicht der Fall. Vielmehr erzeugtthrow
einNSError
-Objekt, dessen Klasse schon seit vielen Jahren in der Foundation-Bibliothek existiert.try
undcatch
verpacken lediglich die Verarbeitung von derartigenNSError
-Objekten. Dafür ist try/catch in Swift schneller als in anderen Sprachen. Laut dem eBook »The Swift Programming Language« wirdthrow
in Swift ebenso schnell ausgeführt wird wiereturn
. -
Zweitens: try/catch kann keine Programmierfehler abfangen: Auf Schlampigkeitsfehler im Code reagiert ein Swift-Programm unbarmherzig — und daran hat sich mit Swift 2 nichts geändert. Zugriffe auf nicht vorhandene Array-Elemente, Integer-Divisionen durch 0, Integer-Überläufe, nil-Unwrapping (also
x!
, wennx
nil
enthält) führen auch in Swift 2 zu einem fatal error und somit zum Programmende. Derartige Fehler lassen sich mit try/catch nicht abfangen.
try! und try?
Zum gewöhnlichen Schlüsselwort try
existiert die Variante »try!« mit einem nachgestellten Ausrufezeichen (»Forced Try«). Im Unterschied zum gewöhnlichen try
darf try!
ohne eine do-catch
-Konstruktion verwendet werden, um Methoden aufzurufen, die einen Fehler auslösen können.
let a = try! f1(7)
try! f2(12)
In der Praxis verwenden Sie try!
nur dann, wenn Sie absolut sicher sind, dass kein Fehler auftreten wird. Passiert dies doch, wird der Fehler bei throws
-Methoden weitergegeben oder führt sonst unmittelbar zum Programmende (fatal error …).
Die in Xcode 7 Beta 6 eingeführte Variante »try?« (optionales try
) führt eine mit throws
deklarierte Funktion oder Methode aus. Tritt dabei tatsächlich ein Fehler auf, liefert try?
den Wert nil
zurück, andernfalls das Ergebnis. Die folgenden Zeilen machen dies deutlich:
var result1 = try? f1(10) // Datentyp Int?
print(result1) // Ergebnis Optional(11)
result1 = try? f1(1000)
print(result1) // Ergebnis nil
Bei Methoden, die an sich schon Optionals zurückgeben, führt try?
zu doppelten Optionals, also z.B. zum Datentyp Int??
(entspricht Optional<Optional<Int>>
).
func f5(n:Int) throws -> Int? {
if n<0 { return nil }
if n>100 { throw MyErrors.TooBig(maximum: 100) }
return n+1
}
let result5 = try? f5(10) // Datentyp Int??
if let n = result5 { // Datentyp Int?
if let m = n { // Datentyp Int
print(m) // Ausgabe 11
}
}
Übersichtlicher Code mit defer und guard
Die Schlüsselwörter defer
und guard
, beide ebenfalls neu in Swift 2, haben nichts mit try/catch zu tun. Sie können also auch in »gewöhnlichem« Code verwendet werden. Dessen ungeachtet sind defer
und guard
nützliche Hilfsmittel, um Code übersichtlicher zu strukturieren — und das gilt natürlich auch innerhalb von try/catch-Konstruktionen.
Mit defer können Sie Code formulieren, der auf jeden Fall beim Verlassen einer Funktion oder Methode ausgeführt wird — egal, ob die Funktion/Methode bis zum Ende ausgeführt wird, oder ob sie vorzeitig mit return
oder throw
verlassen wird. Eine Methode darf mehrere defer
-Blöcke aufweisen — dann werde diese zum Schluss in umgekehrter Reihenfolge ausgeführt.
Um defer
auszuprobieren, erstellen Sie am einfachsten ein kleines Projekt des Typs Command Line Tool und fügen den folgenden Code ein. Anschließend führen Sie das Programm mehrere Male aus. Ganz egal, welchen Verlauf der Code nimmt — in jedem Fall enthält die Ausgabe zu jeder open
– ein dazu passende close
-Zeile. (Im Playground lässt sich das nicht gut testen, weil print
-Ausgaben dort nicht chronologisch angezeigt werden.)
// Projekt defer-test, Datei main.swift
func test() {
print("open f1")
defer { print("close f1") }
if arc4random_uniform(100) > 50 { return }
print("open f2")
defer { print("close f2") }
print("read from f1")
if arc4random_uniform(100) > 50 { return }
print("write to f2")
}
test()
Nur zu guard: if-let
-Konstruktionen bergen in sich eine Tendenz zu unübersichtlichem Code: Es passiert recht oft, dass zuerst oft umfangreicher Code für den positiven Fall formuliert wird; zum Ende der Funktion folgen dann kurze Einzeiler, die ausgeführt werden, wenn if-let
nicht erfolgreich war. Das führt zu unnötig verschachteltem Code. Im folgenden Beispiel ruft f
zweimal die Funktion perhapsANumber
auf und verarbeitet die Ergebnisse.
func perhapsANumber() -> Int? {
let n = Int(arc4random_uniform(100))
if n <= 50 {
return n
} else {
return nil
}
}
// Beispielcode ohne guard
func f() {
if let a = perhapsANumber() {
let n = 2 * a
if let b = perhapsANumber() {
print(n + b)
// noch mehr Code, Teil 1
} else {
return
}
} else {
return
}
// noch mehr Code, Teil 2
}
guard
stellt die Funktion von if-let
gewissermaßen auf den Kopf: Ist die Bedingung erfüllt, wird der Code nach dem else
-Block ausgeführt, wo bei alle mit let
definierten Variablen weiterhin gültig sind. Ist die Bedingung hingegen nicht erfüllt, wird der else
-Block ausgeführt. Mit guard
lässt sich die gleiche Aufgabe wie in f
wesentlich eleganter erledigen:
// der gleiche Code mit guard
func g() {
guard let a = perhapsANumber() else { return }
let n = 2 * a
guard let b = perhapsANumber() else { return }
print(n + b)
// noch mehr Code, Teil 1
// noch mehr Code, Teil 2
}