Les ensembles, également connus sous le nom de 'sets', sont des structures de données qui permettent de stocker des éléments uniques dans un ordre non défini. Ils sont similaires aux tableaux, mais ils ne peuvent pas contenir de valeurs dupliquées. Les ensembles sont très utiles lorsque vous voulez vérifier rapidement si un élément existe déjà dans une collection, ou pour supprimer les doublons d'un tableau.

Un ensemble est une collection qui stocke des valeurs. Les tableaux et les dictionnaires sont également des collections. Peut-être moins utilisés, les ensembles ont néanmoins quelques atouts.

  • Unicité : Dans un ensemble, chaque valeur doit être unique. Si vous essayez de mettre deux fois la même valeur, l'ensemble la gardera une seule fois.
  • Pas d'ordre particulier : Contrairement à une liste où les valeurs sont ordonnées, un ensemble ne se soucie pas de l'ordre dans lequel vous ajoutez ou retirez les valeurs.
  • Recherche rapide : Les ensembles sont excellents pour rechercher rapidement si une certaine valeur est à l'intérieur ou non.

Un exemple simple pourrait être votre bibliothèque. Si vous avez déjà le livre nommé Les confortables d'Émile Hugo, c'est inutile d'en ajouter un deuxième. Chaque livre doit être unique. Il est aussi très rapide de vérifier que vous possédez ou non le livre Aux malheurs des hommes de Victor Zola, sans avoir à regarder chacun des livres un par un.

Ensemble

Pour déclarer un ensemble, on utilise la même syntaxe que pour la déclaration d'un tableau. Mais comment Swift fait-il pour savoir lequel nous allons déclarer ? Bonne question. L'inférence de type se fait en fonction du contexte et des éléments fournis lors de la déclaration d'une variable. Cependant, on indique toujours explicitement qu'il s'agit d'un ensemble.


// Déclaration d'un tableau d'entiers
// par interférence de type
let tableauEntiers = [1, 2, 3, 4, 5]

// Déclaration d'un ensemble d'entiers
// par annotation de type
var ensembleEntiers: Set<Int> = [1, 2, 3, 4, 5]

// Déclaration d'un ensemble d'entiers
// par interférence de type
let autreEnsembleEntiers = Set([1, 2, 3, 4, 5]) 

Prenez le temps de bien comprendre ces trois déclarations différentes.

Pour la déclaration d'un ensemble par annotation de type, on indique le type des valeurs contenues par cet ensemble. Un ensemble ne peut contenir que des valeurs du même type. Donc, si un ensemble contient des nombres entiers, il sera de type Int.

Pour déclarer un ensemble vide, on indique simplement à Swift le type d'éléments qu'il pourra potentiellement accepter. Il est obligatoire de toujours déclarer le type d'un ensemble vide :


// Déclaration par annotation de type
var ensembleVide: Set<Int> = []

// Déclaration par interférence de type
var autreEnsembleVide = Set<Int>()

Dans les deux déclarations ci-dessus, les ensembles ne pourront accepter que des valeurs de type Int. Ce qui sécurise votre code et évite certaines réactions inattendues.


var music: Set<String> = ["Rock", "Classique", "Rap"]
// music a été initialisée avec 3 éléments 

La déclaration d'un ensemble ne pouvant pas toujours être différenciée de la déclaration d'un tableau seul, le type Set doit donc être explicitement déclaré. Vous n'avez pas besoin d'écrire le type des éléments contenus par l'ensemble si vous l'initialisez avec au moins une valeur d'un seul type.


var music: Set = ["Rock", "Classique", "Rap"]
// Plus rapide, le type est déduit automatiquement

Itération d'un ensemble

Comme pour un tableau ou pour un dictionnaire, on utilise la boucle for pour parcourir les éléments d'un ensemble.


// Déclaration d'un ensemble 
let fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

// Itération de l'ensemble avec une boucle for-in
for element in fruit {
    print(element)
}

Peu d'explications seront nécessaire ici, vous devez commencer à avoir l'habitude de ce principe. Par contre, si vous avez besoin d'obtenir l'indice de chaque élément en plus de sa valeur, vous pouvez utiliser la méthode enumerated(). Elle vous renverra une séquence de paires (indice, élément) :


let fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

// Itération de l'ensemble avec les indices
for (indice, element) in fruit.enumerated() {
    print("Index \(indice): \(element)")
}

Dans ce cas, l'indice de l'élément et sa valeur correspondante sont stockés temporairement dans un tuple pour que vous puissiez les utiliser entre les accolades. L'ordre des éléments dans un ensemble (Set) n'est pas garanti. Si vous avez besoin de maintenir un ordre spécifique, il est préférable d'utiliser un tableau.

Manipulation d'un ensemble

Compter le nombre d'éléments d'un ensemble

Pour compter le nombre d'éléments qui compose un ensemble, on utilise la méthode count.


let fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

print(fruit.count)
// Cela affichera 4

Vérifier si un ensemble est vide

Pour vérifier si un ensemble est vide, on utilise la méthode isEmpty. Vous devez vous rendre compte que les trois collections fonctionnent de manière assez similaire. Ne le voyez pas comme une répétition mais comme un avantage pour mieux mémoriser et comprendre :


let fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

if fruit.isEmpty {
    print("L'ensemble est vide")
} else {
    print("L'ensemble contient au moins un élément")
}
// Cela va afficher : "L'ensemble contient au moins un élément"

Ajouter un élément à un ensemble

Pour ajouter un élément à un ensemble, on utilise la méthode insert(). Il suffit juste d'indiquer la valeur à ajouter entre parenthèses. N'oubliez pas qu'un ensemble ne peut contenir que des valeurs du même type, donc cette valeur à ajouter doit correspondre au type accepté par l'ensemble.


var fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

fruit.insert("kiwi")
// fruit = ["fraise", "kiwi", "pomme", "banane", "orange"]
// L'ordre des éléments n'est pas garantit

Supprimer un élément d'un ensemble

Pour supprimer un élément d'un ensemble, on utilise la méthode remove(). Il suffit d'indiquer entre parenthèses la valeur à supprimer :


var fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

fruit.remove("orange")
// fruit = ["fraise", "kiwi", "pomme", "banane"]
Si vous indiquez entre parenthèses une valeur qui n'existe pas ou qui n'existe plus, il ne se produira rien. Une valeur absente ne peut être supprimée, elle n'existe pas. Swift considère alors l'action comme effectuée et le programme continuera de s'exécuter.

Vérifier la présence d'un élément dans un ensemble

Pour vérifier si une valeur existe dans un ensemble, on utilise la méthode contains(). Il suffit juste de préciser la valeur à vérifier entre parenthèses :


var fruit: Set<String> = ["pomme", "orange", "banane", "fraise"]

if fruit.contains("poire") {
    print("Nous en avons !")
} else {
    print("Nous n'avons pas cela en stock")
}
// Cela va afficher : "Nous n'avons pas cela en stock"

Opération sur les ensembles

Vous pouvez effectuer des opérations sur des ensembles, telles que les combiner, déterminer quelles valeurs ils ont en commun ou déterminer si deux ensembles contiennent toutes, certaines ou aucune des mêmes valeurs.

Un schéma est parfois plus parlant :

les ensembles swift
Opération sur les ensembles - Source Apple
  • Utilisez la méthode intersection(_:) pour créer un nouvel ensemble avec uniquement les valeurs communes aux deux ensembles.
  • Utilisez la méthode symmetricDifference(_:) pour créer un nouvel ensemble avec des valeurs dans l'un ou l'autre ensemble, mais pas dans les deux.
  • Utilisez la méthode union(_:) pour créer un nouvel ensemble avec toutes les valeurs des deux ensembles.
  • Utilisez la méthode subtracting(_:) pour créer un nouvel ensemble avec des valeurs ne faisant pas partie de l'ensemble spécifié.

À l'aide d'exemples concrets, je vais vous détailler ces 4 méthodes. Prenons comme point de départ, les trois constantes suivantes : impair, pair et hasard. Elles contiennent toutes les trois un ensemble de type Int.


let impair: Set = [1, 3, 5, 7, 9]
let pair: Set = [0, 2, 4, 6, 8]
let hasard: Set = [2, 3, 5, 7]

Pour créer un nouvel ensemble avec toutes les valeurs des deux ensembles, on utilise la méthode union(_:). On applique cette méthode à un ensemble en indiquant comme paramètre, l'ensemble à ajouter :


// Nouvel ensemble avec toutes les valeurs des deux ensembles
let nouvelleEnsemble = impair.union(pair).sorted()

print(nouvelleEnsemble)
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Dans l'exemple ci-dessus, nous avons également utilisé la méthode sorted() pour trier les éléments. Ce sera plus facile pour lire le résultat et comprendre l'effet de la méthode union(_:).

Pour créer un nouvel ensemble avec uniquement les valeurs communes à deux ensembles, on utilise la méthode intersection(_:) :


// Nouvel ensemble avec uniquement les valeurs communes aux deux ensembles
let nouvelleEnsemble = impair.intersection(hasard).sorted()

print(nouvelleEnsemble)
// [3, 5, 7]

Pour créer un nouvel ensemble avec des valeurs ne faisant pas partie de l'ensemble spécifié, on utilise la méthode subtracting(_:) :


// Nouvel ensemble avec des valeurs ne faisant pas partie de l'ensemble spécifié
let nouvelleEnsemble = impair.subtracting(hasard).sorted()

print(nouvelleEnsemble)
// [1, 9]

Dans l'exemple ci-dessus, les éléments 1 et 9 sont présents dans l'ensemble nommé impair mais absents de l'ensemble nommé hasard.

Pour créer un nouvel ensemble avec des valeurs dans l'un ou l'autre ensemble, mais pas dans les deux, on utilise la méthode symmetricDifference(_:) :


// Nouvel ensemble avec des valeurs dans l'un ou l'autre ensemble, mais pas dans les deux
let nouvelleEnsemble = impair.symmetricDifference(hasard).sorted()

print(nouvelleEnsemble)
// [1, 2, 9]
La puissance des ensembles réside essentiellement dans la présence de valeurs uniques sur lesquelles de nombreuses opérations de tri sont possibles.

Comparaison d'ensembles

  • Utilisez l'opérateur "est égal" == pour déterminer si deux ensembles contiennent toutes les mêmes valeurs.
  • Utilisez la méthode isSubset(of:) pour déterminer si toutes les valeurs d'un ensemble sont contenues dans l'ensemble spécifié.
  • Utilisez la méthode isSuperset(of:) pour déterminer si un ensemble contient toutes les valeurs d'un ensemble spécifié.
  • Utilisez les méthodes isStrictSubset(of:) ou isStrictSuperset(of:) pour déterminer si un ensemble est un sous-ensemble ou un sur-ensemble, mais pas égal à, un ensemble spécifié.
  • Utilisez la méthode isDisjoint(with:) pour déterminer si deux ensembles n'ont pas de valeurs en commun.

Voici quelques exemples concrets pour comparer des ensembles :


let domestique: Set = ["chien", "chat"]
let fermier: Set = ["chien", "chat", "poule", "coq", "mouton"]

// Comparaison
let comparaison = domestique == fermier
// egale a pour valeur : false

Pour ce premier exemple, rien de bien compliqué. L'ensemble domestique et l'ensemble fermier ne possèdent pas les mêmes valeurs. La constante comparaison a donc pour valeur false. Rappelez-vous, une comparaison renvoie une valeur de type booléenne.


let domestique: Set = ["chien", "chat"]
let fermier: Set = ["chien", "chat", "poule", "coq", "mouton"]

// Les valeurs d'un ensemble sont contenues dans l'ensemble spécifié ?
let resultat = domestique.isSubset(of: fermier)
// resultat a pour valeur : true

Dans ce deuxième exemple, la méthode isSubset(of:) sert à déterminer si toutes les valeurs d'un ensemble sont contenues dans l'ensemble spécifié. Ici, les valeurs de l'ensemble domestique sont bien présentes dans l'ensemble fermier. La constante resultat aura pour valeur true.


let domestique: Set = ["chien", "chat"]
let fermier: Set = ["chien", "chat", "poule", "coq", "mouton"]

// Un ensemble contient toutes les valeurs d'un ensemble spécifié ?
let resultat = fermier.isSuperset(of: domestique)
// resultat a pour valeur : true

Dans l'exemple ci-dessus, on vérifie grâce à la méthode isSuperset(of:) si un ensemble contient toutes les valeurs d'un ensemble spécifié. La définition pourrait vous paraitre identique à la précédente, mais veillez à bien faire la différence logique entre "sont contenues" et "contient".

L'ensemble fermier contient les valeurs de l'ensemble domestique. La constante resultat aura donc pour valeur booléenne : true.


let fermier: Set = ["chien", "chat", "poule", "coq", "mouton"]
let autre: Set = ["pinguoin", "lion", "crocodile"]

// Deux ensembles n'ont pas de valeurs en commun ?
let resultat = fermier.isDisjoint(with: autre)
// resultat a pour valeur : true

Et enfin, pour ce dernier exemple, on utilise la méthode isDisjoint(with:) pour déterminer si deux ensembles n'ont pas de valeurs en commun. L'ensemble fermier n'ayant aucune valeur en commun avec l'ensemble autre, la valeur booléenne de la constante resultat est : true.

Nous en avons maintenant fini avec les collections. Vous savez désormais quand, comment et pourquoi utiliser des tableaux, des dictionnaires ou des ensemble. Un page se tourne, place aux fonctions.

  • Un ensemble est une collection qui stocke des valeurs distinctes, du même type et sans ordre défini.
  • Ils ne peuvent pas contenir de valeurs dupliquées.
  • Lors de la déclaration, on indique toujours explicitement qu'il s'agit d'un ensemble
  • Les ensembles sont très pratiques car on peut les comparer, les combiner et vérifier leur valeur rapidement.

Je vous conseille de faire chaque exercice de fin de chapitre. Cela consolidera votre apprentissage de manière efficace en passant de la théorie à la pratique. Faites vos propres experiences, n'ayez peur de rien, vous progresserez plus vite ainsi.

Pour cet exercice, vous allez vous transformer en détective. Vous venez de recevoir la liste des prisonniers de deux célèbres prisons : alcatraz et bastille. Malheureusement, certains se sont évadés. Il va vous falloir retrouver de quelle prison se sont échappés ces prisonniers. Leurs noms sont réunis dans l'ensemble fugitif. A vous de jouer !


// La liste des prisonniers
let alcatraz: Set = ["Al Capone", "George Kelly", "Robert Stroud", "Mickey Cohen"]
let bastille: Set = ["Marquis de Sade", "Jean-Pierre Brissot", "Denis Diderot"]

// La liste des évadés
let fugitif: Set = ["Jean-Pierre Brissot", "Robert Stroud"]

// La liste des prisonniers
let alcatraz: Set = ["Al Capone", "George Kelly", "Robert Stroud", "Mickey Cohen"]
let bastille: Set = ["Marquis de Sade", "Jean-Pierre Brissot", "Denis Diderot"]

// La liste des évadés
let fugitif: Set = ["Jean-Pierre Brissot", "Robert Stroud"]

// On commence par passer en revue chaque prisonnier évadé
for prisonnier in fugitif {
    
    // Son nom appartient il au fichier de la prison d'Alcatraz ?
    if alcatraz.contains(prisonnier) {
        print("\(prisonnier) s'est évadé de la prison d'Alcatraz !")
    } 
    
    // Son nom appartient il au fichier de la prison d'Alcatraz ?
    else if bastille.contains(prisonnier) {
        
        
        print("\(prisonnier) s'est évadé de la prison de La Bastille !")
    }
    
    // Alors, on ne sais pas d'ou il vient
    else {
        print("\(prisonnier) s'est évadé d'une prison inconnue !")
    }
    
}

// Cela affichera :
// Jean-Pierre Brissot s'est évadé de la prison de La Bastille !
// Robert Stroud s'est évadé de la prison d'Alcatraz !