Voi siete qui: Inizio Programmare in Scala

Programmazione orientata agli oggetti in Scala: le basi

Scala è un linguaggio orientato agli oggetti come Java, Python, Ruby, Smalltalk e altri. Se provenite dal mondo Java, noterete alcuni notevoli miglioramenti rispetto alle limitazioni del modello a oggetti di Java.

Presumiamo che abbiate una certa esperienza pregressa con la programmazione orientata agli oggetti (OOP), quindi qui non ne discuteremo i principi di base, nonostante il glossario contenga alcuni termini e concetti comuni. Si veda [Meyer1997] per un’introduzione dettagliata alla OOP, si veda [Martin2003] per una recente trattazione dei principi della OOP nel contesto dello “sviluppo agile del software”, si veda [GOF1995] per imparare i pattern di progettazione e si veda [WirfsBrock2003] per una discussione sui concetti della progettazione orientata agli oggetti.

I concetti di base per classi e oggetti

Rivediamo la terminologia OOP in Scala.

Abbiamo visto in precedenza che Scala possiede il concetto di dichiarazione di oggetto, rappresentato da object, che analizzeremo nella sezione Classi e oggetti: dove sono i membri statici? del capitolo 7. Useremo il termine istanza per riferirci genericamente all’istanza di una classe, indicando sia un object che un’istanza di una classe, per evitare la possibile confusione tra questi due concetti.

Le classi si dichiarano con la parola chiave class. Vedremo più avanti che si possono usare anche parole chiave aggiuntive, come final per prevenire la creazione di classi derivate e abstract per indicare che una classe non può essere istanziata, di solito perché contiene o eredita dichiarazioni di membro senza fornirne le definizioni complete.

Un’istanza può fare riferimento a se stessa usando la parola chiave this, proprio come in Java e in linguaggi simili.

Seguendo la convenzione di Scala, usiamo il termine metodo per indicare una funzione che è legata a un’istanza. Alcuni linguaggi orientati agli oggetti usano il termine “funzione membro”. Le definizioni di metodo cominciano con la parola chiave def.

Come Java, ma a differenza di Ruby e Python, Scala ammette metodi sovraccaricati. Due o più metodi possono avere lo stesso nome purché le loro firme complete siano uniche. La firma include il nome del metodo, la lista dei parametri con i loro tipi e il tipo di ritorno del metodo. (Se i tipi di ritorno sono differenti, anche le liste di parametri devono essere differenti.)

Tuttavia, questa regola prevede un’eccezione a causa della cancellazione di tipo (in inglese, type erasure), che è una caratteristica unica della JVM, ma viene usata da Scala anche sulla piattaforma .NET per minimizzare le incompatibilità. Supponete che due metodi siano identici tranne per il fatto che uno accetta un parametro di tipo List[String] mentre l’altro accetta un parametro di tipo List[Int], come nell’esempio seguente.

// esempi/cap-5/type-erasure-wont-compile.scala
// NON verrà compilato!

object Foo {
  def bar(list: List[String]) = list.toString
  def bar(list: List[Int]) = list.size.toString
}

Otterrete un errore di compilazione sul secondo metodo perché i due metodi avranno una firma identica dopo la cancellazione di tipo.

L’interprete scala vi permetterà di digitare entrambi i metodi, perché sovrascrive la prima versione. Tuttavia, se provate a caricare l’esempio precedente usando il comando :load file otterrete lo stesso errore sollevato da scalac.

Discuteremo la cancellazione di tipo in maggior dettaglio nel capitolo 12.

Sempre per convenzione, usiamo il termine campo per indicare una variabile che è legata a un’istanza. In altri linguaggi (come Ruby) viene spesso usato il termine attributo. Notate che lo stato di un’istanza è l’unione di tutti i valori attualmente rappresentati dai campi dell’istanza.

Come abbiamo discusso nella sezione Dichiarazioni di variabile del capitolo 2, i campi a sola lettura (“valori”) si dichiarano usando la parola chiave val e i campi a lettura/scrittura si dichiarano usando la parola chiave var.

Scala permette anche di dichiarare i tipi nelle classi, come abbiamo visto nella sezione Tipi astratti e tipi parametrici del capitolo 2.

Usiamo il termine membro per fare riferimento in modo generico a un campo, un metodo, o un tipo. Notate che i membri che sono campi o metodi (ma non tipi) condividono lo stesso spazio di nomi, a differenza di quanto accade in Java. Riprenderemo l’argomento nella sezione Quando i metodi di accesso e i campi sono indistinguibili: il principio di accesso uniforme del capitolo 6.

Infine, le istanze dei tipi riferimento si creano a partire da una classe usando la parola chiave new, allo stesso modo di linguaggi come Java e C#. Notate che potete omettere le parentesi quando usate un costruttore predefinito (cioè un costruttore che non accetta argomenti). In alcuni casi, si possono usare valori letterali; per esempio, val name = "Programmare in Scala" è equivalente a val name = new String("Programmare in Scala").

Le istanze dei tipi valore, come per esempio Int, Double, &c., che corrispondono ai tipi primitivi in linguaggi come Java, si creano sempre usando valori letterali, come per esempio 1, 3.14, &c. In effetti questi tipi non posseggono costruttori pubblici, quindi un’espressione come val i = new Int(1) non verrebbe compilata.

Discuteremo la differenza tra i tipi riferimento e i tipi valore nella sezione La gerarchia di tipi di Scala del capitolo 7.

Classi genitore

Scala supporta l’ereditarietà singola, ma non l’ereditarietà multipla. Una classe figlia (o derivata) può avere una e una sola classe genitore (o base). L’unica eccezione è la radice della gerarchia di classi di Scala, Any, che non ha alcun genitore.

Abbiamo già visto diversi esempi di classi base e derivate. Riproduciamo alcuni estratti da uno dei primi esempi che abbiamo visto, nella sezione Tipi astratti e tipi parametrici del capitolo 2.

// esempi/cap-2/abstract-types-script.scala

import java.io._

abstract class BulkReader {
  // …
}

class StringBulkReader(val source: String) extends BulkReader {
  // …
}

class FileBulkReader(val source: File) extends BulkReader {
  // …
}

Come in Java, la parola chiave extends indica la classe genitore, in questo caso BulkReader. In Scala, extends viene anche usata quando una classe estende un tratto come suo genitore (e anche quando mescola altri tratti usando la parola chiave with). In più, extends viene usata quando un tratto è figlio di un altro tratto o di una classe. Sì, i tratti possono estendere le classi.

Se non estendete una classe genitore, il genitore predefinito è AnyRef, una classe figlia diretta di Any. (Discuteremo la differenza tra Any e AnyRef quando parleremo della gerarchia dei tipi di Scala nel capitolo 7.)

I costruttori in Scala

Scala distingue tra un costruttore principale e zero o più costruttori ausiliari. In Scala, il costruttore principale è l’intero corpo della classe. Qualsiasi parametro richiesto dal costruttore viene elencato dopo il nome della classe. Abbiamo già visto molti esempi di questo tipo, come la classe ButtonWithCallbacks che abbiamo usato nel capitolo 4.

// esempi/cap-4/ui/button-callbacks.scala

package ui

class ButtonWithCallbacks(val label: String,
    val clickedCallbacks: List[() => Unit]) extends Widget {

  require(clickedCallbacks != null, "La lista di callback non può essere nulla!")

  def this(label: String, clickedCallback: () => Unit) =
    this(label, List(clickedCallback))

  def this(label: String) = {
    this(label, Nil)
    println("Attenzione: il pulsante non ha callback per i clic!")
  }

  def click() = {
    // ... logica per mostrare visivamente il clic sul pulsante ...
    clickedCallbacks.foreach(f => f())
  }
}

La classe ButtonWithCallbacks rappresenta un pulsante in un’interfaccia grafica. Possiede un’etichetta e una lista di funzioni di callback che vengono invocate quando il pulsante viene cliccato. Tutte le funzioni di callback non accettano argomenti e restituiscono Unit. Il metodo click itera attraverso la lista delle callback e le invoca una per una.

ButtonWithCallbacks definisce tre costruttori. Il costruttore principale, che è il corpo dell’intera classe, ha una lista di parametri che contiene una stringa per l’etichetta e una lista di funzioni di callback. Per ogni parametro dichiarato come val, il compilatore genera un campo privato corrispondente a quel parametro (usando un nome interno differente) e un metodo pubblico di lettura che ha lo stesso nome del parametro. “Privato” e “pubblico” hanno qui lo stesso significato che assumono nella maggior parte dei linguaggi orientati agli oggetti. Discuteremo le varie regole di visibilità e le parole chiave che le controllano nella sezione Regole di visibilità più avanti in questo capitolo.

Se un parametro è dichiarato con la parola chiave var, il compilatore genera anche un metodo pubblico di scrittura con il nome del parametro come prefisso seguito da _=. Per esempio, se label fosse dichiarato come var, il metodo di scrittura verrebbe chiamato label_= e accetterebbe un singolo argomento di tipo String.

Ci sono casi in cui non volete che i metodi di accesso vengano generati automaticamente. In altre parole, volete che il campo sia privato. Aggiungete la parola chiave private prima della parola chiave val o var e i metodi di accesso non verranno generati. (Si veda la sezione Regole di visibilità più avanti in questo capitolo per maggiori dettagli.)

I programmatori Java noteranno che Scala non segue la convenzione delle proprietà [JavaBeansSpec] per cui i metodi di lettura e scrittura dei campi cominciano con get e set, rispettivamente, seguiti dal nome del campo con la prima lettera scritta in maiuscolo. Vedremo perché parlando del principio di accesso uniforme nella sezione Quando i metodi di accesso e i campi sono indistinguibili: il principio di accesso uniforme del capitolo 6. Tuttavia, quando ne avete bisogno, potete ottenere i metodi di lettura e scrittura nello stile JavaBeans usando l’annotazione scala.reflect.BeanProperty, come vedremo nella sezione Proprietà JavaBeans del capitolo 14.

Quando viene creata un’istanza di una classe, ogni campo che corrisponde a un parametro nella lista dei parametri verrà inizializzato automaticamente al valore dell’argomento passato. Non è richiesta nessuna logica nel costruttore per inizializzare questi campi, a differenza di quanto accade nella maggior parte degli altri linguaggi orientati agli oggetti.

La prima istruzione nel corpo della classe ButtonWithCallbacks (cioè nel costruttore) è un test per verificare che il costruttore riceva una lista non nulla. (Una lista Nil vuota è ammessa, comunque.) Il test usa la comoda funzione require che viene importata automaticamente nell’ambito corrente (come discuteremo nella sezione L’oggetto Predef del capitolo 7). Se la lista è nulla, require lancerà un’eccezione. La funzione require e la sua compagna assume sono molto utili nella progettazione per contratto (in inglese, design by contract), come discusso nella sezione Una progettazione migliore con la progettazione per contratto del capitolo 13.

Ecco parte di una specifica completa per ButtonWithCallbacks che mostra come agisce l’istruzione require.

// esempi/cap-4/ui/button-callbacks-spec.scala
package ui
import org.specs._

object ButtonWithCallbacksSpec extends Specification {
  "Un pulsante con callback" should {
    // ...
    "non essere costruibile con una lista di callback nulla" in {
      val nullList:List[() => Unit] = null
      val errorMessage =
        "requisito violato: la lista di callback non può essere nulla!"
      (new ButtonWithCallbacks("pulsante1", nullList)) must throwA(
        new IllegalArgumentException(errorMessage))
    }
  }
}

Scala rende difficile persino passare direttamente null come secondo parametro al costruttore, perché non riuscirà a verificarne il tipo durante la compilazione. Tuttavia, potete assegnare null a un valore, come mostrato. Se la clausola must throwA(…) non ci fosse, vedremmo comparire la seguente eccezione.

java.lang.IllegalArgumentException: requisito violato: la lista di callback non può essere nulla!
        at scala.Predef$.require(Predef.scala:112)
        at ui.ButtonWithCallbacks.<init>(button-callbacks.scala:7)
…

ButtonWithCallbacks definisce due costruttori ausiliari per la comodità dell’utente. Il primo costruttore ausiliario accetta un’etichetta e una singola callback, poi invoca il costruttore principale passandogli l’etichetta e una nuova istanza di List che racchiude la singola callback.

Il secondo costruttore ausiliario accetta solo un’etichetta e invoca il costruttore principale con Nil (che rappresenta un’istanza di List vuota). Il costruttore poi stampa un messaggio di avvertimento segnalando che non sono presenti callback, dato che le liste sono immutabili e non c’è modo di sostituire la lista di callback dichiarata come val con una nuova lista.

Per evitare una ricorsione infinita, Scala obbliga ogni costruttore ausiliario a invocare un altro costruttore la cui definizione lo preceda [ScalaSpec2009]. Il costruttore invocato può essere un altro costruttore ausiliario o il costruttore principale, e l’invocazione deve essere la prima istruzione nel corpo del costruttore ausiliario. Dopo questa invocazione possono avere luogo ulteriori operazioni, come la stampa del messaggio di avvertimento nel nostro esempio.

Dato che tutti i costruttori ausiliari finiscono per invocare il costruttore principale, i controlli di logica e le altre inizializzazioni effettuate nel corpo della classe verranno eseguite in maniera consistente per tutte le istanze create.

I vincoli che Scala impone sui costruttori portano alcuni vantaggi.

Eliminazione della duplicazione
Dato che i costruttori ausiliari invocano il costruttore principale, la potenziale duplicazione della logica di costruzione viene in larga misura eliminata.
Riduzione della dimensione del codice
Come mostrato negli esempi, quando uno o più parametri del costruttore principale vengono dichiarati come val o var, Scala genera automaticamente un campo, i metodi di accesso appropriati (a meno che i parametri non siano dichiarati private) e la logica di inizializzazione da eseguire quando le istanze vengono create.

I vincoli imposti da Scala sui costruttori hanno almeno uno svantaggio.

Minore flessibilità
A volte non è proprio conveniente obbligare tutti i costruttori a usare il corpo di un altro costruttore. Tuttavia, troviamo che queste circostanze siano rare. In questi casi, può darsi che la classe abbia troppe responsabilità e che sia preferibile suddividerla in classi più piccole.

Invocare i costruttori della classe genitore

Il costruttore principale di una classe derivata deve invocare uno dei costruttori della classe genitore. Nell’esempio seguente, la classe RadioButtonWithCallbacks derivata da ButtonWithCallbacks invoca il costruttore principale ButtonWithCallbacks. I pulsanti “radio” possono essere attivi (on) o disattivi (off).

// esempi/cap-5/ui/radio-button-callbacks.scala

package ui

/**
 * Pulsante con due stati, attivo (on) o disattivo (off), come
 * il pulsante di selezione del canale sulle radio vecchio stile.
 */
class RadioButtonWithCallbacks(
  var on: Boolean, label: String, clickedCallbacks: List[() => Unit])
      extends ButtonWithCallbacks(label, clickedCallbacks) {

  def this(on: Boolean, label: String, clickedCallback: () => Unit) =
      this(on, label, List(clickedCallback))

  def this(on: Boolean, label: String) = this(on, label, Nil)
}

Il costruttore principale di RadioButtonWithCallbacks accetta tre parametri: uno stato on (true o false), un’etichetta e una lista di callback. Poi, il costruttore passa l’etichetta e la lista di callback alla propria classe genitore ButtonWithCallbacks. Il parametro on è dichiarato come var, quindi è mutabile. on è anche il parametro di costruzione specifico per un pulsante radio, quindi viene memorizzato come attributo di RadioButtonWithCallbacks.

Per consistenza con la propria classe genitore, RadioButtonWithCallbacks dichiara anche due costruttori ausiliari. Notate che essi devono invocare un costruttore che li precede in RadioButtonWithCallbacks, come prima; non possono invocare direttamente un costruttore di ButtonWithCallbacks. Dichiarare tutti questi costruttori in una classe può diventare fastidioso dopo un po’, ma nel capitolo 4 abbiamo esplorato alcune tecniche che possono eliminare la ripetizione.

Sebbene super sia usato per invocare i metodi ridefiniti, come in Java, non può essere usato per invocare un costruttore di una superclasse.

Classi annidate

Come molti linguaggi orientati agli oggetti, Scala vi permette di annidare le dichiarazioni di classe. Supponete di voler dotare tutte le classi Widget di una mappa di proprietà. Queste proprietà potrebbero essere dimensione, colore, visibilità, &c. Potremmo usare una semplice istanza di Map per memorizzare le proprietà, ma supponiamo anche di voler controllare l’accesso alle proprietà e di voler effettuare altre operazioni quando il valore delle proprietà cambia.

Ecco un modo in cui potremmo estendere la nostra classe Widget originale, proveniente da un esempio nella sezione Tratti come mixin del capitolo 4, per aggiungere questa funzione.

// esempi/cap-5/ui/widget.scala

package ui

abstract class Widget {
  class Properties {
    import scala.collection.immutable.HashMap

    private var values: Map[String, Any] = new HashMap

    def size = values.size

    def get(key: String) = values.get(key)

    def update(key: String, value: Any) = {
      // Effettua alcune pre-elaborazioni, per esempio il filtraggio
      values = values.update(key, value)
      // Effettua alcune post-elaborazioni
    }
  }

  val properties = new Properties
}

Abbiamo aggiunto una classe Properties che mantiene un riferimento privato e mutabile a un’istanza immutabile di HashMap. Abbiamo anche aggiunto tre metodi pubblici per ottenere la dimensione (cioè il numero di proprietà definite), per recuperare un singolo elemento della mappa e per aggiornare la mappa con un nuovo elemento, rispettivamente. Potremmo aver bisogno di effettuare ulteriori operazioni nel metodo update, qui indicate con i commenti.

Da questo esempio potete vedere che Scala consente di dichiarare le classi una dentro l’altra, cioè in maniera “annidata”. Una classe annidata ha senso quando volete radunare in una classe alcune funzioni correlate tra loro che però verranno usate solamente dalla classe “esterna”.

Finora abbiamo parlato di come dichiarare e istanziare le classi, e di alcune nozioni basilari di ereditarietà. Nella prossima sezione discuteremo le regole di visibilità all’interno di classi e oggetti.

Regole di visibilità

Per convenienza, in questa sezione useremo la parola “tipo” per riferirci genericamente a classi e tratti, ma non alle dichiarazioni di membro type. Queste ultime si considerano incluse quando usiamo genericamente il termine “membro”, a meno che non sia indicato altrimenti.

La maggior parte dei linguaggi orientati agli oggetti è dotata di costrutti per limitare la visibilità (o l’ambito) delle dichiarazioni dei tipi e dei membri tipo. Questi costrutti supportano la forma di incapsulamento orientata agli oggetti che espone solo le astrazioni pubbliche essenziali di una classe o di un tratto e nasconde alla vista le informazioni di implementazione.

Vorrete rendere pubblicamente visibile tutto quello che gli utenti delle vostre classi e dei vostri oggetti dovrebbero vedere e usare. Ricordatevi che l’astrazione esposta dal tipo è formata dall’insieme dei membri a visibilità pubblica unitamente al nome del tipo stesso.

È opinione diffusa nella progettazione orientata agli oggetti che i campi debbano essere privati o protetti. L’accesso ai campi, se necessario, dovrebbe avvenire attraverso i metodi, ma non tutto dovrebbe essere accessibile per default. Il vantaggio del principio di accesso uniforme (si veda la sezione Quando i metodi di accesso e i campi sono indistinguibili: il principio di accesso uniforme nel capitolo 6) consiste nel poter fornire all’utente la semantica di accesso ai campi pubblici attraverso un metodo o attraverso l’accesso diretto al campo, a seconda di ciò che è più opportuno in una data circostanza.

L’arte della buona progettazione orientata agli oggetti prescrive di definire astrazioni pubbliche minimali, chiare e coese.

Ci sono due categorie di “utenti” di un tipo: i tipi derivati e il codice che lavora con le istanze del tipo. I tipi derivati di solito hanno bisogno di accedere ai membri dei propri tipi genitore più di quanto ne abbiano gli utenti delle istanze.

Le regole di visibilità di Scala sono simili a quelle di Java, ma tendono a venire applicate in maniera più consistente e a essere più flessibili. Per esempio, in Java, se una classe interna possiede un membro privato, la classe che la racchiude può vederlo. In Scala la classe esterna non può vedere questo membro privato, ma Scala fornisce un modo alternativo per dichiararlo visibile alla classe esterna.

Come in Java e C#, le parole chiave che modificano la visibilità, per esempio private e protected, compaiono all’inizio delle dichiarazioni. Le troverete prima delle parole chiave class o trait per i tipi, prima di val o var per i campi e prima di def per i metodi.

Potete anche usare una parola chiave per modificare l’accesso al costruttore principale di una classe. Ponetela dopo il nome del tipo e dopo i parametri del tipo, se ce ne sono, e prima della lista degli argomenti, come in questo esempio: class Restricted[+A] private (name: String) {…}

La tabella 5.1 riepiloga gli ambiti di visibilità.

Tabella 5.1. Ambiti di visibilità.

NomeParola chiaveDescrizione

pubblico

nessuna

I membri e i tipi pubblici sono visibili ovunque, senza alcuna limitazione.

protetto

protected

I membri protetti sono visibili nel tipo che li definisce, nei tipi derivati e nei tipi annidati. I tipi protetti sono visibili solo all’interno dello stesso package e dei sottopackage.

privato

private

I membri privati sono visibili solo nel tipo che li definisce e nei tipi annidati. I tipi privati sono visibili solo all’interno dello stesso package.

protetto ristretto

protected[scope]

La visibilità è limitata a scope, che può essere un package, un tipo, o this (cioè la stessa istanza se applicato ai membri, o il package che li include se applicato ai tipi). Si veda più avanti per i dettagli.

privato ristretto

private[scope]

Sinonimo di visibilità protetta ristretta, tranne in caso di ereditarietà (come discusso più avanti).

Analizziamo queste opzioni di visibilità nel dettaglio. Per semplificare le cose, useremo i campi come esempio di membri. I metodi e le dichiarazioni type si comportano allo stesso modo.

Sfortunatamente, non potete applicare nessun modificatore di visibilità ai package. Di conseguenza, un package è sempre pubblico, anche quando non contiene alcun tipo pubblicamente visibile.

Visibilità pubblica

Qualsiasi dichiarazione priva di una parola chiave di visibilità è “pubblica”, nel senso che è visibile ovunque. La parola chiave public non esiste in Scala. Questo è in contrasto con Java, dove la visibilità predefinita di un elemento è pubblica solo nell’ambito del package che lo contiene (cioè, “privata per il package”). Anche in altri linguaggi orientati agli oggetti, come Ruby, la visibilità predefinita è quella pubblica.

// esempi/cap-5/scoping/public.scala

package scopeA {
  class PublicClass1 {
    val publicField = 1

    class Nested {
      val nestedField = 1
    }

    val nested = new Nested
  }

  class PublicClass2 extends PublicClass1 {
    val field2  = publicField + 1
    val nField2 = new Nested().nestedField
  }
}

package scopeB {
  class PublicClass1B extends scopeA.PublicClass1

  class UsingClass(val publicClass: scopeA.PublicClass1) {
    def method = "UsingClass:" +
      " campo: " + publicClass.publicField +
      " campo annidato: " + publicClass.nested.nestedField
  }
}

Potete compilare questo file con scalac. La compilazione non dovrebbe generare errori.

In questi package e in queste classi è tutto pubblico. Notate che scopeB.UsingClass può accedere a scopeA.PublicClass1 e ai suoi membri, inclusa l’istanza di Nested e i suoi campi pubblici.

Visibilità protetta

La visibilità protetta va a beneficio di chi implementa tipi derivati, che hanno bisogno di un accesso più ampio ai dettagli dei loro tipi genitore. Qualsiasi membro dichiarato con la parola chiave protected è visibile solo nel tipo che lo definisce, incluse altre istanze dello stesso tipo e di tutti i tipi derivati. Quando è applicata a un tipo, protected ne limita la visibilità al package che lo contiene.

In Java, al contrario, i membri protetti sono visibili solo nel package che li contiene. Scala gestisce questo caso con l’accesso ristretto privato e protetto.

// esempi/cap-5/scoping/protected-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class ProtectedClass1(protected val protectedField1: Int) {
    protected val protectedField2 = 1

    def equalFields(other: ProtectedClass1) =
      (protectedField1 == other.protectedField1) &&
      (protectedField1 == other.protectedField1) &&
      (nested == other.nested)

    class Nested {
      protected val nestedField = 1
    }

    protected val nested = new Nested
  }

  class ProtectedClass2 extends ProtectedClass1(1) {
    val field1 = protectedField1
    val field2 = protectedField2
    val nField = new Nested().nestedField // ERRORE
  }

  class ProtectedClass3 {
    val protectedClass1 = new ProtectedClass1(1)
    val protectedField1 = protectedClass1.protectedField1 // ERRORE
    val protectedField2 = protectedClass1.protectedField2 // ERRORE
    val protectedNField = protectedClass1.nested.nestedField // ERRORE
  }

  protected class ProtectedClass4

  class ProtectedClass5 extends ProtectedClass4
  protected class ProtectedClass6 extends ProtectedClass4
}

package scopeB {
  class ProtectedClass4B extends scopeA.ProtectedClass4 // ERRORE
}

Quando compilate questo file con scalac, ottenete l’uscita seguente. (Il nome del file prima dei numeri di riga N: è stato rimosso per adattare meglio il testo allo spazio disponibile.)

16: error: value nestedField cannot be accessed in ProtectedClass2.this.Nested
        val nField = new Nested().nestedField
                                  ^
20: error: value protectedField1 cannot be accessed in scopeA.ProtectedClass1
        val protectedField1 = protectedClass1.protectedField1
                                              ^
21: error: value protectedField2 cannot be accessed in scopeA.ProtectedClass1
        val protectedField2 = protectedClass1.protectedField2
                                              ^
22: error: value nested cannot be accessed in scopeA.ProtectedClass1
        val protectedNField = protectedClass1.nested.nestedField
                                              ^
32: error: class ProtectedClass4 cannot be accessed in package scopeA
    class ProtectedClass4B extends scopeA.ProtectedClass4
                                          ^
5 errors found

I commenti // ERRORE nel codice indicano le righe che generano errori di compilazione.

ProtectedClass2 può accedere ai membri protetti di ProtectedClass1, dato che la estende. Tuttavia, non può accedere al campo nestedField protetto in ProtectedClass1.Nested. In più, ProtectedClass3 usa un’istanza di ProtectedClass1 ma non può accedere ai suoi membri protetti.

Infine, dato che ProtectedClass4 è dichiarata protected, non è visibile nel package scopeB.

Visibilità privata

La visibilità privata nasconde completamente i dettagli di implementazione, anche alle classi derivate. Qualsiasi membro dichiarato con la parola chiave private è visibile solo nel tipo in cui è definito, incluse altre istanze dello stesso tipo. Quando è applicata a un tipo, private ne limita la visibilità al package che lo contiene.

// esempi/cap-5/scoping/private-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class PrivateClass1(private val privateField1: Int) {
    private val privateField2 = 1

    def equalFields(other: PrivateClass1) =
      (privateField1 == other.privateField1) &&
      (privateField2 == other.privateField2) &&
      (nested == other.nested)

    class Nested {
      private val nestedField = 1
    }

    private val nested = new Nested
  }

  class PrivateClass2 extends PrivateClass1(1) {
    val field1 = privateField1 // ERRORE
    val field2 = privateField2 // ERRORE
    val nField = new Nested().nestedField // ERRORE
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1(1)
    val privateField1 = privateClass1.privateField1 // ERRORE
    val privateField2 = privateClass1.privateField2 // ERRORE
    val privateNField = privateClass1.nested.nestedField // ERRORE
  }

  private class PrivateClass4

  class PrivateClass5 extends PrivateClass4 // ERRORE
  protected class PrivateClass6 extends PrivateClass4 // ERRORE
  private class PrivateClass7 extends PrivateClass4
}

package scopeB {
  class PrivateClass4B extends scopeA.PrivateClass4 // ERRORE
}

Il tentativo di compilare questo file produce l’uscita seguente.

14: error: not found: value privateField1
        val field1 = privateField1
                     ^
15: error: not found: value privateField2
        val field2 = privateField2
                     ^
16: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
        val nField = new Nested().nestedField
                                  ^
20: error: value privateField1 cannot be accessed in scopeA.PrivateClass1
        val privateField1 = privateClass1.privateField1
                                          ^
21: error: value privateField2 cannot be accessed in scopeA.PrivateClass1
        val privateField2 = privateClass1.privateField2
                                          ^
22: error: value nested cannot be accessed in scopeA.PrivateClass1
        val privateNField = privateClass1.nested.nestedField
                                          ^
27: error: private class PrivateClass4 escapes its defining scope as part
of type scopeA.PrivateClass4
    class PrivateClass5 extends PrivateClass4
                                ^
28: error: private class PrivateClass4 escapes its defining scope as part
of type scopeA.PrivateClass4
    protected class PrivateClass6 extends PrivateClass4
                                          ^
33: error: class PrivateClass4 cannot be accessed in package scopeA
    class PrivateClass4B extends scopeA.PrivateClass4
                                        ^
9 errors found

Ora PrivateClass2 non può accedere ai membri privati della propria classe genitore PrivateClass1. Essi sono completamente invisibili nella sottoclasse, come indicato dai messaggi di errore. E la sottoclasse non può nemmeno accedere ai campi privati della classe annidata Nested.

Proprio come nel caso dell’accesso protetto, PrivateClass3 usa un’istanza di PrivateClass1 ma non può accedere ai suoi membri privati. Notate tuttavia che il metodo equalFields può accedere ai membri privati dell’istanza other.

La compilazione di PrivateClass5 e PrivateClass6 fallisce, perché altrimenti le dichiarazioni delle due classi permetterebbero a PrivateClass4 di “uscire dal suo ambito di definizione”. Tuttavia la compilazione di PrivateClass7 ha successo, perché questa classe è dichiarata anch’essa come privata. Curiosamente, nel nostro esempio precedente eravamo stati in grado di dichiarare una classe pubblica che estendeva una classe protetta senza generare un simile errore.

Infine, proprio come per le dichiarazioni di tipo protected, i tipi private non possono essere estesi al di fuori del package che li contiene.

Visibilità ristretta privata e protetta

Scala vi consente di calibrare l’ambito di visibilità con le dichiarazioni di visibilità ristretta private e protected. Notate che l’uso di private o protected in una dichiarazione ristretta è intercambiabile perché il loro comportamento è identico, tranne quando i membri coinvolti vengono ereditati.

Sebbene entrambe le dichiarazioni si comportino allo stesso modo nella maggior parte degli scenari, nel codice è più comune vedere usato private[X] che protected[X]. Nella libreria standard di Scala, il rapporto è circa di cinque a uno.

Cominciamo considerando l’unica differenza tra l’ambito privato ristretto e quello protetto ristretto, cioè il comportamento assunto dai membri dichiarati con questa visibilità in caso di ereditarietà.

// esempi/cap-5/scoping/scope-inheritance-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class Class1 {
    private[scopeA]   val scopeA_privateField = 1
    protected[scopeA] val scopeA_protectedField = 2
    private[Class1]   val class1_privateField = 3
    protected[Class1] val class1_protectedField = 4
    private[this]     val this_privateField = 5
    protected[this]   val this_protectedField = 6
  }

  class Class2 extends Class1 {
    val field1 = scopeA_privateField
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERRORE
    val field4 = class1_protectedField
    val field5 = this_privateField       // ERRORE
    val field6 = this_protectedField
  }
}

package scopeB {
  class Class2B extends scopeA.Class1 {
    val field1 = scopeA_privateField     // ERRORE
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERRORE
    val field4 = class1_protectedField
    val field5 = this_privateField       // ERRORE
    val field6 = this_protectedField
  }
}

La compilazione di questo file produrrà l’uscita seguente

17: error: not found: value class1_privateField
    val field3 = class1_privateField
                 ^
19: error: not found: value this_privateField
    val field5 = this_privateField
                 ^
26: error: not found: value scopeA_privateField
    val field1 = scopeA_privateField
                 ^
28: error: not found: value class1_privateField
    val field3 = class1_privateField
                 ^
30: error: not found: value this_privateField
    val field5 = this_privateField
                 ^
5 errors found

I primi due errori in Class2 ci mostrano che, all’interno di uno stesso package, una classe derivata non può fare riferimento a un membro privato ristretto alla classe genitore o a this, ma può fare riferimento a un membro privato ristretto al package (o tipo) che contiene sia Class1 che Class2.

Al contrario, una classe derivata da Class1 al di fuori del package in cui è definita Class1 non può accedere a nessun membro privato ristretto di Class1.

Tuttavia, tutti i membri protetti ristretti sono visibili in entrambe le classi derivate.

Useremo dichiarazioni private ristrette per il resto della discussione e dei nostri esempi, dato che nella libreria Scala l’ambito privato ristretto viene usato un po’ più comunemente di quello protetto ristretto, quando l’ereditarietà non è un fattore rilevante.

Cominciamo con la visibilità più restrittiva, private[this], in quanto coinvolge i membri di un tipo.

// esempi/cap-5/scoping/private-this-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class PrivateClass1(private[this] val privateField1: Int) {
    private[this] val privateField2 = 1

    def equalFields(other: PrivateClass1) =
      (privateField1 == other.privateField1) && // ERRORE
      (privateField2 == other.privateField2) &&
      (nested == other.nested)

    class Nested {
      private[this] val nestedField = 1
    }

    private[this] val nested = new Nested
  }

  class PrivateClass2 extends PrivateClass1(1) {
    val field1 = privateField1 // ERRORE
    val field2 = privateField2 // ERRORE
    val nField = new Nested().nestedField // ERRORE
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1(1)
    val privateField1 = privateClass1.privateField1 // ERRORE
    val privateField2 = privateClass1.privateField2 // ERRORE
    val privateNField = privateClass1.nested.nestedField // ERRORE
  }
}

La compilazione di questo file produce l’uscita seguente.

5: error: value privateField1 is not a member of scopeA.PrivateClass1
            (privateField1 == other.privateField1) &&
                                    ^
14: error: not found: value privateField1
        val field1 = privateField1
                     ^
15: error: not found: value privateField2
        val field2 = privateField2
                     ^
16: error: value nestedField is not a member of PrivateClass2.this.Nested
        val nField = new Nested().nestedField
                                  ^
20: error: value privateField1 is not a member of scopeA.PrivateClass1
        val privateField1 = privateClass1.privateField1
                                          ^
21: error: value privateField2 is not a member of scopeA.PrivateClass1
        val privateField2 = privateClass1.privateField2
                                          ^
22: error: value nested is not a member of scopeA.PrivateClass1
        val privateNField = privateClass1.nested.nestedField
                                          ^
7 errors found

Anche le righe 6-8 non verranno compilate, ma dato che l’espressione di cui fanno parte comincia sulla riga 5, il compilatore si è fermato dopo il primo errore.

I membri private[this] sono visibili solo nella stessa istanza. Un’istanza della stessa classe non può vedere i membri private[this] di un’altra istanza, quindi il metodo equalFields non viene compilato.

Per il resto, la visibilità di questi membri è la stessa di private senza uno specificatore d’ambito.

Quando dichiarate un tipo con private[this], l’uso di this in realtà si lega al package che lo contiene, come mostrato qui di seguito.

// esempi/cap-5/scoping/private-this-pkg-wont-compile.scala
// NON verrà compilato!

package scopeA {
  private[this] class PrivateClass1

  package scopeA2 {
    private[this] class PrivateClass2
  }

  class PrivateClass3 extends PrivateClass1 // ERRORE
  protected class PrivateClass4 extends PrivateClass1 // ERRORE
  private class PrivateClass5 extends PrivateClass1
  private[this] class PrivateClass6 extends PrivateClass1

  private[this] class PrivateClass7 extends scopeA2.PrivateClass2 // ERRORE
}

package scopeB {
  class PrivateClass1B extends scopeA.PrivateClass1 // ERRORE
}

La compilazione di questo file produce l’uscita seguente.

8: error: private class PrivateClass1 escapes its defining scope as part
of type scopeA.PrivateClass1
    class PrivateClass3 extends PrivateClass1
                                ^
9: error: private class PrivateClass1 escapes its defining scope as part
of type scopeA.PrivateClass1
    protected class PrivateClass4 extends PrivateClass1
                                          ^
13: error: type PrivateClass2 is not a member of package scopeA.scopeA2
    private[this] class PrivateClass7 extends scopeA2.PrivateClass2
                                                      ^
17: error: type PrivateClass1 is not a member of package scopeA
    class PrivateClass1B extends scopeA.PrivateClass1
                                        ^
four errors found

Il tentativo di dichiarare una sottoclasse public o protected nello stesso package fallisce. Solo le sottoclassi private e private[this] sono permesse. In più, PrivateClass2 è ristretta a scopeA2, così non potete utilizzarla al di fuori di scopeA2. Similmente, anche il tentativo di dichiarare una classe che estende PrivateClass1 nell’ambito scopeB non correlato fallisce.

Quindi, quando viene applicata ai tipi, private[this] è equivalente alla visibilità package di Java.

Esaminiamo ora la visibilità a livello di tipo, private[T], quando T è un tipo.

// esempi/cap-5/scoping/private-type-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class PrivateClass1(private[PrivateClass1] val privateField1: Int) {
    private[PrivateClass1] val privateField2 = 1

    def equalFields(other: PrivateClass1) =
      (privateField1 == other.privateField1) &&
      (privateField2 == other.privateField2) &&
      (nested  == other.nested)

    class Nested {
      private[Nested] val nestedField = 1
    }

    private[PrivateClass1] val nested = new Nested
    val nestedNested = nested.nestedField // ERRORE
  }

  class PrivateClass2 extends PrivateClass1(1) {
    val field1 = privateField1 // ERRORE
    val field2 = privateField2 // ERRORE
    val nField = new Nested().nestedField // ERRORE
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1(1)
    val privateField1 = privateClass1.privateField1 // ERRORE
    val privateField2 = privateClass1.privateField2 // ERRORE
    val privateNField = privateClass1.nested.nestedField // ERRORE
  }
}

La compilazione di questo file produce l’uscita seguente.

12: error: value nestedField cannot be accessed in PrivateClass1.this.Nested
        val nestedNested = nested.nestedField
                                  ^
15: error: not found: value privateField1
        val field1 = privateField1
                     ^
16: error: not found: value privateField2
        val field2 = privateField2
                     ^
17: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
        val nField = new Nested().nestedField
                                  ^
21: error: value privateField1 cannot be accessed in scopeA.PrivateClass1
        val privateField1 = privateClass1.privateField1
                                          ^
22: error: value privateField2 cannot be accessed in scopeA.PrivateClass1
        val privateField2 = privateClass1.privateField2
                                          ^
23: error: value nested cannot be accessed in scopeA.PrivateClass1
        val privateNField = privateClass1.nested.nestedField
                                          ^
7 errors found

Un membro private[PrivateClass1] è visibile ad altre istanze, quindi il metodo equalFields ora viene compilato. Dunque, private[T] non è altrettanto restrittiva di private[this]. Notate che PrivateClass1 non può vedere Nested.nestedField perché quel campo è dichiarato private[Nested].

Quando i membri di T sono dichiarati private[T], la loro visibilità è uguale a private. La dichiarazione non è equivalente a private[this], che è più restrittiva.

Cosa succede se cambiamo la visibilità di Nested.nestedField in private[PrivateClass1]? Vediamo in che modo private[T] influenza i tipi annidati.

// esempi/cap-5/scoping/private-type-nested-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class PrivateClass1 {
    class Nested {
      private[PrivateClass1] val nestedField = 1
    }

    private[PrivateClass1] val nested = new Nested
    val nestedNested = nested.nestedField
  }

  class PrivateClass2 extends PrivateClass1 {
    val nField = new Nested().nestedField // ERRORE
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1
    val privateNField = privateClass1.nested.nestedField // ERRORE
  }
}

La compilazione di questo file produce l’uscita seguente.

10: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
        def nField = new Nested().nestedField
                                  ^
14: error: value nested cannot be accessed in scopeA.PrivateClass1
        val privateNField = privateClass1.nested.nestedField
                                          ^
two errors found

Qui nestedField è visibile in PrivateClass1, ma è ancora invisibile al di fuori di PrivateClass1. Questo è esattamente il funzionamento di private in Java.

Ora esaminiamo la restrizione usando un nome di package.

// esempi/cap-5/scoping/private-pkg-type-wont-compile.scala
// NON verrà compilato!

package scopeA {
  private[scopeA] class PrivateClass1

  package scopeA2 {
    private [scopeA2] class PrivateClass2
    private [scopeA]  class PrivateClass3
  }

  class PrivateClass4 extends PrivateClass1
  protected class PrivateClass5 extends PrivateClass1
  private class PrivateClass6 extends PrivateClass1
  private[this] class PrivateClass7 extends PrivateClass1

  private[this] class PrivateClass8 extends scopeA2.PrivateClass2 // ERRORE
  private[this] class PrivateClass9 extends scopeA2.PrivateClass3
}

package scopeB {
  class PrivateClass1B extends scopeA.PrivateClass1 // ERRORE
}

La compilazione di questo file produce l’uscita seguente.

14: error: class PrivateClass2 cannot be accessed in package scopeA.scopeA2
    private[this] class PrivateClass8 extends scopeA2.PrivateClass2
                                                      ^
19: error: class PrivateClass1 cannot be accessed in package scopeA
    class PrivateClass1B extends scopeA.PrivateClass1
                                        ^
two errors found

Notate che PrivateClass2 non può essere estesa al di fuori di scopeA2, ma PrivateClass3 può essere estesa in scopeA perché è stata dichiarata private[scopeA].

Infine, diamo un’occhiata agli effetti della restrizione a livello di package sui membri di un tipo.

// esempi/cap-5/scoping/private-pkg-wont-compile.scala
// NON verrà compilato!

package scopeA {
  class PrivateClass1 {
    private[scopeA] val privateField = 1

    class Nested {
      private[scopeA] val nestedField = 1
    }

    private[scopeA] val nested = new Nested
  }

  class PrivateClass2 extends PrivateClass1 {
    val field  = privateField
    val nField = new Nested().nestedField
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1
    val privateField  = privateClass1.privateField
    val privateNField = privateClass1.nested.nestedField
  }

  package scopeA2 {
    class PrivateClass4 {
      private[scopeA2] val field1 = 1
      private[scopeA]  val field2 = 2
    }
  }

  class PrivateClass5 {
    val privateClass4 = new scopeA2.PrivateClass4
    val field1 = privateClass4.field1 // ERRORE
    val field2 = privateClass4.field2
  }
}

package scopeB {
  class PrivateClass1B extends scopeA.PrivateClass1 {
    val field1 = privateField // ERRORE
    val privateClass1 = new scopeA.PrivateClass1
    val field2 = privateClass1.privateField // ERRORE
  }
}

La compilazione di questo file produce l’uscita seguente.

28: error: value field1 cannot be accessed in scopeA.scopeA2.PrivateClass4
        val field1 = privateClass4.field1
                                   ^
35: error: not found: value privateField
        val field1 = privateField
                     ^
37: error: value privateField cannot be accessed in scopeA.PrivateClass1
        val field2 = privateClass1.privateField
                                   ^
three errors found

Gli unici errori avvengono quando tentiamo di accedere ai membri ristretti a scopeA dal package scopeB non correlato e quando tentiamo di accedere a un membro di un package annidato scopeA2 che è ristretto a quel package.

Quando un tipo o un membro è dichiarato private[P], dove P è il package che lo contiene, allora la sua visibilità è equivalente alla visibilità package di Java.

Considerazioni conclusive sulla visibilità

Le dichiarazioni di visibilità in Scala sono molto flessibili e si comportano in maniera coerente. Offrono un controllo a grana fine sulla visibilità in tutti gli ambiti possibili, a partire dal livello di singola istanza (private[this]) fino al livello di package (private[P], per un package P). In questo modo, per esempio, facilitano la creazione di “componenti” che possono esporre alcuni tipi all’esterno del package radice del componente, pur nascondendo i tipi di implementazione e i membri dei tipi all’interno degli altri package del componente.

Infine, abbiamo osservato una potenziale “sorpresa” tenuta in serbo dai membri nascosti di un tratto.

Fate attenzione quando scegliete i nomi dei membri di un tratto. Se due tratti hanno un membro con lo stesso nome e i tratti vengono usati nella stessa istanza, i nomi collideranno tra loro anche se entrambi i membri sono privati.

Fortunatamente, il compilatore è in grado di accorgersi di questo problema.

Riepilogo, e poi?

Abbiamo introdotto i concetti di base del modello a oggetti di Scala, compresi costruttori, ereditarietà, classi annidate e regole di visibilità.

Nel prossimo capitolo esploreremo le caratteristiche OOP più avanzate di Scala, comprese ridefinizione, oggetti associati, classi case e regole per l’uguaglianza tra oggetti.

© 2008–9 O’Reilly Media
© 2009–10 Giulio Piancastelli per la traduzione italiana