Peppe, sei un tipo alquanto generico. Quello che può sembrare un insulto, per gli sviluppatori, è quasi un complimento. Eh si, perché la programmazione generica o, Generics con il linguaggio Swift, è qualcosa che eleva le tue capacità di scrivere e progettare le applicazioni.
Se è la prima volta che ne senti parlare, non fartene una colpa. Chi sa cosa sono i Generics, o crede di saperlo, si muove sempre nell’oscurità. Aspettano il momento buono per uscirsene allo scoperto con questi fantomatici Generics per farti sentire in imbarazzo o quanto meno perplesso.
Ma bando alle ciance, cosa sono i Generics e come si usano con il linguaggio Swift?
Partiamo da un presupposto. Se vuoi diventare un ottimo sviluppatore, non solo di applicazioni iOS, devi essere in grado di riutilizzare il codice che crei. Non solo tra un progetto ed un altro ma anche tra i file del tuo Xcode Project. Quindi, presta attenzione a questo strumento, prima o poi ti ci imbatterai.
Ma andiamo a noi. Per farti capire cosa sono i Generics partiamo da un esempio classico.
Ci sono due variabili all’interno della tua applicazione, a e b, e ad un certo punto ti serve invertire i loro valori. Cioè a deve contenere il valore di b e b il valore di a. In pratica, se a=6 e b=1, alla fine della storia b=6 ed a=4.
Che difficoltà c’è in questo? Nessuna aggiungerei io. Dato che conosci le funzioni del linguaggio Swift, provvederesti subito a creare una funzione di swap analoga alla seguente:
var a = 6 var b = 4 func swapInt(a: inout Int, b: inout Int) { let temp = a a = b b = temp } swapInt(a: &a, b: &b) print("a = \(a)") // a = 4 print("b = \(b)") // b = 6
E se adesso ti chiedessi di provare ad invertire due variabili String?
Aspetta un momento Peppe, ma il codice non è sempre lo stesso con i Tipi di Dato della funzione cambiati?
Esattamente. Solo che, non potendo più utilizzare la funzione swapInt, creata poc’anzi, saresti costretto a crearne una nuova, con il corpo (cioè il codice) uguale e l’intestazione diversa:
func swapString(a: inout String, b: inout String) {/*codice uguale al precedente*/}
Qui entrano in gioco i Generics.
I Generics permettono di creare funzioni, classi, strutture, enum e quant’altro in con una sintassi universale. Cioè che non dipendono da un Tipo di Dato definito (che sia di sistema o generato dallo sviluppatore).
Comincia a capire l’importanza di questo strumento?
Allora vediamo insieme come usare i Generics con il linguaggio Swift. Si parte!
Capire i Generics
Essendo per definizioni generici, i Generics non hanno una simbologia definita. Cioè non esiste l’equivalente di String/Double ecc per i Generics. Lo dico perché ci cascano tutti.
Esistono, invece, delle regole e delle sintassi particolari che ti permettono di definire un qualcosa come Generics (altri linguaggi di programmazione, tipo Java, esortano ad utilizzare una simbologia ben definitiva anche se non obbligatoria).
La regola è abbastanza semplice.
Dopo il nome della funzione, classe ecc vengono fatte seguire le parentesi angolari <>. Da questo momento in poi, il compilatore, sa che stai per utilizzare i Generics.
Cosa va dentro le parenti angolari?
Dentro le angular brackets <> puoi scrivere qualsiasi cosa. Questo qualcosa che scriverai, farà da placeholder o rimpiazzo, per il Tipo di Dato che andrai ad utilizzare realmente.
Dato che è difficile da spiegare a parole, ti faccio subito un esempio.
Riprendiamo la funzione di swap e proviamo a convertirla in una Generics function.
Chiamiamo swapTwoElement e subito il nome piazziamo le parentesi angolari. All’interno delle parentesi mettiamo la lettera maiuscola T (diminutivo di Type). A questo punto, invece di utilizzare un tipo reale, per i parametri della funzione, potrai utilizzare quella T messa all’interno delle parentesi angolari.
func swapTwoElement<T>(a: inout T, b: inout T) { let temp = a a = b b = temp }
Cioè quel T, messo dentro le parentesi angolari, viene identificato solo in questo contesto, come nuovo Tipo di Dato fittizio da poter utilizzare a nostro piacimento.
E l’utilizzo? La cosa fantastica e che, all’occhio dell’utilizzatore, non cambia assolutamente niente se non il dover utilizzare una ed una sola funzione per tutte queste operazioni di swap:
var a = 6; var b = 4 swap(&a, &b) var peppe = "Giuseppe" var delia = "Delia" swap(&peppe, &delia)
Voglio essere logorroico.
Non ho usato T perché me lo ha imposto Steve Jobs dall’alto dei cieli. Al posto di T, dentro le angolari, avrei potevo scrivere qualsiasi cosa. L’importante è capire che è solamente un nome fittizio (chiamato placeholder) per permetterti di lavorare con qualcosa di astratto e non definito.
Cioè potevi scrivere anche:
func laMiaGenerics<CazzNeSo>(a: CazzNeSo, b: CazzNeSo) {}
Questa è la regola principale per capire e comprendere i Generics del linguaggio Swift. Adesso vediamo come applicarli e alcuni casi particolari.
Classi Generics
Anche le classi possono essere definite come tipi Generics.
Immagina di voler creare una classe che gestisce una lista di cose non definite. Possono essere Oggetti, String, Double, non ha importanza. La classe deve poter restituire il primo elemento e l’ultimo elemento della lista che gestisce.
Anche per le classi, dopo il nome, vengono fatte seguire le parentesi angolari contenenti il nome del nuovo tipo generico. Le proprietà ed i metodi potranno utilizzare quel placeholder come nuovo tipo fittizio:
class Lista<Element> { var lista: [Element] = [] init(data: [Element]) { self.lista = data } func firstElement() -> Element { return lista[0] } func lastElement() -> Element { return lista[lista.endIndex] } }
Per utilizzarla, in fase di inizializzazione del nuovo oggetto, subito dopo il nome dovrai specificare il Tipo di Dato reale che dovrà utilizzare la Lista, i suoi metodi ed attributi.
Per esempio, se volessi creare una Lista di stringhe, scriverei:
var listaStringhe = Lista<String>(data: ["ciccio", "peppe", "chiara"]) listaStringhe.firstElement()
Potresti anche utilizzarlo come contenitore di altri oggetti.
Se la tua app serve a gestire una fattoria di animali, ti potrebbe essere utile la classe Lista generica per conservarli tutti dentro ad un contenitore:
class Animale {} class Mucca: Animale {} class Cavallo: Animale {} var muccaPippo = Mucca() var cavalloGino = Cavallo() var listaAnimaliFattoria = Lista<Animale>(data: [muccaPippo, cavalloGino])
Nota bene che non ho dovuto riscrivere una nuova classe per la gestione della fattoria. Ho semplicemente riutilizzato la classe Lista con un contesto differente.
Gli stessi esempi, senza Generics, avrebbero comportato la scrittura di due classi Lista differenti.
Type Constraint
Esercizio 0. Proviamo a creare una semplice funzione Generics che permetta di confrontare due valori dello stesso tipo. Se i valori sono uguali deve restituire true.
func confronta<T>(a: T, b: T) -> Bool { guard a == b else {return false} return true }
Il nostro Playground genera un errore inaspettato. Ovvero ci comunica che l’operatore di comparazione == non può essere applicato a due oggetti di tipo T, perché?
Questo accade perché, i Generics, per loro natura sono svincolati da qualsiasi tipo di vincolo.
Per dirla in maniera più tecnica, i Generics, non ereditano da nessun Protocol o Classe.
Peppe, cosa vuol dire?
I Tipi di Dato del linguaggio Swift, come le String, Double ecc sono delle Struct che ereditano alcuni comportamenti da diversi protocolli. I protocolli, ti ricordo, obbligano una Classe/Struct ad implementare dei metodi.
Nel particolare, i Tipi di default, implementano un protocollo chiamato Equatable. Il protocollo Equatable permette di determinare il comportamento dell’operatore == per quel particolare Tipo.
In base a questa semplice considerazione possiamo dire con certezza che il nostro tipo generico T non eredita dal protocollo Equatable.
Voglio arrivare a dirti che, i Generics, possono essere specializzati facendogli ereditare delle caratteristiche dalle Classi o Protocolli. Quest’operazione prende il nome di Type Constraint (costringere un tipo a comportarsi in un determinato modo).
Puoi aggiungere un Constraint ad un tipo Generics utilizzando questa sintassi:
func generica<T: Protocol, Z: Class>() class Generica<T: Protocol> {}
Se adesso costringiamo la funzione confronta ad utilizzare il protocollo Equatable, riusciamo facilmente a risolvere l’errore:
func confronta<T: Equatable>(a: T, b: T) -> Bool { guard a == b else {return false} return true } confronta(a: "Giuseppe", b: "Delia") // false confronta(a: 2, b: 2) // true
Esercizio 1. Vogliamo creare una funzione generica che permetta di contare, all’interno di un Array qualsiasi, la ricorrenza di un determinato valore. Per esempio, se hai il seguente array [4,67,4,7,2] ed il valore da cercare è 4, la funzione deve stampare “ci sono due 4 nell’array“.
Ovviamente la funzione deve essere riutilizzabile in altri contesti, quindi anche con array di String, Oggetti ecc.
func countOccurrences <T: Comparable> (array: [T], element: T) -> Int { var n = 0 for x in array { if x == element {n += 1} } return n }
Se sei alle prime armi con il linguaggio Swift ti puoi fermare anche qui con i Generics. Il prossimo paragrafo l’ho pensato per chi ha già un po’ le mani in pasta con lo sviluppo delle applicazioni.
Protocolli e Associated Type
La regola vale anche per i protocolli. Anch’essi possono essere definiti come Generics. Ma cosa succederebbe se i metodi del protocollo avessero bisogno di gestire oggetti che implementano quel particolare protocollo?
Per queste particolare situazioni, si può creare un protocollo di tipo Generics, utilizzando la parola chiave associatedtype seguita dal nome di un Generic Type per riferirti a qualsiasi tipologia d’oggetto non ancora specificato.
protocol SomeProtocol { associatedtype GenericType } // Questa sintassi è l'equivalente di protocol SomeProtocol<GenricType> N.B. Che non puoi utilizzare nei protocolli
Vediamo un esempio.
Immaginiamo d’avere un protocollo che definisca i metodi di login ad una piattaforma (Firebase, il tuo Oauth sys ecc).
Il protocollo lo chiamiamo Loggable. Al suo interno, definiremo una funzione login con un completionHandler (una closure) che conterrà l’istanza del LoggableObject (il GenericType che conterrà i valori de login corretto) ed un ErrorType per la gestione dell’errore (un altro generic enum definito sotto):
protocol Loggable { associatedtype LoggableObject func login(withCompletion: (LoggableObject?, ErrorType<String>?) -> ()) } enum ErrorType<T> { case failLogin(T) // T rappresenterà il numero d'errore, la stringa d'errore ecc. }
Adesso crea una classe Utente che implementa questo protocollo. Nella fase di implementazione dei metodi definiti dal protocol Loggable potrai aggiungere, al primo parametro della withCompletion, il tipo della classe in cui si trova il metodo:
class Utente: Loggable { func login(withCompletion: (Utente?, ErrorType<String>?) -> ()) { } }
Interessante, no?
[su_spoiler title=”Esempio protocol Loggable applicato” style=”fancy”]
class Utente: Loggable { var nome: String! func login(withCompletion: (Utente?, ErrorType<String>?) -> ()) { /* qualche altra funzione per il recupero dell'utente */ let utenteLoggato: Utente? = self // cambia a nil per simulare il login non avvenuto guard utenteLoggato != nil else { withCompletion(nil, .failLogin("problema nel login")) return } /* recupero i dati dell'utente e li assegno a self */ utenteLoggato!.nome = "Giuseppe" withCompletion(utenteLoggato!, nil) } } var utenteA = Utente() utenteA.login { (utente, error) in if error != nil { print(error!) return } print("Benvenuto", utente!.nome) }
[/su_spoiler]
[su_spoiler title=”Esempio di un protocol Generics per la gestione di una lista applicando ad una classe generica per la descrizione della lista” style=”fancy”]
import UIKit protocol List { associatedtype Item func insert(newElement: Item) } class SomeList<T> : List { var list: [T] init() { self.list = [] } func insert(newElement: T) { self.list.append(newElement) } } class Cane {} var rex = Cane() var listaCani = SomeList<Cane>() listaCani.insert(newElement: rex)
[/su_spoiler]
Con questo mi fermo qui come primo approccio ai Generics con il linguaggio Swift. Voglio comunque elencarti alcune motivazioni che dovrebbero spingerti ad utilizzarli.
Considerazioni
Se il tuo obiettivo è quello di scrivere un paio di applicazioni, allora no, i Generics non fanno per te. I Generics nascono per la riusabilità e la condivisione del codice. La maggior parte dei framework di terze parti, come quelli standard, utilizzano i Generics proprio per lasciarti piena autonomia d’utilizzo.
Quindi se stavi cercando un sistema per aumentare la tua capacità di scrivere, riutilizzare, astrarre e condividere il codice, allora i Generics fanno proprio al caso tuo.
Non ho trattato tutti gli argomenti che concernono il mondo dei Generics. Essendo dei concetti astratti avrai bisogno di una certa quantità di tempo per poterli sentire pienamente tuoi. Ecco perché non ho voluto sovraccaricare questo tutorial.
Sicuramente scriverò un altro articolo in cui ti farò vedere qualche utilizzo applicato dei Generics con il linguaggio Swift.
Se è la prima volta che approdi sul mio sito, dai un’occhiata a:
Buona Programmazione!
👨💻
Hai bisogno d’aiuto?
Scrivi un commento in fondo alla lezione