¿Cuándo usar val o def en los rasgos de Scala?

90

Estaba revisando las diapositivas de Scala efectivas y en la diapositiva 10 se menciona que nunca se debe usar valen una traitpara miembros abstractos y usar defen su lugar. La diapositiva no menciona en detalle por qué usar abstracto valen a traites un anti-patrón. Agradecería que alguien pudiera explicar las mejores prácticas sobre el uso de val vs def en un rasgo para métodos abstractos

Mansur Ashraf
fuente

Respuestas:

130

A defpuede implementarse mediante a def, a val, a lazy valo an object. Entonces es la forma más abstracta de definir un miembro. Dado que los rasgos suelen ser interfaces abstractas, decir que quieres una vales decir cómo debería funcionar la implementación. Si solicita a val, una clase de implementación no puede usar a def.

A valsolo es necesario si necesita un identificador estable, por ejemplo, para un tipo dependiente de la ruta. Eso es algo que normalmente no necesitas.


Comparar:

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
  }
}

Si tuvieras

trait Foo { val bar: Int }

no podrías definir F1o F3.


Ok, y para confundirte y responder @ om-nom-nom, el uso de vals abstractos puede causar problemas de inicialización:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Este es un problema feo que, en mi opinión personal, debería desaparecer en futuras versiones de Scala al arreglarlo en el compilador, pero sí, actualmente esta es también una razón por la que uno no debería usar vals abstractos .

Editar (enero de 2016): se le permite anular una valdeclaración abstracta con una lazy valimplementación, por lo que también evitaría el error de inicialización.

0__
fuente
8
palabras sobre orden de inicialización complicado y nulos sorprendentes?
om-nom-nom
Sí ... ni siquiera iría allí. Es cierto que estos también son argumentos en contra de val, pero creo que la motivación básica debería ser simplemente ocultar la implementación.
0__
2
Esto puede haber cambiado en una versión reciente de Scala (2.11.4 a partir de este comentario), pero puede anular a valcon un lazy val. Su afirmación de que no podría crear F3si barfuera un valno es correcta. Dicho esto, los miembros abstractos en rasgos siempre deben ser def's
mplis
El ejemplo Foo / Fail funciona como se esperaba si reemplaza val schoko = bar + barcon lazy val schoko = bar + bar. Esa es una forma de tener cierto control sobre el orden de inicialización. Además, usar en lazy vallugar de defen la clase derivada evita el recálculo.
Adrian
2
Si cambia val bar: Inta def bar: Int Fail.schokosigue siendo cero.
Jasper-M
8

Prefiero no usar valen rasgos porque la declaración de val tiene un orden de inicialización poco claro y no intuitivo. Puede agregar un rasgo a la jerarquía que ya funciona y rompería todas las cosas que funcionaron antes, vea mi tema: por qué usar val simple en clases no finales

Debe tener en cuenta todo lo relacionado con el uso de estas declaraciones val que eventualmente lo conducen a un error.


Actualizar con un ejemplo más complicado

Pero hay momentos en los que no puede evitar el uso val. Como @ 0__ mencionó, a veces necesitas un identificador estable y defno lo es.

Daría un ejemplo para mostrar de qué estaba hablando:

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
}

Este código produce el error:

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

Si se toma un minuto para pensar, comprenderá que el compilador tiene una razón para quejarse. En el Access2.accesscaso, no podría derivar el tipo de retorno de ninguna manera. def holdersignifica que podría implementarse de manera amplia. Podría devolver diferentes titulares para cada llamada y los titulares incorporarían diferentes Innertipos. Pero la máquina virtual Java espera que se devuelva el mismo tipo.

ayvango
fuente
3
El orden de inicialización no debería importar, sino que obtenemos NPE sorprendentes durante el tiempo de ejecución, en comparación con el anti-patrón.
Jonathan Neufeld
scala tiene una sintaxis declarativa que oculta la naturaleza imperativa. A veces, ese imperativo funciona en contra de la intuición
ayvango
-4

Usar siempre def parece un poco incómodo ya que algo como esto no funcionará:

trait Entity { def id:Int}

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

Obtendrá el siguiente error:

error: value id_= is not a member of Entity
Dimitry
fuente
2
No relevante. También tiene un error si usa val en lugar de def (error: reasignación a val), y eso es perfectamente lógico.
volia17
No si lo usa var. El caso es que, si son campos, deberían designarse como tales. Creo que tener todo como defes miope.
Dimitry
@Dimitry, claro, el uso te varpermite romper la encapsulación. Pero se prefiere usar a def(o a val) sobre una variable global. Creo que lo que estás buscando es algo case class ConcreteEntity(override val id: Int) extends Entityasí como para poder crearlo desde. def create(e: Entity) = ConcreteEntity(1)Esto es más seguro que romper la encapsulación y permitir que cualquier clase cambie de Entidad.
Jono