Les génériques

Comprendre l'utilisation des génériques en Swift.

Les génériques en Swift permettent de créer des fonctions, des types, et des structures qui peuvent travailler avec des valeurs de n'importe quel type, tout en maintenant la sécurité de type. Ils sont particulièrement utiles pour écrire du code flexible et réutilisable sans compromettre la sécurité des types.

1. Concept des Génériques

Les génériques permettent de définir des fonctions, des classes, des structures ou des énumérations de manière indépendante du type des données qu'ils manipulent. Cela signifie que vous pouvez écrire du code une fois et l'utiliser avec différents types de données.

2. Fonctions Génériques

Les fonctions génériques vous permettent de créer des fonctions qui acceptent des paramètres de types variables. Vous définissez un ou plusieurs paramètres de type entre des chevrons (< et >).

func swap<T>(a: inout T, b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 10
var y = 20
swap(&x, &y)
print("x = \(x), y = \(y)")  // Affiche : "x = 20, y = 10"

Ici, T est un paramètre de type générique qui représente le type des paramètres a et b.

3. Types Génériques

Les génériques peuvent également être utilisés pour créer des types comme des classes, des structures, ou des énumérations qui fonctionnent avec différents types.

struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop() ?? "Pile vide")  // Affiche : 2

var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print(stringStack.pop() ?? "Pile vide")  // Affiche : "World"

4. Contraintes de Type

Vous pouvez ajouter des contraintes à un paramètre de type pour restreindre les types qui peuvent être utilisés avec vos génériques. Les contraintes permettent d’assurer que le type générique satisfait à certaines exigences.

protocol Summable {
    func sum() -> Int
}

struct Numbers: Summable {
    var numbers: [Int]
    func sum() -> Int {
        return numbers.reduce(0, +)
    }
}

func printSum<T: Summable>(of summable: T) {
    print("La somme est \(summable.sum()).")
}

let numbers = Numbers(numbers: [1, 2, 3, 4, 5])
printSum(of: numbers)  // Affiche : "La somme est 15."

Ici, T: Summable signifie que T doit adopter le protocole Summable.

5. Types Contraints

Vous pouvez ajouter des contraintes à un type générique pour que ce type soit une sous-classe d'une autre classe ou adopte un protocole spécifique.

class Animal {
    func makeSound() {}
}

class Dog: Animal {
    override func makeSound() {
        print("Woof!")
    }
}

func printAnimalSound<T: Animal>(animal: T) {
    animal.makeSound()
}

let dog = Dog()
printAnimalSound(animal: dog)  // Affiche : "Woof!"

6. Génériques et Extensions

Vous pouvez étendre les types génériques avec des extensions pour ajouter des fonctionnalités supplémentaires.

extension Stack {
    func isEmpty() -> Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
print(intStack.isEmpty())  // Affiche : true
intStack.push(1)
print(intStack.isEmpty())  // Affiche : false

7. Protocoles avec Types Associés

Les protocoles peuvent contenir des types associés qui peuvent être utilisés avec des génériques pour créer des abstractions plus flexibles.

protocol Container {
    associatedtype Item
    mutating func add(_ item: Item)
    func getAll() -> [Item]
}

struct MyContainer<T>: Container {
    private var items: [T] = []
    mutating func add(_ item: T) {
        items.append(item)
    }
    func getAll() -> [T] {
        return items
    }
}

var stringContainer = MyContainer<String>()
stringContainer.add("Hello")
stringContainer.add("World")
print(stringContainer.getAll())  // Affiche : ["Hello", "World"]

8. Type Placeholder

Le type générique est souvent utilisé comme un espace réservé pour le type spécifique qui sera utilisé lors de l'instanciation. Cela permet au code de rester flexible et de réutiliser les mêmes structures et fonctions avec différents types de données.

func makeArray<T>(elements: T...) -> [T] {
    return elements
}

let intArray = makeArray(elements: 1, 2, 3, 4)
let stringArray = makeArray(elements: "a", "b", "c")
print(intArray)  // Affiche : [1, 2, 3, 4]
print(stringArray)  // Affiche : ["a", "b", "c"]

9. Génériques avec Protocoles

Les génériques peuvent également être utilisés avec des protocoles pour créer des abstractions puissantes qui permettent de travailler avec différents types tout en respectant les exigences des protocoles.

protocol Identifiable {
    var id: String { get }
}

struct User: Identifiable {
    var id: String
}

func printID<T: Identifiable>(item: T) {
    print("ID: \(item.id)")
}

let user = User(id: "12345")
printID(item: user)  // Affiche : "ID: 12345"