Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Overriding e polimorfismo

Imparare ad utilizzare i costrutti del linguaggio Swift per implementare l'overriding dei metodi e sfruttare il polimorfismo.
Imparare ad utilizzare i costrutti del linguaggio Swift per implementare l'overriding dei metodi e sfruttare il polimorfismo.
Link copiato negli appunti

Nell'articolo precedente abbiamo accennato al fatto che se una sottoclasse necessita di ridefinire un inizializzatore con la stessa signature (l'elenco degli argomenti e i rispettivi tipi, nonché il tipo del valore di ritorno) di un inizializzatore della sua superclasse, è necessario marcarlo esplicitamente con la parola riservata override.

In realtà è possibile utilizzare lo stesso meccanismo per ridefinire qualsiasi metodo di una superclasse. Questo è utile nel caso in cui il metodo della sottoclasse voglia fornire un'implementazione diversa rispetto a quello della superclasse.

Consideriamo nuovamente la classe Veicolo, a cui aggiungiamo due metodi accelera: uno per incrementare il valore della proprietà velocita di un dato incremento, e l'altro per incrementarla di un valore fisso (es.: 10). Si dice che i metodi accelera sono overloaded, dato che hanno lo stesso nome, ma differente signature:

class Veicolo {
    var marca: String
    var modello: String
    var velocita: Int
    init(marca: String, modello: String, velocita: Int) {
        self.marca = marca
        self.modello = modello
        self.velocita = velocita
    }
    func accelera(incremento: Int) {
        self.velocita += incremento
    }
    func accelera() {
        print("accelero veicolo")
        self.accelera(incremento: 10)
    }
}

Definiamo adesso una sottoclassse Auto, in cui vogliamo ridefinire (o farne l'override) il metodo accelera, permettendogli di incrementare la velocità con un valore costante diverso da quello della classe base. Tale valore sarà determinato dal tipo di cambio definito nella proprietà della sottoclasse tipoCambio:

class Auto: Veicolo {
    var tipoCambio: String = "manuale"
    init(marca: String, modello: String, velocita: Int, tipoCambio: String) {
       self.tipoCambio = tipoCambio
        super.init(marca: marca, modello: modello, velocita: velocita)
    }
    override func accelera() {
        print("accelero auto")
        if tipoCambio == "manuale" {
            super.accelera(incremento: 10)
        } else {
            super.accelera(incremento: 20)
        }
    }
}

Notiamo come sia stato necessario utilizzare la parola riservata override, altrimenti il compilatore avrebbe restituito un errore. Inoltre il metodo ridefinito ha richiamato il metodo accelera della superclasse, usando la parola riservata super.

Se creiamo un'istanza di Veicolo e una di Auto:

var v = Veicolo(marca: "N/A", modello: "Carretto", velocita: 20)
print("Velocità corrente: \(v.velocita)")
// Velocità corrente: 20
v.accelera()
print("Il \(v.modello) ha adesso velocità \(v.velocita)\n")
// Il Carretto ha adesso velocità 30
var a = Auto(marca: "Audi", modello: "Q3", velocita: 130, tipoCambio: "automatico")
print("Velocità corrente: \(a.velocita)")
// Velocità corrente: 130
a.accelera()
print("La \(a.modello) ha adesso velocità \(a.velocita)\n")
// La Q3 ha adesso velocità 150

noteremo che vengono invocati correttamente i metodi delle rispettive sottoclassi.

L'utilizzo esplicito della parola riservata override rende chiaro a chi legge il codice, che l'implementazione del metodo è diversa da quella della classe padre. Inoltre aiuta il compilatore Swift ad effettuare ulteriori verifiche: se lo sviluppatore non digita correttamente il nome del metodo ridefinito, il compilatore avviserà lo sviluppatore informandolo che il metodo è inesistente nella superclasse. O viceversa, se in una sottoclasse si sceglie un nome di metodo già esistente nella superclasse, il compilatore Swift ne darà notifica.

Non sempre però si vuole che un metodo possa essere ridefinito. Per prevenire ciò è possibile marcare il metodo della superclasse con la parola riservata final:

class Veicolo2 {
...
    final func accelera() {
        ...
    }
...
}

Se una sottoclasse provasse a ridefinire il metodo accelera, il compilare lo segnalerebbe immediatamente.

Polimorfismo

Grazie all'ereditarietà e all'overriding, possiamo implementare in Swift un meccanismo molto importante della programmazione orientata agli oggetti: il polimorfismo.

Il polimorfismo permette di usare un'istanza di una sottoclasse al posto di un'istanza della superclasse. Il vantaggio che ne ha lo sviluppatore è quello di avere una stessa interfaccia (ossia la medesima signature) ma implementazioni diverse per uno stesso metodo.
Questo meccanismo si applica quando si utilizza l'ereditarietà: ad una variabile o proprietà, e più in generale ad un'espressione, definita come tipo di una data superclasse, è possibile assegnare istanze delle sue sottoclassi. Durante l'esecuzione il runtime di Swift verificherà di che tipo sia effettivamente l'istanza e invocherà l'implementazione corretta.

Facciamo chiarezza con un esempio. Definiamo una nuova sottoclasse Moto di Veicolo, in cui ridefiniamo nuovamente il metodo accelera:

class Moto: Veicolo {
    var cilindrata: Int = 50
    init(marca: String, modello: String, velocita: Int, cilindrata: Int) {
        self.cilindrata = cilindrata
        super.init(marca: marca, modello: modello, velocita: velocita)
    }
    override func accelera() {
        print("accelero moto")
        if cilindrata > 50 {
            super.accelera(incremento: 30)
        } else {
            super.accelera(incremento: 5)
        }
    }
}

e creiamone un'istanza:

var m = Moto(marca: "Malaguti", modello: "Password", velocita: 80, cilindrata: 250)

Abbiamo dunque un'implementazione diversa di accelera nella superclasse e nelle sue due sottoclassi Auto e Moto.

Se adesso definiamo una variabile vv di tipo Veicolo, a questa possiamo assegnare un'istanza di qualsiasi sua sottoclasse, ad esempio di Moto o di Auto, e questo potrebbe anche variare durante l'esecuzione. Si dice dunque che vv è polimorfica dato che il suo tipo di fatto verrà determinato durante l'esecuzione:

var vv: Veicolo
vv = m      // questo potrebbe variare a runtime
print("vv è un Veicolo? \(vv is Veicolo)")
// vv è un Veicolo? true
vv.accelera()
// accelero moto

In questo caso viene richiamata la versione accelera di Moto, nonostante la variabile vv sia stata dichiarata di tipo Veicolo.

Notiamo che abbiamo utilizzato una nuova parola riservata: is. Essa ci consente di verificare durante l'esecuzione se un'istanza è di un dato tipo o meno.

Vediamo un altro esempio. Creiamo un array di Veicoli:

var veicoli: [Veicolo] = [v, a, m]

Ricordiamo che in Swift gli elementi di un array devono essere dello stesso tipo (l'eccezione è per il tipo AnyObject). Ci potremmo dunque aspettare un errore di compilazione, dato che il secondo e terzo elemento sono rispettivamente di tipo Auto e Moto, ma invece non è così, dato che questi ereditano da Veicolo, e per Swift ogni istanza di una sottoclasse è anche istanza della classe base, come abbiamo detto sopra.

Se iteriamo sull'array veicoli e invochiamo il metodo accelera su ognuno dei suoi elementi, vedremo di nuovo il meccanismo di polimorfismo in azione:

for i in veicoli {
    print("Velocita corrente di \(i.marca) \(i.modello): \(i.velocita)")
    i.accelera()
    print("Nuova velocita di \(i.marca) \(i.modello): \(i.velocita)\n")
}

L'output del precedente codice sarà:

Velocita corrente di N/A Carretto: 30
Accelero veicolo
Nuova velocita di N/A Carretto: 40
Velocita corrente di Audi Q3: 150
Accelero auto
Nuova velocita di Audi Q3: 170
Velocita corrente di Malaguti Password: 80
accelero moto
Nuova velocita di Malaguti Password: 110

Come si evince, Swift invoca l'implementazione del metodo corretto durante l'esecuzione, nonostante la variabile m sia di tipo Veicolo.

Downcasting

Oltre all'operatore is, possiamo utilizzare as! e as? per "convertire" (in realtà non avviene nessuna conversione), o meglio trattare un'istanza di una data classe come un'istanza di una sua sottoclasse, rispettivamente in modo forzato o condizionale. Il codice che segue chiarisce quest'ultimo aspetto:

var t = vv as! Moto     // t è di tipo Moto adesso
t.accelera()
// accelero moto
var t2 = vv as? Moto    // t2 è di tipo Moto?
t2?.accelera()          // se vv riesce ad essere convertito bisogna effettuare l'unwrapping
// accelero moto

Il playground e tutti gli esempi di questo articolo sono reperibili su GitHub.


Ti consigliamo anche