Les fonctions et les closures en Swift sont deux concepts très proches. On les peut retrouver dans tous les langages de programmations.
La différence fondamentale entre une closure et une fonction est que la closure n’a pas de nom et peut être utilisée comme une expression. Une fonction, quant à elle, a un nom et peut être appelée à partir d’autres parties de votre code.
Pour schématiser, on peut dire que la closure est comme une version miniature de la fonction. C’est en quelque sorte, son mini-moi 👩👧
Les fonctions en Swift
En Swift, les fonctions sont des blocs de code qui effectuent une tâche spécifique. Elles peuvent prendre des paramètres en entrée et peuvent retourner des valeurs en sortie. Elles peuvent également avoir des fonctions imbriquées (nested functions). C’est une fonction dans une fonction (dans une fonction, etc). Les fonctions imbriquées peuvent accéder aux variables et aux paramètres de la fonction parente.
Voici un exemple de fonction :
func greet(person: String) -> String {
let greeting = "Bonjour, " + person + " !"
return greeting
}
print(greet(person: "Pomme"))
// Affiche : "Bonjour, Pomme !"
La fonction est créée avec le mot clé func
.
Elle doit s’appeler avec un terme précis qui décrit son utilisation.
Si elle prend des paramètres (ce qu’on lui donne entre parenthèse), le type du paramètre doit être spécifié.
Si elle retourne un paramètre, le type de retour doit être spécifié après la flèche ->
.
print()
qui permet d’afficher les variables dans la console de Xcode est une fonction native de Swift.
D’autres exemples de fonctions :
// Sans paramètres
func sayHelloWorld() -> String {
return "hello, world"
}
// Avec plusieurs paramètres
func trenteSixQuinzeMyLife(name: String, age: Int, hasCat: Bool) -> String {
var blaBla = "Je m'appelle \(name), j'ai \(age) "
if hasCat {
blaBla += "et j'ai un chat !"
} else {
blaBla += "!"
}
return blaBla
}
// Sans valeur de retour, nous n'avons pas besoin de la flèche de retour
func greet(person: String) {
print("Bonjour, \(person) !")
}
greet(person: "Pomme") // Affiche "Bonjour Pomme !"
Le sachiez-tu ? La fonction qui ne retourne rien ne retourne pas vraiment rien… 🥴 C’est à dire qu’elle a un type de retour qui s’appelle
void
. Elle renvoie donc « rien ».
Si tu ne veux pas de label spécial en argument, tu peux utiliser le _
:
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// écris ton code
}
someFunction(1, secondParameterName: 2)
Fonction avec un paramètre par défaut :
func exampleFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// Si tu ne passes pas d'argument pour le deuxième paramètre
// c'est la valeur par défaut qui s'applique
}
exampleFunction(parameterWithoutDefault: 4) // parameterWithDefault is 12
Fonction qui a plusieurs types de retours :
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
//J'appelle ma fonction :
if let bounds = minMax(array: [98, 28, 222, -12, 13]) {
print("Le min est \(bounds.min) et le max est \(bounds.max)")
}
// Affiche "Le min est -12 et le max est 222"
Cas particulier d’une fonction sans retour qui appelle une fonction avec retour :
func printAndCount(string: String) -> Int {
print(string)
return string.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// Affiche "hello, world" et retourne 12
printWithoutCounting(string: "hello, world")
// Affiche "hello, world" mais ne retourne rien
Paramètres In-Out
En Swift, il n’est pas possible de changer les paramètres d’entrée d’une fonction sous peine d’une erreur de compilation. En utilisant inout
, il sera possible de faire ce changement.
Attention, ce n’est pas applicable aux paramètres par défaut.
On utilise & devant le nom de la variable lorsqu’elle est passée en argument de la fonction.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
Cas particulier d’une fonction imbriquée qui utilise le type de retour d’une autre fonction
Ça paraît plus compliqué que ça ne l’est vraiment 🧘♀️
Pour ce faire, tu vas écrire un type de fonction complet immédiatement après la flèche de retour ->
de la fonction de retour :
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 } // Fonction n°1
func stepBackward(input: Int) -> Int { return input - 1 } // Fonction n°2
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
Cet exemple pourrait être écrit avec des fonctions globales (toutes écrites dans le scope global).
Les bonnes pratiques pour l’utilisation et la création de fonctions :
- Définir un nom explicite et qui décrit son action : évite de mettre trop de commentaires dans ton code
- Taille des fonctions : si elle est trop longue, la diviser avec une (ou plusieurs) autres fonctions
- Principe de responsabilité unique (SRP) : chaque fonction ne doit avoir qu’une seule responsabilité ou tâche à accomplir.
- Utilisation de paramètres : les fonctions doivent prendre en entrée les paramètres nécessaires à leur exécution, mais pas plus.
- Retour de valeurs : les fonctions doivent retourner une valeur si cela est nécessaire, en évitant les valeurs de retour superflues.
- Test des fonctions : les fonctions doivent être testées pour s’assurer qu’elles fonctionnent comme prévu dans toutes les conditions possibles.
Les closures en Swift
Les closures sont un peu plus complexes à comprendre que les fonctions, mais je suis sûre que ce n’est pas insurmontable (ok, j’essaie de me rassurer aussi en disant ça 🤪).
Mais pour faire simple, une closure est une fonction sans nom qui peut être utilisée comme une valeur. Elle peut être passée en paramètre à une autre fonction ou stockée dans une variable. Elles sont similaires aux expressions lambdas dans d’autres langages de programmation (comme Java).
Les closures sont souvent utilisées pour des opérations asynchrones, telles que la manipulation de tableaux ou la gestion des erreurs. C’est à dire qu’elle fera son action en parallèle de l’action principale et donc ne bloquera pas celle-ci.
Les closures peuvent également être utilisées pour trier des tableaux, filtrer des éléments et mapper des données, en utilisant les fonctions natives de Swift : map()
, filter()
, sorted()
.
La syntaxe est la suivante :
{ (paramètres) -> typeDeRetour in
// corps de la closure
}
Par exemple :
let numbers = [5, 2, 8, 1, 3]
let sortedNumbers = numbers.sorted { (a, b) -> Bool in
return a > b
} // Version "classique"
sortedNumbers = numbers.sorted { a, b in a > b } // Version simplifiée
sortedNumbers = numbers.sorted { $0 > $1 } // Encore plus simple
print(sortedNumbers) // Affiche "[8, 5, 3, 2, 1]"
On peut utliser $0
et $1
pour faire référence au premier argument ($0
) et au second argument ($1
).
Capturing values
Lorsqu’une closure en Swift capture des valeurs de son environnement, on parle de « capturing values ». Cela signifie que la closure a accès aux variables et constantes, même si la valeur a changé entre temps. C’est à dire que lorsqu’une closure capture une valeur, elle crée une copie de cette valeur et la stocke dans sa propre mémoire. Cette copie est alors utilisée par la closure chaque fois qu’elle est exécutée et référencée par des variables ou constantes. Ce sont donc des types de références.
var multiplyByTwo: (Int) -> Int = { x in
return x * 2
}
let alsoMultiplyByTwo = multiplyByTwo
multiplyByTwo(5) // Retourne 10
alsoMultiplyByTwo(5) // Retourne également 10
multiplyByTwo = { x in
return x * x
}
multiplyByTwo(5) // Retourne 25
alsoMultiplyByTwo(5) // Retourne toujours 10
Escaping closure
On dit qu’une closure échappe (escape) à une fonction lorsqu’elle est transmise en tant qu’argument à une fonction mais qu’elle est appelée après le retour de la fonction :
func delay(seconds: Double, completion: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
Dans cet exemple, la fonction delay
exécute la closure après le nombre de secondes spécifié en utilisant la file d’exécution principale. Cette fonction est utile quand tu as besoin de délai dans l’exécution de certaines tâches, par exemple lorsque tu veux afficher une notification après un certain temps.
On parlera de async
& await
dans un autre article.
Autoclosures
C’est une closure qui est automatiquement créée lorsqu’elle est passée en tant que paramètre d’une fonction. Elle ne prend aucun argument et, lorsqu’elle est appelée, elle renvoie la valeur de l’expression qu’elle contient.
func evaluate(condition: @autoclosure () -> Bool) {
if condition() {
print("La condition est vraie")
} else {
print("La condition est fausse")
}
}
evaluate(condition: 2 + 2 == 4) // Affiche "La condition est vraie"
Une autoclosure est utile çar elle permet de retarder le moment où elle est appelée et donc d’économiser en temps de calcul. Ce que tes utilisateurs apprécieront grandement !
Pour t’entraîner à utiliser les closures, tu peux utiliser les fonctions map()
, filter()
, sorted()
ou encore essayer de simplifier ton code en les utilisant, … Le mot d’ordre est de pratiquer et pratiquer encore pour bien assimiler le concept.
Les bonnes pratiques pour l’utilisation et la création de closures :
- Éviter les fuites de mémoire. Les closures captureront les références aux objets qu’elles utilisent, il est donc important d’éviter les fuites de mémoire en utilisant la capture de valeurs plutôt que de références.
- Rendre les closures courtes et concises. Les closures sont censées être courtes et concises
- Utiliser les closures pour la gestion des erreurs : Les closures sont souvent utilisées pour gérer les erreurs dans les fonctions asynchrones.
- Éviter les références circulaires. Les closures peuvent facilement entraîner des références circulaires, il est donc important d’être conscient de ce risque et de prendre des mesures pour l’éviter.
- Utiliser des types de closures appropriés. Soit celles qui renvoient des valeurs, soit celles qui ne renvoient pas de valeurs.
- Éviter les blocs de code imbriqués. Les blocs de code imbriqués peuvent rendre le code difficile à comprendre et à maintenir.
- Nommer les paramètres de la closure. Les noms de paramètre doivent être clairs et précis pour indiquer leur fonction dans la closure.
Voici un peu de documentation et des petits exercices pour t’aider à comprendre les closures (le jour 9 des 100DaysOfSwiftUI de Paul Hudson).
Merci d’avoir lu jusqu’au bout cet article bien dense mais que j’ai essayé de remplir avec le plus d’exemple différents possibles. Souviens-toi que c’est en pratiquant que tu maîtriseras ton sujet. Donc n’hésites pas à t’entraîner, à faire, à échouer et à recommencer ! 💪