Le estensioni sono un potente meccanismo di Swift che ci consente di aggiungere funzionalità a classi esistenti. Possiamo aggiungere nuovi metodi, inizializzatori, proprietà (calcolate). Oltre ad estendere una classe da noi definita, è possibile utilizzare le estensioni per aggiungere nuove metodi a classi di cui non si ha il codice sorgente, come quelle nei framework distribuiti da Apple.
Vediamone la sintassi con un esempio. Consideriamo nuovamente la classe Persona
e creiamone un'instanza:
class Persona {
var nome: String
var cognome: String
var eta: Int
init(nome: String, cognome: String, eta: Int) {
self.nome = nome
self.cognome = cognome
self.eta = eta
}
}
var p = Persona(nome: "Antonio", cognome: "Calanducci", eta: 105)
Supponiamo adesso di voler aggiungere due nuovi metodi: infoComplete()
e ringiovanisci()
. Possiamo procedere in questo modo:
extension Persona {
func infoComplete() {
print("\(nomeCompleto) di età \(eta)")
}
func ringiovanisci(anni: Int) {
self.eta -= anni
}
}
Ci basterà includere le nuove funzioni in un blocco preceduto dalla parola riservata extension
e dal nome della classe che vogliamo estendere.
Adesso possiamo invocare i nuovi metodi su p
:
p.infoComplete()
// Antonio Calanducci di età 105
p.ringiovanisci(10)
p.infoComplete()
// Antonio Calanducci di età 95
Chi conosce Objective-C avrà intuito che le estensioni forniscono funzionalità simili a quelle offerte dalla categorie. A differenze di queste, in Swift non è necessario dare un nome all'estensione.
Prima di presentare ulteriori usi delle estensioni, introduciamo le proprietà calcolate.
Proprietà calcolate
Come abbiamo visto già in moltissimi esempi, ogni classe è composta da metodi e proprietà. Finora abbiamo utilizzato le cosiddette proprietà memorizzate (stored properties) per le quali viene allocato spazio in memoria per conservarne il contenuto. Oltre a queste, in realtà, esistono anche le cosiddette proprietà calcolate, che di fatto non hanno un'occupazione di memoria ma sono calcolate "al volo" ogni volta che vengono accedute o modificate.
Aggiungiamo la proprietà calcolata nomeCompleto
alla classe Persona
, inserendo il seguente frammento di codice dopo la definizione della proprietà eta
:
var nomeCompleto: String {
return "\(self.nome) \(self.cognome)"
}
Dall'esempio si evince che dopo la dichiarazione di una proprietà calcolata, a cui si deve necessariamente indicare il tipo, si utilizza un blocco di codice che ne determina il valore durante gli accessi. Ad esempio:
p.nomeCompleto
// "Antonio Calanducci"
In realtà la sintassi precedente è la versione abbreviata della definizione di una proprietà calcolata, in quanto la versione estesa ha la seguente forma:
var nomeCompleto: String {
get {
return "\(self.nome) \(self.cognome)"
}
}
Si noti che è stata utilizzata la parola riservata get
per implementare il getter della proprietà. È possibile definire anche un setter che viene invocato quando si cambia valore alla proprietà. Questa sintassi è uguale a quella del linguaggio di programmazione C#.
Vediamo un esempio utilizzando la classe Quadrato
:
class Quadrato {
var lato: Double
var area: Double {
get {
return lato * lato
}
set {
self.lato = sqrt(newValue)
}
}
init(lato: Double) {
self.lato = lato
}
}
Quadrato
definisce due proprietà, una memorizzata lato
e una calcolata area
, dove per quest'ultima definiamo sia un getter che un setter. All'interno del setter si ottiene il riferimento al valore appena assegnato tramite la costante predefinita newValue
.
Se creiamo un oggetto di tipo Quadrato
inizializzandolo con un dato lato
:
var q = Quadrato(lato: 5.0)
q.area // 25
q.area = 144 // {lato 12}
q.lato // 12
possiamo calcolarne l'area accedendo al valore della proprietà calcolata area
. Se invece assegniamo direttamente un valore alla proprietà area
, Swift invocherà il suo setter che calcolerà e assegnarà il corretto valore alla proprietà memorizzata lato
. Notiamo quindi che getter e setter sono dei veri e propri metodi che possono agire su ogni proprietà di una classe.
Osservatori di proprietà memorizzate
Una caratteristica di Swift che si trova a metà strada tra le capacità offerte dalle proprietà memorizzare e quelle calcolate è quella dei cosiddetti osservatori. Questi sono dei blocchi di istruzioni che vengono eseguiti quando una proprietà memorizzata sta per cambiare valore o l'ha già cambiato tramite un'assegnazione. Questo meccanismo viene implementato rispettivamente tramite le parole riservate willSet
e didSet
.
Creiamo una nuova classe Persona2
alla quale aggiungiamo due osservatori delle proprietà nome
e cognome
in modo da stampare un messaggio sulla console quando il valore di nome
sta per essere modificato e quando il valore di cognome
è appena stato variato:
class Persona2 {
var nome: String {
willSet {
print("stiamo per cambiare nome da \(nome) a \(newValue)")
}
}
var cognome: String {
didSet {
print("abbiamo cambiato cognome da \(oldValue) a \(cognome)")
}
}
var nomeCompleto: String {
return "\(nome) \(cognome)"
}
init(nome: String, cognome: String) {
self.nome = nome
self.cognome = cognome
}
}
All'interno del corpo di willSet
la costante newValue
ci permette di accedere al nuovo valore che la proprietà nome
sta per assumere, mentre in didSet
possiamo usare la costante oldValue
che conterrà il valore precedente di cognome
. All'interno di didSet
potremmo di fatto modificare ulteriormente il valore di cognome
se è necessario.
Se instanziamo un oggetto di tipo Persona2
e riassegnamo i valori alle proprietà nome
e cognome
, vedremo sulla console l'output delle istruzioni print
che abbiamo aggiunto nel corpo dei due osservatori:
var p2 = Persona2(nome: "Mario", cognome: "Rossi")
p2.nome = "Rosanna"
// stiamo per cambiare nome da Mario a Rosanna
p2.cognome = "Bianchi"
// abbiamo cambiato cognome da Rossi a Bianchi
Estensioni e proprietà calcolate
Adesso che conosciamo il funzionamento delle proprietà calcolate, vediamo come possiamo aggiungerle a classi pre-esistenti tramite le estensioni.
Tornando all'esempio della classe Persona
, se volessimo aggiungere una nuova proprietà che ci ritorni il nomeCompleto
convertito in maiuscolo, possiamo utilizzare il codice seguente:
extension Persona {
var tuttoMaiuscolo: String {
return self.nomeCompleto.uppercased()
}
}
p.tuttoMaiuscolo
// "ANTONIO CALANDUCCI"
La nostra proprietà calcolata sarà di sola lettura, dato che è stato fornito soltanto un getter. Ricordiamo che in questo caso non è obbligatorio utilizzare esplicitamente la parola riservata get
, dato che questa è la forma implicita.
Abbiamo dunque aggiunto sia metodi che proprietà calcolate ad una classe esistente. Tuttavia non possiamo aggiungere proprietà memorizzate nè fare l'overriding di metodi esistenti. In entrambi questi casi sarà necessario utilizzare il meccanismo di ereditarietà e creare una nuova sottoclasse.
Questo è dunque uno dei criteri che possiamo adottare per decidere se è più conveniente utilizzare un'estensione o creare una sottoclasse.
Le estensioni rappresentano anche un modo per raggruppare insieme funzionalità distinte di una classe. Possiamo infatti creare molteplici estensioni per classe, dove all'interno di ognuna possiamo definire metodi, inizializzatori e proprietà calcolate correlate tra loro. Molti dei framework di Apple utilizzano questo approccio per meglio organizzare il codice, anzicchè avere un'unica ed enorme classe.
Infine ricordiamo che le estensioni possono aggiungere funzionalità a classi di cui non abbiamo il codice sorgente. Vi mostriamo questa capacità con un esempio, implementando un nuovo inizializzatore per la classe NSDate
, una delle classi fornite da Apple per la gestione delle date (a partire da Swift 3.x, il framework Foundation predilige l'uso del tipo Date
, che è implementato come una struttura, argomento che tratteremo tra qualche articolo e che supporta il meccanismo delle estensioni in maniera del tutto simile alle classi). Il nuovo inizializzatore ci permetterà di creare un'instanza a partire da una stringa che rappresenta una data:
extension NSDate
{
convenience init(dateString:String) {
let dateStringFormatter = NSDateFormatter()
dateStringFormatter.dateFormat = "dd-MM-yyyy"
let d = dateStringFormatter.dateFromString(dateString)!
self.init(timeInterval:0, since:d)
}
}
var giorno = NSDate(dateString: "18-01-2011") // "Jan 18, 2011 at 12:00 AM"
Nella prossimo lezione vedremo come estendere una classe aggiungendole la conformità ad uno o più protocolli.
Il playground con tutti gli esempi visti in questa lezione è disponibile su GitHub.