SwiftProgrammationDéveloppeur Swift

Décrivez le mécanisme par lequel les blocs **defer** garantissent un ordre d'exécution LIFO lors de la sortie de la portée, et expliquez pourquoi ce comportement assure la sécurité des ressources même lorsque plusieurs instructions **defer** sont entrelacées avec des instructions de contrôle de flux comme **throw** ou **return**.

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Swift implémente l'instruction defer par le biais d'une pile de fermetures générée par le compilateur, attachée à chaque portée lexicale. Lorsque le compilateur rencontre un bloc defer, il extrait le code dans une fermeture et l'enregistre avec l'enregistrement de nettoyage de la portée actuelle. Lors de la sortie de la portée — que ce soit par un flux normal, un return, un throw ou un break — le runtime exécute ces fermetures dans l'ordre Last-In-First-Out (LIFO). Cette discipline de pile garantit que les ressources acquises plus tard sont libérées en premier, préservant les chaînes de dépendance sans comptabilité manuelle.

Historique de la question

Le nettoyage des ressources s'est historiquement appuyé soit sur des destructeurs déterministes, soit sur un traitement des exceptions verbeux. C++ couple le nettoyage aux durées de vie des objets via RAII, tandis que Java et C# nécessitent des blocs try-finally explicites qui séparent la logique de nettoyage du code d'acquisition. Go a introduit l'instruction defer pour fournir un nettoyage basé sur la portée sans surcharge orientée objet, influençant la conception de Swift. Swift a adopté defer dans la version 2.0 pour compléter son modèle de gestion des erreurs, offrant une alternative déclarative à finally qui s'intègre parfaitement avec les instructions guard et les retours anticipés.

Le problème

Les fonctions complexes avec plusieurs chemins de sortie — telles que les opérations sur les fichiers avec authentification, journalisation et transmission réseau — nécessitent une gestion des ressources méticuleuse. Les développeurs doivent s'assurer que chaque site de return ou de throw libère toutes les ressources acquises précédemment, des descripteurs de fichiers aux signets de ressources sécurisées. Omettre un seul point de nettoyage entraîne des fuites ou des blocages, tandis qu'un ordre incorrect (fermer une base de données avant de vider son journal de transactions) cause une corruption des données. Le nettoyage manuel devient ingérable à mesure que la complexité de la fonction augmente, créant ainsi un besoin de dispositions automatiques, déterministes et ordonnées des ressources liées aux limites de portée.

La solution

Le compilateur Swift transforme les instructions defer en une pile de pointeurs de fonction stockés dans l'enregistrement d'activation de la portée englobante. Chaque defer pousse sa fermeture sur cette pile gérée par le compilateur pendant l'exécution. Lorsque le flux de contrôle atteint l'accolade de fermeture de la portée ou rencontre une instruction de sortie, le code d'épilogue injecté itère à travers la pile à l'envers, exécutant chaque fermeture. Ce mécanisme s'intègre à la gestion des erreurs de Swift en garantissant que tous les blocs defer en attente s'exécutent avant qu'une erreur ne se propage vers une portée catch externe, garantissant que le nettoyage se produit indépendamment du chemin de sortie.

Situation de la vie réelle

Considérez une application iOS exportant des données utilisateur chiffrées. Le processus acquiert une URL de ressource sécurisée, ouvre un FileHandle, écrit des octets chiffrés et télécharge le résultat. Chaque étape peut échouer et nécessite un nettoyage strict pour éviter de fuir des descripteurs de fichiers ou des signets de ressources persistants.

Solution 1 : Nettoyage manuel à chaque point de sortie.

Les développeurs pourraient dupliquer fileHandle.close() et url.stopAccessingSecurityScopedResource() avant chaque return ou throw. Cette approche est fragile ; l'ajout d'un nouveau contrôle d'erreur nécessite de mettre à jour plusieurs sites, et les réviseurs doivent vérifier que l'ordre de nettoyage reflète l'ordre d'acquisition. Le risque de fuites augmente avec chaque nouveau point de sortie ajouté lors de la maintenance.

Solution 2 : Objets d'emballage avec deinit.

Créer une classe ScopeManager qui effectue le nettoyage dans son deinit repose sur ARC. Cependant, ARC ne garantit pas de désallocation immédiate à la sortie de la portée ; les objets peuvent persister jusqu'à ce que la piscine d'autofinissantes se vide ou que la variable soit réécrite. Dans les boucles de longue durée, cela retarde la libération des ressources, entraînant des erreurs système "trop de fichiers ouverts" difficiles à reproduire.

Solution 3 : Blocs defer.

L'équipe a déclaré les blocs defer immédiatement après l'acquisition de chaque ressource :

func exportData() throws { let url = try acquireResource() defer { url.stopAccessingSecurityScopedResource() } let fileHandle = try FileHandle(forWritingTo: url) defer { fileHandle.close() } let encrypted = try encrypt(data) try fileHandle.write(encrypted) try upload(fileHandle) }

Lorsqu'une erreur de chiffrement a déclenché un throw, le runtime a automatiquement fermé le gestionnaire de fichiers puis a cessé d'accéder à la ressource, maintenant l'ordre inverse correct. Cette solution a été choisie pour son déterminisme et sa proximité — le code de nettoyage apparaît adjacent au code d'acquisition.

Résultat :

La fonctionnalité d'exportation a passé des tests de stress avec 10 000 opérations concurrentes sans fuites de descripteurs de fichiers. La révision du code a révélé zéro chemin de nettoyage manqué, et le profilage a montré une libération immédiate des ressources par rapport à l'approche deinit.

Ce que les candidats manquent souvent

Question 1 : Un bloc defer s'exécute-t-il si la fonction se termine via fatalError ou une boucle infinie ?

Non. defer s'exécute uniquement lorsque le flux de contrôle atteint la fin de sa portée englobante. Si fatalError est invoqué, le processus se termine immédiatement sans défaire les portées ni exécuter les blocs de nettoyage. De même, une boucle while infinie empêche la portée de sortir ; les blocs defer à l'intérieur du corps de la boucle s'exécutent uniquement lorsque l'itération est terminée, mais une boucle while true au niveau de la fonction ne déclenche jamais les blocs defer au niveau de la fonction.

Question 2 : Comment defer gère-t-il la capture de variables lorsque la variable est modifiée après la déclaration du defer ?

defer capture les variables par référence par défaut, non par valeur. Par exemple :

var count = 0 defer { print("Retardé : \(count)") } count = 5 // Imprime 5, pas 0

Pour capturer la valeur au moment de la déclaration, les développeurs doivent utiliser une liste de capture explicite : defer { [value = currentValue] in ... }. Les candidats supposent souvent que defer capture un instantané au moment de la déclaration, ce qui entraîne des erreurs de logique dans les boucles ou les algorithmes mutables.

Question 3 : Quel est l'ordre d'exécution lorsque les blocs defer sont imbriqués à l'intérieur de branches conditionnelles par rapport à la portée parente ?

Les blocs defer sont liés à la portée lexicale dans laquelle ils apparaissent, et non à la portée de la fonction. Un defer à l'intérieur d'un bloc if s'exécute lorsque ce bloc if sort, et non à la sortie de la fonction. S'il existe plusieurs blocs defer à différents niveaux d'imbrication, le defer de la portée la plus interne s'exécute d'abord lors de la sortie de ce bloc spécifique. Cela entraîne un ordre contre-intuitif lorsque les développeurs s'attendent à ce que tous les blocs defer s'exécutent à la sortie de la fonction, en particulier lors de l'entrelacement de defer avec des instructions guard qui créent des sorties de sous-portée anticipées.