try/catch in Swift 2

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. An throw muss ein Element/Objekt übergeben werden, dessen Typ das ErrorType-Protokoll implementiert.

  • Der Aufruf von Methoden, die mit throws deklariert sind, muss mit try abgesichert werden. try gilt immer für eine gesamte Anweisung, nicht nur für einen einzelnen Methodenaufruf. Wenn xxx() und yyy() \linebreak Fehler auslösen können, lautet der korrekte Aufruf zur Berechnung der Summe dieser Funktionen try xxx() + yyy(), nicht try xxx() + try yyy(). Bei Zuweisungen muss try unmittelbar nach dem Zuweisungsoperator angegeben werden, also let c = try xxx() oder var v = try xxx(). Analog lautet die korrekte Syntax return try xxx(), nicht try return xxx().

  • try kann nicht für sich verwendet werden, sondern nur innerhalb eines do-Blocks. In diesem Block ist try beliebig oft erlaubt — aber eben nur für Anweisungen, die throws-Methoden aufrufen.

  • Zur Reaktion auf Fehler muss es zumindest einen catch-all-Block geben, also einfach catch oder catch _ oder catch 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 erzeugt throw ein NSError-Objekt, dessen Klasse schon seit vielen Jahren in der Foundation-Bibliothek existiert. try und catch verpacken lediglich die Verarbeitung von derartigen NSError-Objekten. Dafür ist try/catch in Swift schneller als in anderen Sprachen. Laut dem eBook »The Swift Programming Language« wird throw in Swift ebenso schnell ausgeführt wird wie return.

  • 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!, wenn x 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
}