Swift führte in Version 2.0 das strukturierte Fehlerhandling ein und ersetzte die Fehlerzeiger-Schemata von Objective-C durch native throw und catch Semantiken. Das rethrows Schlüsselwort entstand, um die spezifische Reibung zu lösen, bei der generische höherordentliche Funktionen wie map oder filter die Aufrufer zwangen, try zu verwenden, selbst wenn sie nicht werfende Closures übergaben, und somit unnötige Fehlerbehandlungszeremonien erzeugten.
Das Problem liegt im Bereich der Funktionswirkungspolymorphismus und Subtypisierung. Im Typensystem von Swift ist ein nicht werfendes Closure ein Subtyp eines werfenden Closures, da es das "kann werfen"-Vertrags erfüllt, indem es niemals wirft. Ohne rethrows muss eine Funktion, die ein werfendes Closure akzeptiert, bedingungslos Fehler propagieren, was alle Aufrufstellen zwingt, Fehler zu behandeln, unabhängig vom tatsächlichen Verhalten des Arguments.
Die Lösung ist die rethrows-Annotation, die einen bedingten Vertrag etabliert: Die Funktion wirft nur, wenn ihr Closure-Parameter wirft. Der Swift-Compiler implementiert dies, indem er die Werf-Eigenschaft der Closure-Argumente zur Compile-Zeit verfolgt. Wenn ein nicht werfendes Closure übergeben wird, wird die Funktion an der Aufrufstelle als nicht werfend behandelt, was die Notwendigkeit für try beseitigt; wenn ein werfendes Closure übergeben wird, übernimmt die Funktion die Werf-Wirkung.
Wir bauten eine modulare Datenverarbeitungspipeline für eine iOS-Anwendung, bei der Benutzer Operationen wie JSON-Parsing, Bildverkleinerung und kryptographisches Hashing verketten konnten. Die Kernfunktion pipeline akzeptierte ein Array von Transformationen, das als (Data) throws -> Data definiert war. Zunächst verwendeten wir eine Standard-throws-Annotation für pipeline, die jede Aufrufstelle zwang, selbst einfache Transformationen in do-catch-Blöcke zu wickeln, trotz vieler Operationen, die reine Funktionen ohne Fehlermodi waren.
Unser erster Ansatz duplizierte die gesamte Funktion: eine Version namens pipeline für nicht werfende Transformationen und eine andere namens pipelineThrowing für werfende. Diese Trennung erlaubte saubere Aufrufstellen, führte jedoch zu einem Wartungsnightmare, bei dem jede Fehlerbehebung das Bearbeiten von zwei Stellen erforderte, und die API-Oberfläche sich mit jeder neuen Konfigurationsoption verdoppelte. Darüber hinaus mussten die Benutzer Implementierungsdetails kennen, um die richtige Methode auszuwählen, was die Prinzipien der Kapselung verletzte.
Der zweite Ansatz behielt eine einzelne throws-Signatur bei, ermutigte jedoch zur Verwendung von try?, um Warnungen zu beschwichtigen, was effektiv Fehlerinformationen verwertete und das Debuggen unmöglich machte, wenn tatsächliche Fehler auftraten. Dies verletzte Sicherheitsgarantien und machte den Code brüchig, da Entwickler es vergaßen, echte Fehlerfälle in gemischten Pipelines beizubehalten, die sowohl sichere als auch unsichere Operationen enthielten.
Letztendlich übernahmen wir die rethrows-Lösung und deklarierten func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. Dadurch konnte der Compiler try nur durchsetzen, wenn das Closure-Array werfende Operationen enthielt, während direkte Aufrufe für reine Berechnungen erlaubt waren. Das Ergebnis war eine 40%ige Reduzierung des Boilerplate-Codes, die Beseitigung doppelter Funktionssignaturen und verbesserte API-Ergonomie, bei der das Typensystem die tatsächlichen Fehlerdomänen spezifischer Anwendungsfälle genau widerspiegelte.
Warum verbietet Swift die direkte Fehlerweitergabe innerhalb eines rethrows-Funktionskörpers und nicht ausschließlich über den Closure-Parameter?
Das rethrows-Schlüsselwort schafft einen strikten Transparenzvertrag, der besagt, dass die Funktion nur Fehler propagiert, die von ihren Argumenten erzeugt werden. Wenn Sie versuchen, direkt im Funktionskörper throw CustomError() zu verwenden, lehnt der Swift-Compiler dies ab, da dies einen bedingungslosen Wurf darstellt, was das "nur wenn das Closure wirft"-Versprechen verletzt. Die Funktion muss entweder ihre eigenen Fehler intern mithilfe von do-catch bearbeiten, sie in Rückgabewerte umwandeln oder die Signatur auf einen bedingungslosen throws erhöhen, um sicherzustellen, dass die Aufrufer davon ausgehen können, dass keine neuen Fehlerdomänen von der Funktion selbst ausgehen.
Wie interagiert rethrows mit mehreren Closure-Parametern und welche Auswirkungen hat dies auf die Fehlerweitergabe?
Wenn eine Funktion mehrere als werfend markierte Closure-Parameter hat und die Funktion selbst als rethrows markiert ist, wirft die Funktion, wenn irgendeines der Closures wirft, was eine Vereinigung von Wirkungen schafft. Der Compiler von Swift verfolgt diese Wirkungen einzeln über die Aufrufkette, sodass das Zusammensetzen von rethrows-Funktionen die bedingte Natur ohne manuelles Eingreifen erhält. Wenn Sie jedoch die Closures transformieren oder umschließen, bevor Sie sie übergeben, müssen Sie die werfende Signatur im Wrapper beibehalten, andernfalls behandelt der Compiler das Argument als nicht werfend, was dazu führt, dass die äußere Funktion ihre bedingte Wurf-Fähigkeit verliert.
Wie steht rethrows in Beziehung zu @autoclosure und warum erscheint dieses Muster in Assertions-APIs?
Die Kombination von @autoclosure und rethrows ermöglicht eine verzögerte Auswertung mit bedingter Fehlerweitergabe, bei der die Autoclosure die Auswertung bis zur Notwendigkeit verzögert und die Funktion nur wirft, wenn diese verzögerte Auswertung wirft. Dieses Muster treibt die Funktionen assert und precondition von Swift an, die es ermöglichen, werfende Ausdrücke an Assertions zu übergeben, ohne den Assertionsaufruf mit try zu kennzeichnen. Kandidaten übersehen oft, dass die Autoclosure explizit () throws -> T deklarieren muss, um am rethrows-Vertrag teilzunehmen, und dass dieser Mechanismus die Auswertungszeit (verzögert) von der Fehlerweitergabesemantik (bedingt) trennt, was für leistungsoptimierte Codepfade entscheidend ist, in denen Assertions in Release-Bauten deaktiviert sind.