Joseph Wright, le prisonnier - Illustration d'une capture forte
La liste des valeurs «capturées» se trouve devant la liste des paramètres de fermeture et peut «capturer» les valeurs du périmètre de trois manières différentes: en utilisant les liens «fort», «faible» ou «sans propriétaire». Nous l'utilisons souvent, principalement pour éviter les cycles de référence forts («cycles de référence forts» ou «conserver les cycles»).
Il peut être difficile pour un développeur novice de décider quelle méthode utiliser, vous pouvez donc passer beaucoup de temps à choisir entre «fort» et «faible» ou entre «faible» et «sans propriétaire», mais avec le temps, vous vous rendrez compte que le bon choix - un seul.
Créez d'abord une classe simple:
class Singer { func playSong() { print("Shake it off!") } }
Ensuite, nous écrivons une fonction qui crée une instance de la classe
Singer et renvoie une fermeture qui appelle la méthode
playSong () de la classe
Singer :
func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing }
Enfin, nous pouvons appeler
sing () n'importe où pour obtenir le résultat de la
lecture de playSong () let singFunction = sing() singFunction()
En conséquence, la ligne «Secouez-le!» Sera affichée.
Capture forte
Sauf si vous spécifiez explicitement une méthode de capture, Swift utilise une capture «forte». Cela signifie que la fermeture capture les valeurs externes utilisées et ne les laissera jamais libres.
Jetons à nouveau un œil à la fonction
sing () func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing }
La
constante de Taylor est définie à l'intérieur d'une fonction, donc dans des circonstances normales, sa place serait libérée une fois que la fonction a terminé son travail. Cependant, cette constante est utilisée à l'intérieur de la fermeture, ce qui signifie que Swift assurera automatiquement sa présence tant que la fermeture elle-même existera, même après la fin de la fonction.
Il s'agit d'une capture "forte" en action. Si Swift autorisait la libération de
taylor , appeler la fermeture serait dangereux - sa méthode
taylor.playSong () n'est plus valide.
Capture "faible" (capture faible)
Swift nous permet de créer une «
liste de capture » pour déterminer comment les valeurs utilisées sont capturées. Une alternative à la capture «forte» est «faible» et son application entraîne les conséquences suivantes:
1. Les valeurs capturées «faiblement» ne sont pas conservées par la fermeture et peuvent donc être libérées et réglées sur
zéro .
2. En conséquence du premier paragraphe, les valeurs capturées «faiblement» dans Swift sont toujours
facultatives .
Nous modifions notre exemple en utilisant une capture «faible» et voyons immédiatement la différence.
func sing() -> () -> Void { let taylor = Singer() let singing = { [weak taylor] in taylor?.playSong() return } return singing }
[taylor faible] - c'est notre "
liste de capture ", une partie spéciale de la syntaxe de fermeture dans laquelle nous donnons des instructions sur la façon dont les valeurs doivent être capturées. Ici, nous disons que
taylor doit être capturé "faiblement", nous devons donc utiliser
taylor? .PlaySong () - maintenant, il est
facultatif , car il peut être défini sur
nil à tout moment.
Si vous exécutez maintenant ce code, vous verrez que l'appel à
singFunction () n'entraîne plus de message. La raison en est que
taylor n'existe qu'à l'intérieur de
sing () , et la fermeture retournée par cette fonction ne maintient pas
taylor «fortement» à l'intérieur de lui-même.
Essayez maintenant de changer
taylor? .PlaySong () en
taylor! .PlaySong () . Cela entraînera un déballage forcé du
taylor à l'intérieur de la fermeture et, par conséquent, une erreur fatale (déballage du contenu contenant
zéro )
Capture "sans propriétaire" (capture sans propriétaire)
Une alternative à la capture «faible» est «sans propriétaire».
func sing() -> () -> Void { let taylor = Singer() let singing = { [unowned taylor] in taylor.playSong() return } return singing }
Ce code se terminera anormalement de la même manière avec l'option facultative déployée illustrée ci-dessus - un
taylor sans propriétaire dit: "Je sais avec certitude que
taylor existera pendant la durée de la fermeture, donc je n'ai pas besoin de le garder en mémoire." En fait,
taylor sera publié presque immédiatement et ce code plantera.
Alors, utilisez
-le très soigneusement sans
propriétaire .
Problèmes courants
Les développeurs sont confrontés à quatre problèmes lorsqu'ils utilisent la capture de valeur dans les fermetures:
1. Difficultés avec l'emplacement de la liste de capture dans le cas où la fermeture prend des paramètres
Il s'agit d'un problème courant que vous pouvez rencontrer au début de l'étude des fermetures, mais, heureusement, Swift nous aidera dans ce cas.
Lorsque vous utilisez la liste de capture et les paramètres de fermeture ensemble, la liste de capture est d'abord entre crochets, puis les paramètres de fermeture, puis le mot clé in, marquant le début du «corps» de fermeture.
writeToLog { [weak self] user, message in self?.addToLog("\(user) triggered event: \(message)") }
Tenter de mettre une liste de capture après les paramètres de fermeture entraînera une erreur de compilation.
2. L'émergence d'un cycle de liens forts, conduisant à une fuite de mémoire
Lorsqu'une entité A a une entité B, et vice versa, vous avez une situation appelée «cycle de rétention».
À titre d'exemple, considérons le code:
class House { var ownerDetails: (() -> Void)? func printDetails() { print("This is a great house.") } deinit { print("I'm being demolished!") } }
Nous avons défini la classe
House , qui contient une propriété (fermeture), une méthode et un désinitialiseur qui affichera un message lorsqu'une instance de la classe est détruite.
Créez maintenant une classe
Owner similaire à la précédente, sauf que sa propriété de fermeture contient des informations sur la maison.
class Owner { var houseDetails: (() -> Void)? func printDetails() { print("I own a house.") } deinit { print("I'm dying!") } }
Créez maintenant des instances de ces classes dans le bloc
do . Nous n'avons pas besoin d'un bloc catch, mais l'utilisation d'un bloc
do détruira les instances juste après}
print("Creating a house and an owner") do { let house = House() let owner = Owner() } print("Done")
En conséquence, des messages seront affichés: «Création d'une maison et d'un propriétaire», «Je meurs!», «Je suis en train d'être démoli!», Puis «Terminé» - tout fonctionne comme il se doit.
Créez maintenant une boucle de liens solides.
print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = owner.printDetails owner.houseDetails = house.printDetails } print("Done")
Maintenant, le message «Création d'une maison et d'un propriétaire» apparaît, puis «Terminé». Les désinitialiseurs ne seront pas appelés.
Cela est dû au fait que la maison a une propriété qui pointe vers le propriétaire et que le propriétaire a une propriété qui pointe vers la maison. Par conséquent, aucun d'entre eux ne peut être libéré en toute sécurité. Dans une situation réelle, cela conduit à des fuites de mémoire, ce qui entraîne des performances médiocres et même un crash de l'application.
Pour corriger la situation, nous devons créer une nouvelle fermeture et utiliser une capture «faible» dans un ou deux cas, comme ceci:
print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = { [weak owner] in owner?.printDetails() } owner.houseDetails = { [weak house] in house?.printDetails() } } print("Done")
Il n'est pas nécessaire de déclarer les deux valeurs capturées, il suffit de le faire en un seul endroit - cela permettra à Swift de détruire les deux classes si nécessaire.
Dans les projets réels, la situation d'un cycle aussi évident de liens forts se pose rarement, mais cela souligne d'autant plus l'importance d'utiliser la capture «faible» avec un développement compétent.
3. L'utilisation par inadvertance de liens solides, généralement lors de la capture de plusieurs valeurs
Swift utilise une forte adhérence par défaut, ce qui peut entraîner un comportement inattendu.
Considérez le code suivant:
func sing() -> () -> Void { let taylor = Singer() let adele = Singer() let singing = { [unowned taylor, adele] in taylor.playSong() adele.playSong() return } return singing }
Maintenant, nous avons deux valeurs capturées par la fermeture, et nous utilisons les deux de la même manière. Cependant, seul
Taylor est capturé comme non possédé - Adele
est capturé fortement car le mot-clé
Unowned doit être utilisé pour chaque valeur capturée.
Si vous l'avez fait exprès, alors tout va bien, mais si vous voulez que les deux valeurs soient capturées "sans
propriétaire ", vous avez besoin des éléments suivants:
[unowned taylor, unowned adele]
4. Copiez les fermetures et partagez les valeurs capturées
Le dernier cas sur lequel les développeurs tombent est la façon dont les erreurs sont copiées car les données qu'elles capturent deviennent disponibles pour toutes les copies de l'erreur.
Prenons un exemple de fermeture simple qui capture la variable entière
numberOfLinesLogged déclarée en dehors de la fermeture, afin que nous puissions augmenter sa valeur et l'imprimer chaque fois que la fermeture est appelée:
var numberOfLinesLogged = 0 let logger1 = { numberOfLinesLogged += 1 print("Lines logged: \(numberOfLinesLogged)") } logger1()
Cela affichera le message «Lignes enregistrées: 1».
Nous allons maintenant créer une copie de la fermeture qui partagera les valeurs capturées avec la première fermeture. Ainsi, si nous appelons la fermeture originale ou sa copie, nous verrons la valeur croissante de la variable.
let logger2 = logger1 logger2() logger1() logger2()
Cela affichera les messages "Lignes enregistrées: 1" ... "Lignes enregistrées: 4" car
logger1 et
logger2 pointent vers la même variable
numberOfLinesLogged capturée.
Quand utiliser une capture "forte", "faible" et "sans propriétaire"
Maintenant que nous comprenons comment tout fonctionne, essayons de résumer:
1. Si vous êtes sûr que la valeur capturée ne deviendra jamais
nulle lors de la fermeture, vous pouvez utiliser la
«capture sans propriétaire» . Il s'agit d'une situation peu fréquente où l'utilisation de la capture «faible» peut entraîner des difficultés supplémentaires, même lors de l'utilisation d'une garde laissée à une valeur faiblement capturée à l'intérieur de la fermeture.
2. Si vous avez un cas de cycle de liens forts (l'entité A possède l'entité B et l'entité B possède l'entité A), alors dans l'un des cas, vous devez utiliser la
«capture faible» . Il est nécessaire de prendre en compte laquelle des deux entités sera libérée en premier, donc si le contrôleur de vue A représente le contrôleur de vue B, alors le contrôleur de vue B peut contenir un lien «faible» vers «A».
3. Si la possibilité d'un cycle de liens forts est exclue, vous pouvez utiliser la capture "forte" (
"capture forte" ). Par exemple, l'exécution d'une animation ne se bloque pas à l'intérieur de la fermeture contenant l'animation, vous pouvez donc utiliser une liaison forte.
4. Si vous n'êtes pas sûr, commencez par une reliure «faible» et ne la changez que si nécessaire.
Facultatif - Guide Swift officiel:
Court-circuitsComptage automatique des liens