Costruttori in Scala

Costruttori in Scala

Nel post precedente abbiamo visto come creare una classe, in questo breve articolo esaminiamo più in dettaglio i costruttori in Scala e le loro proprietà. Ripartiamo dall’esempio con cui c’eravamo lasciati la volta scorsa:

package it.cosenonjaviste

class HardDisk(val size: Int, var usedSpace: Int) {

  override def toString = s"HardDisk[Size $size, Used space $usedSpace]"
}

Abbiamo detto che la prima riga fonde la definizione della classe con il suo costruttore principale e che size e usedSpace sono contemporaneamente parametri del costruttore e member variable che verranno allocate nella classe. Il primo parametro è di tipo val, quindi una volta assegnato non sarà modificabile perché non ci aspettiamo di cambiare la dimensione dell’hard disk una volta creato, mentre il secondo parametro è variabile e sarà preceduto dal modificatore var.

Cosa succede però se omettiamo val oppure var? E’ ancora una dichiarazione valida? Aggiungiamo ad esempio l’etichetta del disco tra i parametri del costruttore e vediamo che succede.

class HardDisk(val size: Int, var usedSpace: Int, label: String) {

  override def toString = s"HardDisk[Size $size, Used space $usedSpace, Label: $label]"
}

Il compilatore Scala non si lamenta se vede questo codice: esaminiamo il dietro le quinte. Viene aggiunta alla classe una member variable privata chiamata label ma priva di setter e getter, inoltre, il suo valore sarà immutabile. L’automatismo si attiva a condizione che il parametro venga effettivamente usato da qualche parte nella classe, in caso contrario non verrà creato nulla. Siamo in un caso esattamente opposto rispetto a quanto avviene per i primi due parametri che, anche se non usati, generano member variable, setter e getter.

Costruttori ausiliari

Ora la nostra classe ha tre parametri, ma non vogliamo toccare nella nostra applicazione il codice che la istanzia. Da bravi Javisti sappiamo che la soluzione è quella di aggiungere un costruttore che prenda solo i primi due parametri. Scala chiama ausiliari questi costruttori aggiuntivi e impone due semplici regole. La prima è che il costruttore ausiliario richiami il costruttore principale oppure un altro costruttore ausiliario. La seconda è che l’invocazione del costruttore prescelto sia fatta nella prima riga del costruttore ausiliario. Nel nostro caso quindi aggiungiamo un costruttore che prenda solo dimensione e spazio occupato ma tralasci l’etichetta.

class HardDisk(val size: Int, var usedSpace: Int,  label: String) {

  def this(size: Int, usedSpace: Int) = {
    this(size, usedSpace, "")
    println("Costruttore ausiliario che chiama il principale")
  }

  override def toString = s"HardDisk[Size $size, Used space $usedSpace, Label $label]"

}

Rispetto al costruttore principale, il nome della classe non viene ripetuto ma si usa invece this, inoltre va notato che ne var ne val vengono ripetuti, anzi è un errore usarli perché Scala non permette di cambiare nel costruttore ausiliario la natura del parametro.

Volendo possiamo aggiungere anche il costruttore che usa solo la dimensione.

  def this(size: Int) = {
    this(size, 0)
    println("Costruttore ausiliario che chiama un altro costruttore ausiliario")
  }

In questo caso il secondo costruttore ausiliario non chiama il principale, ma, sempre in conformità alle regole, il primo costruttore ausiliario che abbiamo definito. Se dopo aver definito i costruttori ausiliari, vogliamo fare in modo che non sia possibile chiamare il costruttore principale dall’esterno della classe, possiamo definirlo privato aggiungendo la keyword private. L’unica cosa un po’ spiazzante all’inizio per chi scrive abitualmente in Java è il posto dove va posizionata:

class HardDisk private(val size: Int, var usedSpace: Int,  label: String) {

  def this(size: Int, usedSpace: Int) = {
    this(size, usedSpace, "")
    println("Costruttore ausiliario che chiama il principale")
  }

  override def toString = s"HardDisk[Size $size, Used space $usedSpace, Label $label]"

}

Costruttori ausiliari? Ma anche no

Bene, non abbiamo fatto tempo a padroneggiare i costruttori ausiliari che scopriamo che Scala, almeno in questi casi semplici, ci permette di farne a meno 🙂 . Tutti i metodi, costruttori compresi, ci permettono di specificare un valore di default per ogni parametro. Come si intuisce, questo valore verrà usato quando non viene fornito nel codice chiamante facendo diventare il parametro di fatto opzionale. Volendo quindi risparmiare la fatica di definire due costruttori ausiliari, possiamo riscrivere la classe in questo modo:

class HardDisk(var size: Int, var usedSpace: Int = 0,  label: String= "") {

  override def toString = s"HardDisk[Size $size, Used space $usedSpace, Label $label]"
}

Sono dunque valide queste chiamate:

var hd1 = new HardDisk(250, 10, "Documenti")
var hd2 = new HardDisk(250, 10)
var hd3 = new HardDisk(250)

E se si volesse passare l’etichetta ma non lo spazio occupato? Anche a questa domanda Scala ha una risposta: i named parameters. Possiamo scrivere i parametri passati ad un metodo (e quindi anche ad un costruttore) come se stessimo specificando una mappa, cioè usandone il nome. Per capire di cosa si tratta, riscriviamo l’esempio precedente usando questa feature.

var hd1 = new HardDisk(size = 250, usedSpace = 10, label = "Documenti")
var hd2 = new HardDisk(size = 250, usedSpace = 10)
var hd3 = new HardDisk(size = 250)

Quindi per creare un hard disk usando solo l’etichetta e la dimensione basterà specificare unicamente questi due parametri, anche non in ordine

var hd4 = new HardDisk(size = 250, label = "Documenti")
var hd5 = new HardDisk(label = "Documenti", size = 250)

Oltre a permetterci di usare solo i parametri opzionali che vogliamo, guadagniamo in leggibilità del codice, soprattutto se abbiamo molti parametri dello stesso tipo. E’ possibile anche mescolare parametri posizionali e parametri per nome, ma oltre a essere meno flessibile, a mio avviso una cosa tipo new HardDisk(250, label = "Documenti") non è elegante ne tantomeno di semplice comprensione.

Conclusioni

In questo post abbiamo visto le proprietà dei costruttori di Scala parlando sia del costruttore principale che di quelli ausiliari. L’argomento ci è tornato utile anche per introdurre i parametri con valori di default, un’utility sintattica che ci permette di risparmiare l’esplosione di metodi “finti” usati solo per specificare un parametro di default. I named parameter sono invece interessanti per migliorare la leggibilità del codice e assegnare solo alcuni dei parametri opzionali di un metodo.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Radicalbit. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub