1. Quando serve Set: controlli rapidi «c'e` l'elemento»
Quando inizi a programmare, sembra che l'array sia la risposta universale a tutte le domande. Servono dati? Un array. Devi controllare se c'e` un elemento? Beh... scorriamo l'array e confrontiamo.
Con dati piccoli tutto funziona, e il cervello si rilassa. Il problema inizia quando «scorriamo» diventa centinaia di migliaia di confronti, poi milioni, e il tuo codice comincia a suonare come un portatile che sta decollando.
Il controllo di membership e` qualsiasi domanda del tipo: «c'e` l'elemento X nell'insieme?». E queste domande compaiono continuamente: non aggiungere di nuovo un tag, non registrare due volte lo stesso e-mail, non elaborare due volte lo stesso ID, trovare interessi in comune, verificare se un valore e` consentito.
Per questo tipo di compiti Set in Swift e` uno strumento di base: conserva il fatto della presenza di un elemento e rende questi controlli veloci.
2. Set in Swift: unicita`, Hashable e operazioni di base
Se la diciamo in modo molto semplice, Set e` una collezione che conserva elementi unici. Cioe` «dentro non ci sono duplicati»: due valori identici in Set non possono convivere, anche se glielo chiedi con grande insistenza. Proprio per questo Set e` perfetto per la domanda «abbiamo gia` visto questo valore?».
Perche` a volte si parla di «HashSet»? Perche` sotto il cofano una struttura del genere di solito usa l'hashing per trovare rapidamente gli elementi. In Swift non andiamo a guardare dentro l'implementazione (e facciamo bene — non stiamo scrivendo un compilatore nel fine settimana), ma il risultato pratico e` importante: operazioni come contains e insert in genere scalano bene su grandi dati.
Un altro punto importante: gli elementi di Set devono essere Hashable. Per noi, a questo livello, e` una regola semplice: Int, String, Character e molti altri tipi base vanno bene, mentre i «tipi complessi definiti da noi» li vedremo piu` avanti. Per ora basta sapere questo: se un tipo si puo` mettere in Set, significa che Swift sa confrontarlo e hasharlo in modo abbastanza coerente da usarlo come una «chiave».
Creare un insieme e contains
import Foundation
let bannedWords: Set<String> = ["spam", "scam", "ads"]
print(bannedWords.contains("spam")) // true
print(bannedWords.contains("swift")) // false
Qui Set si legge letteralmente come «insieme di parole vietate». Il controllo contains suona come una normale domanda umana.
Insert e «e` stato inserito o c'era gia`»
import Foundation
var ids: Set<Int> = [10, 20, 30]
let r1 = ids.insert(20)
print(r1.inserted) // false
let r2 = ids.insert(40)
print(r2.inserted) // true
insert restituisce un risultato in cui si puo` vedere se abbiamo aggiunto un nuovo elemento o semplicemente «provato ad aggiungere cio` che c'era gia`». Spesso questo aiuta a scrivere codice senza troppi if.
3. Pattern pratici: seen set e deduplicazione
C'e` un piacere particolare nel sostituire cicli annidati con un solo passaggio. E` come togliere i mobili inutili da una stanza: lo stesso significato, meno inciampi. Il pattern seen set e` proprio questo: scorri i dati da sinistra a destra e tieni in Set tutto cio` che hai gia` visto. Se incontri un elemento che e` gia` nel Set, allora e` un duplicato.
Questo pattern e` utile non solo per «trovare un duplicato». Funziona benissimo per «verificare che tutti i valori siano unici», «fermarsi al primo duplicato», «contare il numero di unici», «trovare il primo valore che si ripete». La cosa importante e` che smetti di fare il confronto «tutti contro tutti» e inizi a fare il confronto «ognuno — con Set».
Piccolo schema del processo (e` uno schema, non Swift):
flowchart TD
A[Prendiamo il prossimo elemento x] --> B{seen contains x?}
B -- yes --> C[E` un duplicato: reagiamo]
B -- no --> D["seen.insert(x)"]
D --> A
Seen set: primo duplicato in un array
import Foundation
let nums = [5, 1, 4, 1, 9]
var seen: Set<Int> = []
var firstDuplicate: Int? = nil
for x in nums {
if seen.contains(x) {
firstDuplicate = x
break
}
seen.insert(x)
}
print(firstDuplicate as Any) // Optional(1)
Fai attenzione all'ordine: prima controlliamo contains, poi insert. Cosi` non «permettiamo» a un elemento di coincidere con se stesso all'interno della stessa iterazione.
Deduplicazione tramite Set: veloce, ma l'ordine non e` garantito
Quando senti la parola «deduplicazione», la mano va verso la soluzione piu` corta:
let unique = Set(array)
Ed e` davvero un trucco utile: ottenere un insieme di elementi unici da un array si puo` fare letteralmente con una sola riga. Ma c'e` un dettaglio importante: Set non e` obbligato a mantenere l'ordine degli elementi. Quindi, se per te e` importante «in che ordine sono comparsi», un solo Set(array) non basta.
E non e` una «particolarita` di Swift oggi», ma l'idea stessa di insieme: si occupa dell'unicita`, non dell'ordine. Per questo, nei problemi del tipo «elimina i duplicati ma mantieni l'ordine della lista originale», spesso si usa una combinazione: Set per controllare «visto/non visto» e Array per il risultato.
Tra l'altro, nei materiali sui miglioramenti della libreria standard si sottolinea separatamente che dopo alcune operazioni (per esempio, lazy.filter) puoi ottenere non un Set, ma un «wrapper-collezione», e allora a volte conviene raccogliere di nuovo esplicitamente Set(...), per tornare proprio all'insieme e alle sue operazioni.
import Foundation
let tags = ["swift", "cli", "swift", "set"]
let uniqueTags = Set(tags)
print(uniqueTags) // l'ordine puo` essere qualsiasi
Il risultato puo` arrivare in un ordine diverso, ed e` normale.
Deduplicazione mantenendo l'ordine
import Foundation
let tags = ["swift", "cli", "swift", "set"]
var seen: Set<String> = []
var orderedUnique: [String] = []
for t in tags {
if !seen.contains(t) {
seen.insert(t)
orderedUnique.append(t)
}
}
print(orderedUnique) // ["swift", "cli", "set"]
Questo e` uno dei metodi piu` pratici: Set e` responsabile dell'«unicita`», l'array — dell'«ordine».
4. Matematica degli insiemi nel codice
Set ha anche un altro superpotere: sa esprimere la «matematica degli insiemi» direttamente nel codice. E questo, all'improvviso, rende le soluzioni piu` leggibili: invece di «scorri un array, poi l'altro, poi confronta...», scrivi «intersezione» o «unione», e il significato si vede subito.
Le operazioni piu` utili:
- intersection — elementi in comune;
- union — tutto insieme senza duplicati;
- subtracting — elementi del primo insieme senza quelli del secondo;
- symmetricDifference — elementi presenti in uno solo dei due insiemi (come un «XOR», ma per collezioni).
Si`, suona un po' «matematico», ma in pratica sono domande del tipo «quali tag sono in comune tra due libri» oppure «quali utenti ci sono nella lista A ma non nella lista B».
Intersezione degli interessi
import Foundation
let alice: Set<String> = ["swift", "music", "travel"]
let bob: Set<String> = ["java", "music", "swift"]
let common = alice.intersection(bob)
print(common) // {"music", "swift"} (l'ordine non conta)
Differenze e symmetricDifference
import Foundation
let alice: Set<String> = ["swift", "music", "travel"]
let bob: Set<String> = ["java", "music", "swift"]
let onlyAlice = alice.subtracting(bob)
let onlyBob = bob.subtracting(alice)
let exactlyOne = alice.symmetricDifference(bob)
print(onlyAlice) // {"travel"}
print(onlyBob) // {"java"}
print(exactlyOne) // {"travel", "java"}
Con operazioni del genere Set smette di essere «solo un contenitore» e diventa un modo per formulare un pensiero in modo compatto.
5. Mini esempio: una piccola libreria e i sottoinsiemi
Non siamo ancora arrivati ai tipi personalizzati tramite struct, quindi usiamo con sincerita` cio` che possiamo gia` usare: tuple, array e insiemi. Immaginiamo un mini catalogo di libri come un array di tuple (id, title, tags). Poi facciamo due funzioni utili: deduplicare i tag su tutta la libreria e controllare se un libro contiene tutti i tag richiesti.
Dati «cosi` come sono»
import Foundation
let books: [(id: Int, title: String, tags: [String])] = [
(1, "Swift Basics", ["swift", "beginner", "cli"]),
(2, "Algorithms", ["arrays", "set", "dictionary"]),
(3, "Swift CLI Tools", ["swift", "cli", "set"])
]
Raccogliere tutti i tag unici della libreria
import Foundation
func allUniqueTags(in books: [(id: Int, title: String, tags: [String])]) -> Set<String> {
var result: Set<String> = []
for b in books {
for t in b.tags {
result.insert(t)
}
}
return result
}
print(allUniqueTags(in: books))
Qui usiamo Set come «contenitore dell'unicita`»: non importa quante volte un tag appare, importa che ci sia.
Verificare che un libro contenga tutti i tag richiesti
import Foundation
func hasAllTags(bookTags: [String], required: Set<String>) -> Bool {
let tagSet = Set(bookTags)
return required.isSubset(of: tagSet)
}
print(hasAllTags(bookTags: ["swift", "cli", "set"], required: ["swift", "set"])) // true
print(hasAllTags(bookTags: ["swift", "cli"], required: ["swift", "set"])) // false
Qui c'e` nascosto un importante «bonus di leggibilita`»: trasformiamo l'array di tag in Set, perche` un insieme ha un'operazione naturale isSubset(of:). E` proprio il caso in cui scrivi il codice «come un pensiero»: «required e` un sottoinsieme dei tag del libro».
6. Prestazioni e limiti reali
Viene voglia di dire: «Set e` magia, e tutto funziona all'istante». Ma e` meglio tenere un quadro realistico. Di solito Set.contains e Set.insert funzionano velocemente, perche` all'interno si usa l'hashing: dal valore si calcola una «impronta» e, grazie a questa, si trova rapidamente il posto nella struttura. Anche se ci sono collisioni (quando valori diversi producono impronte simili), la struttura sa comunque distinguere correttamente gli elementi tramite il confronto.
Pero` «veloce» non significa «sempre istantaneo allo stesso modo con qualunque dato». E` un modello «in media», su input normali. In pratica, per noi basta questa regola: se fai molte domande «c'e` l'elemento» su dati grandi, l'array puo` cominciare a rallentare, mentre Set di solito se la cava molto meglio.
Per fissare la scelta della struttura, e` utile tenere una piccola tabella:
| Domanda del problema | Cosa scegliere | Perche` semanticamente |
|---|---|---|
| «C'e` un elemento del genere?» | |
conserva il fatto della presenza |
| «Eliminare i duplicati» | Set oppure Set + Array | Set elimina, Array mantiene l'ordine |
| «Trovare gli elementi in comune di due liste» | |
ci sono intersection e amici |
| «Quante volte compare?» | |
serve un contatore, non solo il fatto della presenza |
Se riesci a prendere questo stile di pensiero «a domanda», comincerai a scegliere la struttura dati quasi automaticamente.
7. Errori tipici quando si usa Set per i controlli di membership
Errore n. 1: aspettarsi che Set conservi l'ordine.
Una trappola molto comune: fai Set(array) e poi ti stupisci perche` gli elementi si sono «mescolati». Set e` pensato per l'unicita` e per controlli rapidi, mentre l'ordine non fa parte del suo contratto. Se l'ordine conta, conserva il risultato in un array e usa seen set come filtro.
Errore n. 2: risolvere il problema del «primo unico» solo con Set.
Set risponde alla domanda «c'e` l'elemento in generale», ma non conserva «quante volte e` apparso» e «in quale punto e` apparso per la prima volta». Per questo, per problemi come «trova il primo simbolo unico», di solito serve un dizionario delle frequenze + una seconda passata. Set qui puo` essere parte della soluzione, ma non tutta la logica.
Errore n. 3: fare il controllo di membership in un array dentro un ciclo, quando i controlli sono molti.
Un codice del tipo «per ogni elemento controllo array.contains(...)» spesso diventa in modo silenzioso una complessita` quadratica, soprattutto se dentro c'e` un altro ciclo. Se dentro un ciclo chiedi regolarmente «l'abbiamo gia` visto?», quasi sempre conviene introdurre Set.
Errore n. 4: confondere «deduplicazione» con «unificazione con mantenimento dell'ordine».
Se il problema suona come «rimuovi i duplicati», non avere fretta. A volte si intende «rimuovi i duplicati, ma lascia l'ordine come nella lista originale». Un solo Set non mantiene l'ordine, quindi usa la coppia: Set per il controllo e un array per il risultato.
Errore n. 5: provare a conservare in Set dati che hanno bisogno di informazioni aggiuntive.
Se capisci che non ti serve solo «c'e` l'elemento», ma anche «quante volte», «in quali posizioni», «con quale contesto», allora Set diventa un contenitore troppo povero. In quel momento e` meglio passare a Dictionary, dove il valore puo` essere un contatore, un array di posizioni o qualsiasi altro payload utile.
GO TO FULL VERSION