Swift a introduit la gestion des erreurs structurées dans sa version 2.0, remplaçant les motifs de pointeur d'erreur de Objective-C par des sémantiques natives throw et catch. Le mot clé rethrows est apparu pour résoudre le frottement spécifique où des fonctions d'ordre supérieur génériques comme map ou filter forçaient les appelants à utiliser try même lorsqu'ils passaient des closures non-throwing, créant une cérémonie de gestion des erreurs inutile.
Le problème est centré sur la polymorphie des effets des fonctions et le sous-typage. Dans le système de types de Swift, une closure non-throwing est un sous-type d'une closure throwing car elle satisfait le contrat "peut lancer" en ne lançant jamais. Sans rethrows, une fonction acceptant une closure throwing doit propager les erreurs de manière inconditionnelle, forçant tous les points d'appel à gérer les erreurs indépendamment du comportement réel de l'argument.
La solution est l'annotation rethrows, qui établit un contrat conditionnel : la fonction ne lance que si son paramètre de closure lance. Le compilateur Swift met cela en œuvre en suivant la capacité de lancer des arguments de closure au moment de la compilation. Lorsqu'une closure non-throwing est passée, la fonction est considérée comme non-throwing au site d'appel, éliminant le besoin de try; lorsqu'une closure throwing est passée, la fonction hérite de l'effet throwing.
Nous construisions un pipeline de transformation de données modulaire pour une application iOS où les utilisateurs pouvaient enchaîner des opérations comme le parsing JSON, le redimensionnement d'images et le hachage cryptographique. La fonction de base pipeline acceptait un tableau de transformations définies comme (Data) throws -> Data. Au départ, nous utilisions une annotation throws standard sur pipeline, ce qui forçait chaque site d'appel à encapsuler même des transformations simples dans des blocs do-catch malgré de nombreuses opérations étant des fonctions pures sans modes d'échec.
Notre première approche dupliquait l'ensemble de la fonction : une version nommée pipeline pour les transformations non-throwing et une autre nommée pipelineThrowing pour celles qui lançaient des erreurs. Cette séparation permettait des sites d'appel clairs mais créait un cauchemar de maintenance où chaque correction de bogue nécessitait d'éditer deux emplacements, et la surface de l'API doublait avec chaque nouvelle option de configuration. De plus, les utilisateurs devaient connaître les détails d'implémentation pour choisir la méthode correcte, violant les principes d'encapsulation.
La deuxième approche conservait une seule signature throws mais encourageait l'utilisation de try? pour faire taire les avertissements, écartant effectivement les informations d'erreur et rendant le débogage impossible lorsque de vraies erreurs se produisaient. Cela violait les garanties de sécurité et rendait le code fragile, les développeurs oubliaient de gérer les véritables cas d'erreur dans des pipelines mixtes contenant des opérations à la fois sûres et non sûres.
Nous avons finalement adopté la solution rethrows, en déclarant func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. Cela permettait au compilateur d'exiger try uniquement lorsque le tableau de closures contenait des opérations qui lançaient des erreurs, tout en permettant des appels directs pour des calculs purs. Le résultat fut une réduction de 40 % du code standard, l'élimination des signatures de fonction en double, et une ergonomie améliorée de l'API où le système de types reflétait fidèlement les véritables domaines d'erreur des cas d'utilisation spécifiques.
Pourquoi Swift interdit-il de lancer des erreurs directement dans le corps d'une fonction rethrows plutôt que exclusivement via le paramètre de closure ?
Le mot clé rethrows crée un contrat de transparence strict stipulant que la fonction ne propage que les erreurs générées par ses arguments. Si vous essayez de throw CustomError() directement dans le corps de la fonction, le compilateur Swift le rejette car cela représente un lancement inconditionnel, violant la garantie "uniquement si la closure lance". La fonction doit soit gérer ses propres erreurs en interne à l'aide de do-catch, soit les convertir en valeurs de retour, soit élever la signature à throws inconditionnel, garantissant que les appelants peuvent supposer en toute sécurité qu'aucun nouveau domaine d'erreur n'émerge de la fonction elle-même.
Comment rethrows interagit-il avec plusieurs paramètres de closure, et quelles sont les implications pour la propagation des effets ?
Lorsqu'une fonction a plusieurs paramètres de closure marqués comme lançant des erreurs et que la fonction elle-même est marquée rethrows, la fonction lance si l'un des closures lance, créant une union d'effets. Le compilateur de Swift suit ces effets individuellement à travers la chaîne d'appels, donc composer des fonctions rethrows préserve la nature conditionnelle sans intervention manuelle. Cependant, si vous transformez ou enveloppez les closures avant de les passer, vous devez préserver la signature throwing dans l'enveloppe, sinon le compilateur considérera l'argument comme non-throwing, faisant perdre à la fonction extérieure sa capacité de lancer conditionnelle.
Quelle est la relation entre rethrows et @autoclosure, et pourquoi ce modèle apparaît-il dans les APIs d'assertion ?
La combinaison de @autoclosure et rethrows permet une évaluation paresseuse avec propagation conditionnelle des erreurs, où l'autoclosure retarde l'évaluation jusqu'à ce qu'elle soit nécessaire et la fonction ne lance que si cette évaluation différée lance. Ce modèle alimente les fonctions assert et precondition de Swift, permettant de passer des expressions lançantes aux assertions sans marquer l'appel d'assertion avec try. Les candidats oublient souvent que l'autoclosure doit déclarer explicitement () throws -> T pour participer au contrat rethrows, et que ce mécanisme sépare le moment de l'évaluation (paresseux) des sémantiques de propagation des erreurs (conditionnelles), ce qui est crucial pour les chemins de code critiques en termes de performance où les assertions sont désactivées dans les builds de production.