1. Pourquoi parler de la capture de variables par une closure
Quand vous venez juste de vous habituer aux fonctions, les closures ressemblent a des « fonctions de poche »: vous creez { ... }, vous le passez quelque part, et ca s'execute. Mais une closure a un superpouvoir: elle se souvient des valeurs de la portee externe. C'est pratique, parce qu'on peut « empaqueter » un comportement avec les donnees necessaires. Et c'est dangereux, parce qu'une closure peut retenir accidentellement un objet plus longtemps que prevu.
Commencons par un exemple parfaitement sur: la capture d'une variable numerique ordinaire. Ici, aucun drame ARC ne nous attend — nous allons simplement ressentir la mecanique.
import Foundation
func makeGreeter() -> () -> Void {
var counter = 0
return {
counter += 1
print("Hello #\(counter)") // Hello #1, then Hello #2, ...
}
}
let greet = makeGreeter()
greet()
greet()
La closure a capture counter et continue de le modifier, alors que la fonction makeGreeter() est depuis longtemps terminee. C'est un comportement normal et utile.
Les problemes commencent quand une closure capture un objet reference (class), surtout si cet objet stocke lui-meme la closure.
2. Comment apparait le cycle « objet → closure → objet »
Le scenario le plus courant a l'air innocent: un objet a une propriete callback (une closure), qui sera appelee « un jour ». Vous y stockez { ... }, et a l'interieur vous utilisez self, parce que « qui d'autre va appeler mes methodes ? ». Et la, sans fanfare, vous pouvez creer un retain cycle.
Le schema est simple :
- l'objet detient fortement la closure dans sa propriete;
- la closure capture fortement self (donc l'objet);
- on obtient un cercle dont ARC ne sait pas deviner que vous vouliez bien faire.
C'est exactement ce cycle classique self -> self.closure -> self, souvent cite comme exemple de fuite memoire.
Faisons un petit morceau pedagogique pour notre application console (que nous appellerons ici LibraryCLI). Imaginons qu'il existe un objet « session » qui stocke un gestionnaire d'evenement.
import Foundation
final class LibrarySession {
var onFinish: (() -> Void)?
deinit {
print("deinit LibrarySession")
}
func finish() {
onFinish?()
}
}
Ecrivons maintenant un controller qui possede la session et s'abonne a la fin :
import Foundation
final class LibraryController {
let session = LibrarySession()
deinit {
print("deinit LibraryController")
}
func start() {
session.onFinish = {
print("Session finished, controller reacts")
self.cleanup()
}
}
private func cleanup() {
print("cleanup()")
}
}
Cela semble logique. Mais si vous creez le controller, appelez start(), puis « relachez » la reference externe vers le controller, deinit peut ne jamais etre appele. La raison: LibraryController detient session, LibrarySession detient onFinish, et onFinish detient self (c'est-a-dire LibraryController).
On peut representer cela par un mini-schema :
flowchart LR
C[LibraryController] -->|strong| S[LibrarySession]
S -->|strong| F[onFinish closure]
F -->|captures strong| C
ARC regarde cela et dit: « Eh bien... LibraryController a clairement une reference forte (a l'interieur de la closure). Donc, on continue a vivre. » Et ce « on continue » peut durer indefiniment, tant que le programme tourne.
3. Capture list et [weak self]
Qu'est-ce qu'une capture list et ou se place-t-elle
Capture list est la liste des captures au debut d'une closure, ecrite entre crochets avant in. Elle existe justement pour vous permettre de controler explicitement la facon dont la closure retient les valeurs externes.
La syntaxe (en gros) ressemble a ceci :
{ [capture list] (params) -> ReturnType in
// body
}
En pratique, on rencontre le plus souvent ces variantes :
| Exemple | Signification |
|---|---|
|
Capturer self explicitement (fortement). |
|
Capturer self faiblement: ne pas retenir l'objet. |
|
Capturer self sans retention, mais avec la garantie stricte que self est vivant. |
|
Capturer la valeur x (souvent la « figer » au moment de la creation). |
|
Capturer faiblement un autre objet logger. |
Aujourd'hui, nous avons besoin de la variante la plus populaire: [weak self].
Important: meme si vous n'ecrivez rien dans la capture list, la closure peut quand meme capturer self. Simplement, cela se fait « par defaut » et ne saute pas visuellement aux yeux, ce qui rend le cycle de retention particulierement aga cant: le code a l'air normal, mais la memoire fuit comme si de rien n'etait.
Ce que fait [weak self] et pourquoi self devient Optional apres cela
Lorsque vous ecrivez [weak self], vous demandez a la closure de ne pas posseder l'objet. Autrement dit, la closure aura une « reference faible » vers l'objet: si l'objet disparait, la reference devient automatiquement nil. C'est la suite directe de ce que nous avons vu au sujet des references weak: elles ne retiennent pas l'objet et sont toujours optionnelles.
Donc, a l'interieur d'une telle closure, self devient de type Self? (par exemple, LibraryController?). Et le compilateur commence honnetement a exiger: « Montre-moi comment tu geres le cas ou self == nil. »
Autrement dit, on ne peut pas ecrire cela (apres [weak self]) :
session.onFinish = { [weak self] in
cleanup() // the compiler will complain: unclear what to do when self == nil
}
En revanche, ceci est possible, parce que nous avons defini le comportement de maniere explicite :
session.onFinish = { [weak self] in
self?.cleanup()
}
Ou encore via guard, ce qui se lit souvent mieux, surtout quand la logique depasse quelques lignes.
4. Deux styles avec [weak self]: guard et self?.
Avec [weak self], un debutant ressent en general deux emotions. La premiere: « Super, il n'y aura pas de fuite ! » La seconde, dix secondes plus tard: « Pourquoi y a-t-il partout des ? et des guard, et pourquoi le compilateur ne croit-il pas en mon amour de la proprete ? »
C'est une etape normale. En pratique, dans 90 % des cas, vous choisirez l'un des deux styles: soit « si self est mort, on sort calmement », soit « on effectue certaines actions si self est encore vivant ». Examinons les deux approches pour les choisir consciemment, et non « comme dans la premiere reponse trouvee sur internet ».
Style guard let self else { return }
Ce style est bon lorsque vous avez un morceau de logique qui n'a de sens que si l'objet est vivant, et que vous voulez que self ne « disparaisse » pas soudainement au milieu de l'execution (par exemple, pour qu'il n'arrive pas que vous ayez execute la premiere ligne, puis que l'objet soit deja libere).
Le patron ressemble a ceci :
session.onFinish = { [weak self] in
guard let self = self else { return }
self.cleanup()
print("Done")
}
Ici, une idee importante: apres guard let self = self, vous creez une reference locale forte self (qui masque la faible). Pendant l'execution de la closure, l'objet est retenu par cette reference locale forte, puis apres la sortie de la closure, la reference disparait.
Vous verrez aussi souvent une forme plus courte :
session.onFinish = { [weak self] in
guard let self else { return }
cleanup()
}
Ici, self est deja une constante locale (et non un « self capture » directement), donc on peut ensuite appeler les methodes sans self.: cleanup() se lit comme self.cleanup().
Appliquons cela a notre LibraryController, en corrigeant le cycle :
import Foundation
final class LibraryController {
let session = LibrarySession()
deinit { print("deinit LibraryController") }
func start() {
session.onFinish = { [weak self] in
guard let self else { return }
self.cleanup()
}
}
private func cleanup() {
print("cleanup()")
}
}
Maintenant, la closure ne retient plus le controller en permanence, et ARC pourra appeler deinit lorsque les references externes au controller disparaitront.
Style self?.
Ce style est bon lorsque la logique a l'interieur de la closure est tres courte, et que ce n'est pas grave si « rien ne se passe » lorsque l'objet a deja disparu.
session.onFinish = { [weak self] in
self?.cleanup()
}
Il est compact, mais il a un piege caracteristique: si vous avez 5 ou 6 lignes, vous devrez ecrire self?. plusieurs fois, et le code commence a ressembler a « je ne suis sur de rien ». De plus, parfois vous voulez faire tout ou rien, et self?. transforme l'execution en « peut-etre qu'on fera la moitie, si on a de la chance » — ce n'est pas toujours ce que vous voulez.
Si vous remarquez que self?. se repete plus de deux fois, c'est un bon signal pour passer a guard let self.
5. Nuances utiles: closures imbriquees et diagnostic
Implicit self apres unwrap et prudence avec les closures imbriquees
Une fois que vous avez fait guard let self else { return }, vous pouvez appeler les methodes sans self. — autrement dit, utiliser implicit self. C'est particulierement agreable dans du code avec weak self, car sinon il y aurait beaucoup trop de « bruit visuel ».
Exemple :
session.onFinish = { [weak self] in
guard let self else { return }
cleanup() // this means self.cleanup()
print("Finished")
}
Mais il y a un endroit ou il faut activer le « mode programmeur prudent »: les closures imbriquees.
Imaginez que, dans votre callback, vous creez encore une autre closure et que vous la stockez/transmettez quelque part. Les closures imbriquees peuvent a nouveau capturer self de maniere invisible et creer un nouveau cycle de retention. C'est pourquoi, pour les closures imbriquees, il faut controler explicitement la capture: soit reecrire capture list, soit ecrire explicitement self. — afin que le lecteur voie clairement ce qui se passe.
Petit exemple pedagogique en guise de « balise » (ici, l'idee compte plus que l'utilite) :
func configure() {
session.onFinish = { [weak self] in
guard let self else { return }
let later = { [weak self] in
guard let self else { return }
self.cleanup()
}
later()
}
}
Oui, c'est un peu plus long. Mais le lecteur du code voit immediatement: « ah, il y a encore une closure ici, et elle gere aussi la capture de self. »
Mini-diagnostic: comment verifier que [weak self] a vraiment aide
On aimerait beaucoup que, apres avoir ajoute [weak self], tout aille bien « par defaut ». Mais en pratique, il arrive que vous ayez mis [weak self] a un endroit, alors que le cycle continue a exister via une autre propriete, une collection ou une autre closure.
Le detecteur le plus simple dans les exemples pedagogiques est print dans deinit. Nous l'utilisons deja, et c'est une technique tout a fait normale aux premiers stades.
Construisons un mini-demo en top-level code pour voir la difference a l'oeil :
import Foundation
func demo() {
var controller: LibraryController? = LibraryController()
controller?.start()
controller?.session.finish()
controller = nil
print("controller is nil now")
}
demo()
Si tout est fait correctement, vous devez voir qu'apres controller = nil (ou juste apres la sortie de demo), deinit de LibraryController et deinit de LibrarySession sont declenches.
Si ce n'est pas le cas, cela signifie presque toujours: « il reste quelque part une reference forte ». La cause frequente est une closure stockee, qui retient l'objet via la capture de self.
Et encore un conseil pratique: si vous stockez une closure dans une propriete et qu'elle n'est necessaire que temporairement, il est parfois utile de se « desabonner » apres usage en lui assignant nil :
session.onFinish = { [weak self] in
guard let self else { return }
self.cleanup()
self.session.onFinish = nil // remove the subscription
}
Ce n'est pas un « remplacement de weak self », mais une hygiene supplementaire: vous supprimez les liens inutiles, et le graphe des references devient plus simple.
6. Erreurs typiques lors de la capture de self et de l'utilisation de [weak self]
Les premieres erreurs ici ne viennent pas souvent du fait que l'etudiant est « distrait », mais du fait que le cerveau n'est pas encore habitue a penser en graphe de references. Vous ecrivez le code comme une histoire d'actions, tandis que ARC vit comme un comptable: « qui reference qui, et qui retient qui ? ». Parcourons calmement les pieges les plus frequents, afin que vous les reconnaissiez au son avant meme la chute.
Erreur n°1: mettre [weak self], puis ecrire self! “parce que sinon ca ne compile pas”.
Cela arrive souvent: la personne voit que self est devenu Optional, et decide « tant pis, je suis sur ». Le probleme est que weak self dit justement: « l'objet peut disparaitre ». Si vous faites ensuite self!, vous transformez la disparition de l'objet en crash potentiel. La bonne habitude est soit guard let self else { return }, soit self?..
Erreur n°2: utiliser self?. la ou la logique doit s'executer entierement ou ne pas s'executer du tout.
self?.step1(); self?.step2(); self?.step3() a l'air compact, mais c'est une strategie « douce »: si self devient nil entre les lignes (ou si d'autres conditions apparaissent), vous pouvez obtenir une execution partielle. Quand une logique coherente est importante, guard let self rend l'intention beaucoup plus claire.
Erreur n°3: ne pas remarquer un second cycle via une closure imbriquee.
Vous avez corrige le callback principal: vous avez ajoute [weak self]. Puis, a l'interieur de ce callback, vous avez cree une autre closure et l'avez stockee/transmise quelque part — et elle a deja capture self fortement. Les closures imbriquees exigent une attention particuliere: la capture doit y etre clairement visible dans le code.
Erreur n°4: choisir unowned « parce que c'est plus court ».
Court, oui — sur, pas toujours. unowned exige un contrat reel: l'objet est garanti vivant au moment ou vous accedez a la reference. Si cette garantie n'existe pas, mieux vaut weak. Ici, nous nous concentrons sur [weak self] justement parce que, pour un debutant, c'est la strategie par defaut la plus sure.
Erreur n°5: attendre que deinit soit appele « juste apres la derniere utilisation », et non apres la disparition de la derniere reference forte.
C'est un piege psychologique subtil. Vous avez « arrete d'utiliser » l'objet, mais quelque part dans une collection, une propriete ou une closure, une reference forte existe encore — et ARC maintient honnetement l'objet en vie. Donc deinit n'est pas une recompense pour un bon comportement, mais simplement un signal: « il n'y a plus de references fortes ».
GO TO FULL VERSION