Voi siete qui: Inizio ‣ Programmare in Scala ‣
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.
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 unobject
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.
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.)
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
eset
, 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’annotazionescala.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.
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.
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.
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.
☞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à.
Nome | Parola chiave | Descrizione |
---|---|---|
pubblico | nessuna | I membri e i tipi pubblici sono visibili ovunque, senza alcuna limitazione. |
protetto |
| 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 |
| 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 |
| La visibilità è limitata a |
privato ristretto |
| 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.
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.
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
.
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.
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]
cheprotected[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 dichiaratiprivate[T]
, la loro visibilità è uguale aprivate
. La dichiarazione non è equivalente aprivate[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]
, doveP
è il package che lo contiene, allora la sua visibilità è equivalente alla visibilità package di Java.
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.
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