Die Array-Methode map
ist ebenso wie unzählige weitere Collection-Methoden aus der Swift-Standardbibliothek mit dem Schlüsselwort rethrows
deklariert:
// Definition von map in der Swift-Standardbibliothek
public struct Array<Element> : RandomAccessCollection,
MutableCollection
{
public func map<T>(_ transform: (Element) throws -> T) rethrows
-> [T]
}
Dieser Beitrag erklärt, was es mit rethrows
auf sich hat und wie Sie rethrows
in eigenen Funktionen bzw. Methoden verwenden.
Zum Verständnis dieses Artikels wäre es gut, wenn Sie die prinzipielle Idee der try/catch
-Syntax von Swift kennen. Die Grundlagen habe ich in diesem Blog vor einiger Zeit präsentiert.
Das Schlüsselwort rethrows
rethrows
kann anstelle von throws
nach der Parameterliste, aber vor einem eventuellen Rückgabedatentyp angegeben. Das Schlüsselwort rethrows
bedeutet für die obige map
-Methode:
- Wenn an
map
eine Funktion übergeben wird, die keinen Fehler auslöst, dann mussmap
nicht abgesichert werden.map
verspricht laut Deklaration, selbst keinen Fehler auszulösen. -
Die gute Nachricht besteht darin, dass Sie
map
sogar eine Funktion oder Closure übergeben dürfen, die selbst einen Fehler auslösen kann. Deswegen ist der Parametertransform
mitthrows
deklariert. Sollte bei der Ausführung vonmap
tatsächlich ein Fehler auftreten, gibtmap
diesen Fehler zurück. Der Fehler wird also neuerlich ausgelöst (daher die Bezeichnungrethrows
). -
Die logische Konsequenz besteht aber darin, dass Sie — sobald Sie an
map
eine Funktion mitthrows
übergeben — den gesamten Aufruf vonmap
selbst wieder absichern müssen.
Wie Sie anhand der folgenden Beispiele gleich sehen werden, gibt es dabei durchaus einige Sonderfälle zu beachten.
rethrows in der Praxis
Das folgende Beipiel geht davon aus, dass Sie eine Funktion f1
definiert haben, die unter bestimmten Umständen Fehler auslöst:
// eigene Fehlerzustände definieren
enum MyErrors : Error {
case tooSmall
case tooBig(maximum: Int)
case missing
case other(explanation: String)
}
// eigene Funktion, die Fehler auslösen kann
func f1(_ n: Int) throws -> Int {
if n < 0 { throw MyErrors.tooSmall }
if n > 100 { throw MyErrors.tooBig(maximum: 100) }
return n + 1
}
Die zu verarbeitenden Daten befinden sich in einem Array:
let data = [1, 2, 3, 200]
Wenn Sie map
auf data
anwenden und dabei die Closure { $0 + 1}
übergeben, kann nichts schief gehen. map
muss nicht durch try
abgesichert werden:
// new1 hat den Datentyp [Int], Ergebnis [2, 3, 4, 201]
let new1 = data.map() { $0 + 1}
Syntaktisch nicht erlaubt ist es hingegen, einfach f1
an map
zu übergeben. Xcode reklamiert call can throw but is not marked with try, und meint damit, dass map
jetzt sehr wohl einen Fehler auslösen kann — eben weil f1
mit throws
deklariert ist.
let new2 = data.map(f1) // Syntaxfehler, nicht erlaubt
Sie können sich jetzt für eine do-try-catch
-Konstruktion entscheiden oder wie im folgenden Beispiel einfach try?
voranstellen. new3
enthält in diesem Fall als Ergebnis nil
, weil f1
beim data
-Element 200
einen Fehler auslöst.
// new3 hat den Datentyp [Int]?, Ergebnis nil
let new3 = try? data.map(f1)
Auf den ersten Blick sieht die folgende Variante gleichwertig aus: f1
wird nicht unmittelbar als Parameter von map
übergeben, sondern nachgestellt in eine Closure verpackt. Die Variante ist aber nicht gleichwertig. Durch die Verpackung betrachtet Swift die Closure als neue Funktion — und zwar als eine, die nicht mit throws
deklariert ist. (Das ist syntaktisch in Swift nicht vorgesehen.)
let new4 = try? data.map() { f1($0) } // Syntaxfehler, nicht erlaubt
Der Compiler beklagt auch bei der obigen Zeile, dass ein Funktionsaufruf einen Fehler verursachen kann, aber nicht durch try
abgesichert werden kann. Diesmal meint er aber nicht map
, sondern f1
. Und tatsächlich bietet ein try?
vor f1($0)
eine andere Variante, um f1
an map
zu übergeben:
// new5 hat den Datentyp [Int?], Ergebnis
// [Optional(2), Optional(3), Optional(4), nil]
let new5 = data.map() { try? f1($0) }
Die Closure kann nun keinen Fehler mehr auslösen. Vielmehr liefert f1
entweder eine Integer-Zahl oder nil
, der Datentyp ist also Int?
. Deswegen erhält new5
den Datentyp [Int?]
, nicht [Int]?
wie bei new3
.
rethrows in eigenen Funktionen und Methoden
Wenn Sie selbst Funktionen oder Methoden mit rethrows
kennzeichnen verpflichten Sie sich damit, selbst nur Fehler im catch
-Teil der Fehlerabsicherung Ihrer Funktion bzw. Methode auszulösen. An anderen Stellen ist throws
nicht erlaubt.
Die folgenden Zeilen zeigen eine minimalistische Neuimplementierung von map
für Arrays — zum besseren Verständnis zuerst eine Variante ohne rethrows
. Die transform
-Funktion verarbeitet also die Array-Elemente vom Typ Element
(dieser Name ist durch die Array
-Definition vorgegeben) und liefert Ergebnisse vom Typ T
. Dabei darf kein Fehler auftreten.
extension Array {
func map1<T>(_ transform: (Element) -> T) -> [T] {
var result = [T]()
for item in self {
result.append(transform(item))
}
return result
}
}
Die map2
-Variante akzeptiert wie die echte map
-Methode Transformationsfunktionen mit throws
und gibt gegebenenfalls die resultierenden Fehler mit rethrows
weiter. Die Deklaration sieht jetzt wie bei der richtigen map
-Methode aus. Die Implementierung ist möglicherweise weniger effizient, aber funktionell gleichwertig. Der Aufruf der transform
-Funktion erfolgt mit try
. Sollte dabei ein Fehler auftreten, wird
das resultierende Error
-Objekt im catch
-Zweig einfach durch throw
an den Aufrufer von map
weitergegeben. Hier findet also der Rethrow statt.
extension Array {
func map2<T>(_ transform: (Element) throws -> T) rethrows
-> [T]
{
var result = [T]()
for item in self {
do {
let newitem = try transform(item)
result.append(newitem)
} catch {
throw error
}
}
return result
}
}
Swifts Syntax erlaubt eine noch kürzere Variante, die auf do
und catch
verzichtet. try
ist weiter vorgeschrieben und hat dieselbe Wirkung wie die gesamte do-try-catch
-Konstruktion von map2
:
extension Array {
func map3<T>(_ transform: (Element) throws -> T) rethrows
-> [T]
{
var result = [T]()
for item in self {
let newitem = try transform(item)
result.append(newitem)
}
return result
}
}
Der obige Code verpackt in erstaunlich wenigen Zeilen viele fortgeschrittene Swift-Sprachmerkmale: neben der Fehlerbehandlung sind zur Definition einer neuen map
-Methode für Arrays auch Extensions und Generics mit im Spiel.
Ein kurzer Test stellt sicher, dass sie map3
wie map
verhält:
let new7 = try? data.map3(f1)
// new7: Datentyp [Int]?, Ergebnis nil
let new8 = data.map3() { try? f1($0) }
// new8: Datentyp [Int?],
// Ergebnis [Optional(2), Optional(3), Optional(4), nil]
Quellen
- https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Types.html
- https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html
- https://christiantietze.de/posts/2016/02/rethrows
- http://robnapier.net/throw-what-dont-throw