Swift introduceerde gestructureerde foutafhandeling in versie 2.0, waarbij de fout-pointer patronen van Objective-C werden vervangen door native throw en catch semantiek. Het sleutelwoord rethrows ontstond om de specifieke wrijving op te lossen waarbij generieke hogere-orde functies zoals map of filter de aanroepers dwongen om try te gebruiken, zelfs wanneer ze niet-gooiende closures doorgaven, wat onnodige foutafhandelingsceremonieën creëerde.
Het probleem draait om functie-effect polymorfisme en subtype. In het type-systeem van Swift is een niet-gooiende closure een subtype van een gooiende closure omdat deze voldoet aan het "kan gooien" contract door nooit te gooien. Zonder rethrows moet een functie die een gooiende closure accepteert onvoorwaardelijk gooien, waardoor alle aanroep locaties gedwongen worden om fouten te verwerken, ongeacht het werkelijke gedrag van de argumenten.
De oplossing is de rethrows annotatie, die een voorwaardelijk contract vastlegt: de functie gooit alleen als de closure-parameter gooit. De Swift compiler implementeert dit door het gooien van closure-argumenten tijdens de compileertijd bij te houden. Wanneer een niet-gooiende closure wordt doorgegeven, wordt de functie op de aanroep locatie behandeld als niet-gooiend, wat de noodzaak van try elimineert; wanneer een gooiende closure wordt doorgegeven, erft de functie het gooi-effect.
We bouwden een modulaire gegevens-transformatiepijplijn voor een iOS-applicatie waar gebruikers operaties zoals JSON-parseren, afbeeldingsgrootte aanpassing en cryptografische hashing konden ketenen. De kernfunctie pipeline accepteerde een array van transformaties gedefinieerd als (Data) throws -> Data. Aanvankelijk gebruikten we een standaard throws annotatie op pipeline, wat ervoor zorgde dat elke aanroep locatie zelfs eenvoudige transformaties in do-catch blokken moest wikkelen, ondanks dat veel operaties pure functies waren zonder foutmogelijkheden.
Onze eerste benadering dupliceerde de gehele functie: één versie genaamd pipeline voor niet-gooiende transformaties en een andere genaamd pipelineThrowing voor gooiende. Deze scheiding zorgde voor schone aanroep locaties, maar creëerde een onderhoudsnachtmerrie waarbij elke bugfix vereist was om twee locaties te bewerken, en het API oppervlak verdubbelde met elke nieuwe configuratie-optie. Bovendien moesten gebruikers de implementatiedetails kennen om de juiste methode te kiezen, wat de principes van encapsulatie schaadde.
De tweede benadering hield een enkele throws handtekening, maar moedigde het gebruik van try? aan om waarschuwingen te verdoezelen, wat effectief foutinformatie negeerde en het debuggen onmogelijk maakte wanneer daadwerkelijke fouten optraden. Dit schond de veiligheidsgaranties en maakte de code broos, aangezien ontwikkelaars zouden vergeten om echte foutgevallen te verwerken in gemengde pijplijnen met zowel veilige als onveilige operaties.
Uiteindelijk adopteerden we de rethrows oplossing, waarbij we verklaarden func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. Hierdoor kon de compiler afdwingen dat try alleen werd gebruikt als de array van closures gooiende operaties bevatte, terwijl directe aanroepen voor pure berekeningen mogelijk waren. Het resultaat was een vermindering van 40% in boilerplate-code, de eliminatie van dubbele functietekens en verbeterde API-ergonomie waarbij het type-systeem nauwkeurig de werkelijke foutdomeinen van specifieke gebruiksgevallen weerspiegelde.
Waarom verbiedt Swift het direct gooien van fouten binnen een rethrows functie lichaam in plaats van uitsluitend via de closure parameter?
Het rethrows sleutelwoord creëert een strikt transparantiecontract dat stelt dat de functie alleen fouten doorgeeft die door zijn argumenten zijn gegenereerd. Als je probeert throw CustomError() direct in het functie lichaam te gooien, weigert de Swift compiler dit, omdat dit een onvoorwaardelijke throw vertegenwoordigt, wat het "alleen als de closure gooit" garantie schendt. De functie moet zijn eigen fouten intern afhandelen met do-catch, ze omzetten in retourwaarden, of de handtekening verhogen naar onvoorwaardelijke throws, zodat aanroepers veilig kunnen aannemen dat er geen nieuwe foutdomeinen van de functie zelf afkomstig zijn.
Hoe interageert rethrows met meerdere closure parameters, en wat zijn de implicaties voor effectpropagatie?
Wanneer een functie meerdere closure parameters heeft die als gooiend zijn gemarkeerd en de functie zelf is gemarkeerd als rethrows, gooit de functie als iedere van de closures gooit, wat een vereniging van effecten creëert. De compiler van Swift houdt deze effecten individueel bij via de aanroepketen, waardoor het samenstellen van rethrows functies de voorwaardelijke aard behoudt zonder handmatige tussenkomst. Echter, als je de closures transformeert of verpakt voordat je ze doorgeeft, moet je de gooi-handtekening in de wrapper behouden, anders behandelt de compiler het argument als niet-gooiend, waardoor de buitenste functie zijn voorwaardelijke gooi-capaciteit verliest.
Wat is de relatie tussen rethrows en @autoclosure, en waarom verschijnt dit patroon in Asseritie-API's?
De combinatie van @autoclosure en rethrows maakt trage evaluatie met voorwaardelijke foutpropagatie mogelijk, waarbij de autoclosure de evaluatie uitstelt tot deze nodig is en de functie alleen gooit als die vertraagde evaluatie gooit. Dit patroon aandrijft de assert en precondition functies van Swift, zodat gooiende expressies naar asserities kunnen worden doorgegeven zonder de aanroep van de asseritie met try te markeren. Kandidaten missen vaak dat de autoclosure expliciet () throws -> T moet verklaren om deel te nemen aan het rethrows contract, en dat dit mechanisme de timing van evaluatie (trage) scheidt van de foutpropagatie semantiek (voorwaardelijk), wat cruciaal is voor prestatiekritische codepaden waar asserities zijn uitgeschakeld in release-builds.