Quando usar val ou def em traços Scala?

90

Eu estava examinando os slides de scala eficazes e ele menciona no slide 10 nunca usar valem um traitpara membros abstratos e usar em seu deflugar. O slide não menciona em detalhes por que usar o resumo valem a traité um antipadrão. Eu agradeceria se alguém pudesse explicar as melhores práticas em torno do uso de val vs def em uma característica para métodos abstratos

Mansur Ashraf
fonte

Respostas:

130

A defpode ser implementado por um def, um val, um lazy valou um object. Portanto, é a forma mais abstrata de definir um membro. Como as características são geralmente interfaces abstratas, dizer que você deseja valé dizer como a implementação deve se comportar. Se você solicitar a val, uma classe de implementação não poderá usar a def.

A valé necessário apenas se você precisar de um identificador estável, por exemplo, para um tipo dependente do caminho. Isso é algo que você geralmente não precisa.


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

Se você tinha

trait Foo { val bar: Int }

você não seria capaz de definir F1ou F3.


Ok, e para confundi-lo e responder @ om-nom-nom - usar abstract valpode causar problemas de inicialização:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Este é um problema feio que, em minha opinião pessoal, deveria desaparecer nas futuras versões do Scala corrigindo-o no compilador, mas sim, atualmente esta também é uma razão pela qual não se deve usar vals abstratos .

Editar (janeiro de 2016): Você tem permissão para substituir uma valdeclaração abstrata por uma lazy valimplementação, de modo que também evite a falha de inicialização.

0__
fonte
8
palavras sobre ordem de inicialização complicada e nulos surpreendentes?
om-nom-nom
Sim ... Eu nem iria lá. É verdade que esses também são argumentos contra val, mas acho que a motivação básica deveria ser apenas ocultar a implementação.
0__ de
2
Isso pode ter mudado em uma versão recente do Scala (2.11.4 neste comentário), mas você pode substituir a valpor a lazy val. Sua afirmação de que você não seria capaz de criar F3se barfosse um valnão está correta. Dito isso, membros abstratos em traços devem sempre ser def's
mplis
O exemplo Foo / Fail funciona conforme o esperado se você substituir val schoko = bar + barpor lazy val schoko = bar + bar. Essa é uma maneira de ter algum controle sobre a ordem de inicialização. Além disso, usar em lazy valvez de defna classe derivada evita a recomputação.
Adrian
2
Se você mudar val bar: Intpara def bar: Int Fail.schokoainda é zero.
Jasper-M de
8

Prefiro não usar valem traits porque a declaração val tem uma ordem de inicialização confusa e não intuitiva. Você pode adicionar um traço à hierarquia que já está funcionando e isso quebraria todas as coisas que funcionavam antes, veja meu tópico: por que usar val simples em classes não finais

Você deve manter todas as coisas sobre o uso dessas declarações val em mente, o que eventualmente leva a um erro.


Atualize com um exemplo mais complicado

Mas há momentos em que você não consegue evitar o uso val. Como @ 0__ mencionou, às vezes você precisa de um identificador estável e defnão é.

Eu daria um exemplo para mostrar o que ele estava falando:

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 produz o erro:

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

Se você parar um minuto para pensar, entenderá que o compilador tem um motivo para reclamar. No Access2.accesscaso, ele não poderia derivar o tipo de retorno por nenhum meio. def holdersignifica que pode ser implementado de forma ampla. Ele poderia retornar portadores diferentes para cada chamada e esses portadores incorporariam Innertipos diferentes . Mas a máquina virtual Java espera que o mesmo tipo seja retornado.

Ayvango
fonte
3
A ordem de inicialização não deve importar, mas em vez disso, obtemos NPEs surpreendentes durante o tempo de execução, vis-à-vis o anti-padrão.
Jonathan Neufeld
scala tem sintaxe declarativa que esconde a natureza imperativa por trás. Às vezes, essa imperatividade funciona contra a intuição
ayvango
-4

Sempre usar def parece um pouco estranho, já que algo assim não funcionará:

trait Entity { def id:Int}

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

Você obterá o seguinte erro:

error: value id_= is not a member of Entity
Dimitry
fonte
2
Não é relevante. Você também terá um erro se usar val em vez de def (error: reassignment to val), e isso é perfeitamente lógico.
volia17
Não se você usar var. A questão é que, se são campos, devem ser designados como tal. Só acho que ter tudo como defé míope.
Dimitry
@Dimitry, claro, usando varpermite que você interrompa o encapsulamento. Mas usar a def(ou a val) é preferível a uma variável global. Eu acho que o que você está procurando é algo como case class ConcreteEntity(override val id: Int) extends Entityque você possa criá-lo a partir de def create(e: Entity) = ConcreteEntity(1)Isso é mais seguro do que quebrar o encapsulamento e permitir que qualquer classe altere a Entidade.
Jono