Quando usare val o def nei tratti di Scala?


90

Stavo esaminando le diapositive in scala efficaci e nella diapositiva 10 si menziona di non usare mai valin una traitper i membri astratti e di usare definvece. La diapositiva non menziona in dettaglio perché l'uso di abstract valin a traitè un anti-pattern. Apprezzerei se qualcuno potesse spiegare le migliori pratiche sull'uso di val vs def in un tratto per metodi astratti

Risposte:


130

A defpuò essere implementato da a def, a val, a lazy valo an object. Quindi è la forma più astratta per definire un membro. Poiché i tratti sono solitamente interfacce astratte, dire che si desidera una valè dire come dovrebbe fare l'implementazione. Se chiedi a val, una classe di implementazione non può utilizzare a def.

A valè necessario solo se è necessario un identificatore stabile, ad esempio per un tipo dipendente dal percorso. È qualcosa di cui di solito non hai bisogno.


Confrontare:

trait Foo { def bar: Int }

object F1 extends Foo { def bar = util.Random.nextInt(33) } // ok

class F2(val bar: Int) extends Foo // ok

object F3 extends Foo {
  lazy val bar = { // ok
    Thread.sleep(5000)  // really heavy number crunching
    42
  }
}

Se tu avessi

trait Foo { val bar: Int }

non saresti in grado di definire F1o F3.


Ok, e per confonderti e rispondere @ om-nom-nom, l'uso di vals astratte può causare problemi di inizializzazione:

trait Foo { 
  val bar: Int 
  val schoko = bar + bar
}

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Questo è un brutto problema che a mio parere personale dovrebbe scomparire nelle future versioni di Scala riparandolo nel compilatore, ma sì, attualmente questo è anche un motivo per cui non si dovrebbero usare abstract vals.

Modifica (gennaio 2016): è consentito sostituire una valdichiarazione astratta con lazy valun'implementazione, in modo da evitare anche l'errore di inizializzazione.


8
parole sull'ordine di inizializzazione complicato e sui null sorprendenti?
om-nom-nom

Sì ... non ci andrei nemmeno. È vero che anche questi sono argomenti contro val, ma penso che la motivazione di base dovrebbe essere solo quella di nascondere l'implementazione.
0__

2
Questo potrebbe essere cambiato in una recente versione di Scala (2.11.4 a partire da questo commento), ma puoi sovrascrivere a valcon a lazy val. La tua affermazione che non saresti in grado di creare F3se barfosse una valnon è corretta. Detto questo, i membri astratti nei tratti dovrebbero sempre essere def's
mplis

L'esempio Foo / Fail funziona come previsto se si sostituisce val schoko = bar + barcon lazy val schoko = bar + bar. Questo è un modo per avere un certo controllo sull'ordine di inizializzazione. Inoltre, l'utilizzo al lazy valposto di defnella classe derivata evita il ricalcolo.
Adrian

2
Se cambi val bar: Intin def bar: Int Fail.schokoè ancora zero.
Jasper-M

8

Preferisco non usarlo valnei tratti perché la dichiarazione val ha un ordine di inizializzazione poco chiaro e non intuitivo. Puoi aggiungere un tratto alla gerarchia già funzionante e spezzerebbe tutte le cose che funzionavano prima, vedi il mio argomento: perché usare la semplice val in classi non finali

Dovresti tenere a mente tutte le cose sull'uso di queste dichiarazioni val che alla fine ti portano a un errore.


Aggiorna con un esempio più complicato

Ma ci sono momenti in cui non puoi evitare di usare val. Come menzionato da @ 0__, a volte è necessario un identificatore stabile edef non è uno.

Fornirei un esempio per mostrare di cosa stava parlando:

trait Holder {
  type Inner
  val init : Inner
}
class Access(val holder : Holder) {
  val access : holder.Inner =
    holder.init
}
trait Access2 {
  def holder : Holder
  def access : holder.Inner =
    holder.init
}

Questo codice produce l'errore:

 StableIdentifier.scala:14: error: stable identifier required, but Access2.this.holder found.
    def access : holder.Inner =

Se ti prendi un minuto per pensare che capiresti che il compilatore ha un motivo per lamentarsi. Nel Access2.accesscaso in cui non potrebbe derivare il tipo di ritorno in alcun modo. def holdersignifica che potrebbe essere attuato in modo ampio. Potrebbe restituire diversi titolari per ogni chiamata e tali titolari incorporerebbero Innertipi diversi . Ma la macchina virtuale Java si aspetta che venga restituito lo stesso tipo.


3
L'ordine di inizializzazione non dovrebbe avere importanza, ma invece otteniamo sorprendenti NPE durante il runtime, di fronte all'anti-pattern.
Jonathan Neufeld

scala ha una sintassi dichiarativa che nasconde la natura imperativa dietro. A volte
quell'imperatività

-4

Usare sempre def sembra un po 'imbarazzante poiché qualcosa del genere non funzionerà:

trait Entity { def id:Int}

object Table { 
  def create(e:Entity) = {e.id = 1 }  
}

Otterrai il seguente errore:

error: value id_= is not a member of Entity

2
Non rilevante. Hai anche un errore se usi val invece di def (errore: riassegnazione a val), e questo è perfettamente logico.
volia17

Non se usi var. Il punto è che se sono campi dovrebbero essere designati come tali. Penso solo che avere tutto come defmiope.
Dimitry

@ Dimitry, certo, usando ti varpermette di rompere l'incapsulamento. Ma l'uso di a def(o a val) è preferibile rispetto a una variabile globale. Penso che quello che stai cercando sia qualcosa di simile in case class ConcreteEntity(override val id: Int) extends Entitymodo da poterlo creare da def create(e: Entity) = ConcreteEntity(1)Questo è più sicuro che rompere l'incapsulamento e consentire a qualsiasi classe di cambiare Entità.
Jono
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.