Generics en Swift : pourquoi les utiliser et comment les utiliser en pratique
Les generics (programmation générique) — l'une des fonctionnalités les plus puissantes et fondamentales du langage Swift. Ils permettent d'écrire un code flexible, type-safe et réutilisable, en évitant la duplication. Si vous avez déjà utilisé Array<Int>, Dictionary<String, Any> ou Optional<T>, vous avez déjà rencontré les generics. Dans cet article, nous allons voir pourquoi ils sont nécessaires, comment les utiliser correctement et examiner des exemples pratiques pour le développement iOS/macOS.
Pourquoi utiliser les Generics en Swift ?
Le principal problème résolu par les generics est la nécessité d'écrire le même code pour différents types de données. Sans eux, vous devriez créer des fonctions séparées pour travailler avec Int, String, Double et des structures personnalisées. Cela entraîne une duplication de code, des erreurs et une maintenance complexe.
Avantages clés des generics :
- Type-safety : le compilateur vérifie les types à la compilation, évitant les erreurs d'exécution.
- Réutilisabilité du code : une seule fonction ou un seul type fonctionne avec n'importe quel type de données.
- Performance : les generics en Swift sont implémentés via la spécialisation — le compilateur crée un code optimisé pour chaque type spécifique, comparable à l'écriture manuelle de code dupliqué.
- Lisibilité et abstraction : le code devient plus concis et expressif.
Prenons un exemple simple. Supposons que nous ayons besoin d'une fonction pour échanger les valeurs de deux variables. Sans generics, il faudrait écrire :
func swapInts(_ a: inout Int, _ b: inout Int) { let temp = a a = b b = temp}
func swapStrings(_ a: inout String, _ b: inout String) { let temp = a a = b b = temp}// ... et ainsi de suite pour chaque typeAvec les generics, la solution devient universelle :
func swapValues<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp}
var x = 10var y = 20swapValues(&x, &y) // Fonctionne avec Int
var str1 = "Hello"var str2 = "World"swapValues(&str1, &str2) // Fonctionne avec StringComme vous le voyez, <T> est un placeholder (espace réservé) pour le type. Le compilateur déduit automatiquement le type spécifique lors de l'appel de la fonction.
Fonctions et types génériques
Les generics peuvent être utilisés non seulement avec des fonctions, mais aussi avec des types personnalisés : structures, classes et énumérations.
Fonctions génériques
En plus du simple placeholder, on peut imposer des contraintes sur le type. Par exemple, une fonction qui fonctionne uniquement avec les types implémentant le protocole Equatable :
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? { for (index, item) in array.enumerated() { if item == value { return index } } return nil}
let numbers = [1, 5, 3, 12]if let index = findIndex(of: 5, in: numbers) { print("Index trouvé : \\(index)") // Affiche : Index trouvé : 1}La contrainte <T: Equatable> garantit que l'opérateur == est défini pour le type T.
Types génériques (structures et classes)
Créons une pile simple — une structure de données fonctionnant selon le principe LIFO (Last In, First Out) :
struct Stack<Element> { private var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element? { return items.popLast() } var top: Element? { return items.last }}
var intStack = Stack<Int>()intStack.push(10)intStack.push(20)print(intStack.pop() ?? 0) // 20
var str