2015-04-16 14 views
6

Sto vedendo alcune stranezze di inizializzazione quando si mescolano val e def nel mio tratto. La situazione può essere riassunta con il seguente esempio.Ottenere un valore null con un valore val di valore astratto in un tratto

Ho un tratto che fornisce un campo astratto, chiamiamolo fruit, che dovrebbe essere implementato nelle classi child. Utilizza anche quel campo in val:

scala> class FruitTreeDescriptor(fruit: String) { 
    | def describe = s"This tree has loads of ${fruit}s" 
    | } 
defined class FruitTreeDescriptor 

scala> trait FruitTree { 
    | def fruit: String 
    | val descriptor = new FruitTreeDescriptor(fruit) 
    | } 
defined trait FruitTree 

Quando sovrascriviamo fruit con un def, le cose funzionano come previsto:

scala> object AppleTree extends FruitTree { 
    | def fruit = "apple" 
    | } 
defined object AppleTree 

scala> AppleTree.descriptor.describe 
res1: String = This tree has loads of apples 

Tuttavia, se sovrascrivo fruit utilizzando un val ...

scala> object BananaTree extends FruitTree { 
    | val fruit = "banana" 
    | } 
defined object BananaTree 

scala> BananaTree.descriptor.describe 
res2: String = This tree has loads of nulls 

Cosa sta succedendo qui?

risposta

3

In termini semplici, al punto che si sta chiamando:

val descriptor = new FruitTreeDescriptor(fruit) 

il costruttore per BananaTree non è stata data la possibilità di correre ancora.Ciò significa che il valore di fruit è ancora null, anche se è un val.

Questa è una subcase del noto coincidenza di inizializzazione non dichiarativa di vals, che può essere illustrato con un esempio più semplice:

class A {       
    val x = a 
    val a = "String" 
} 

scala> new A().x 
res1: String = null 

(Sebbene fortuna, in questo caso particolare, il compilatore rileverà qualcosa in corso e presenterà un avviso.)

Per evitare il problema, dichiarare fruit come lazy val, che imporrà la valutazione.

2

Il problema è l'ordine di inizializzazione. val fruit = ... viene inizializzato dopo val descriptor = ..., quindi nel momento in cui descriptor viene inizializzato, fruit è ancora null. È possibile risolvere questo rendendo fruit a lazy val, perché quindi verrà inizializzato al primo accesso.

1

Il campo descriptor si inizializza prima del campo fruit come tratto di intializzazione precedente alla classe, che lo estende. null è il valore di un campo prima dell'inizializzazione: ecco perché lo ottieni. Nel caso def è solo una chiamata al metodo invece di accedere ad alcuni campi, quindi tutto va bene (poiché il codice del metodo può essere chiamato più volte - nessuna inizializzazione qui). Vedi, http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

Perché def è così diverso? Questo perché def può essere chiamato più volte, ma val - solo una volta (quindi la sua prima e unica chiamata è in realtà l'inizializzazione del fileld).

Soluzione tipica a tale problema - utilizzando lazy val invece, verrà inizializzato quando ne hai veramente bisogno. Un'altra soluzione è early intializers.

Un altro, più semplice esempio di quello che sta succedendo:

scala> class A {val a = b; val b = 5} 
<console>:7: warning: Reference to uninitialized value b 
     class A {val a = b; val b = 5} 
         ^
defined class A 

scala> (new A).a 
res2: Int = 0 //null 

Parlando più in generale, in teoria scala potrebbe analizzare il grafico delle dipendenze tra i campi (quale campo ha bisogno di altro campo) e avviare l'inizializzazione dai nodi finali. Ma in pratica ogni modulo è compilato separatamente e il compilatore potrebbe anche non sapere quelle dipendenze (potrebbe essere anche Java, che chiama Scala, che chiama Java), quindi è solo fare l'inizializzazione sequenziale.

Così, a causa di ciò, non poteva nemmeno rilevare semplici loop:

scala> class A {val a: Int = b; val b: Int = a} 
<console>:7: warning: Reference to uninitialized value b 
     class A {val a: Int = b; val b: Int = a} 
          ^
defined class A 

scala> (new A).a 
res4: Int = 0 

scala> class A {lazy val a: Int = b; lazy val b: Int = a} 
defined class A 

scala> (new A).a 
java.lang.StackOverflowError 

In realtà, tale ciclo (all'interno di un modulo) può essere teoricamente individuato in costruzione separata, ma non aiuterà molto come è abbastanza ovvio.

Problemi correlati