Quand utiliser val ou def dans les traits Scala?

90

J'étais en train de parcourir les diapositives scala efficaces et il est mentionné sur la diapositive 10 de ne jamais utiliser valdans un traitpour les membres abstraits et de l'utiliser à la defplace. La diapositive ne mentionne pas en détail pourquoi l'utilisation de résumé valdans a traitest un anti-motif. J'apprécierais que quelqu'un puisse expliquer les meilleures pratiques autour de l'utilisation de val vs def dans un trait pour les méthodes abstraites

Mansur Ashraf
la source

Réponses:

130

A defpeut être implémenté soit par a def, a val, a lazy valou an object. C'est donc la forme la plus abstraite de définition d'un membre. Puisque les traits sont généralement des interfaces abstraites, dire que vous voulez un, valc'est dire comment l'implémentation doit fonctionner. Si vous demandez a val, une classe d'implémentation ne peut pas utiliser a def.

A valn'est nécessaire que si vous avez besoin d'un identifiant stable, par exemple pour un type dépendant du chemin. C'est quelque chose dont vous n'avez généralement pas besoin.


Comparer:

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 tu avais

trait Foo { val bar: Int }

vous ne seriez pas en mesure de définir F1ou F3.


Ok, et pour vous embrouiller et répondre @ om-nom-nom - l'utilisation de vals abstraits peut causer des problèmes d'initialisation:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

C'est un problème horrible qui, à mon avis personnel, devrait disparaître dans les futures versions de Scala en le corrigeant dans le compilateur, mais oui, actuellement c'est aussi une raison pour laquelle on ne devrait pas utiliser de résumé val.

Edit (Jan 2016): Vous êtes autorisé à remplacer une valdéclaration abstraite avec une lazy valimplémentation, ce qui empêcherait également l'échec de l'initialisation.

0__
la source
8
des mots sur l'ordre d'initialisation délicat et les nulls surprenants?
om-nom-nom
Ouais ... je n'irais même pas là-bas. Il est vrai que ce sont aussi des arguments contre val, mais je pense que la motivation de base devrait simplement être de cacher la mise en œuvre.
0__
2
Cela peut avoir changé dans une version récente de Scala (2.11.4 à partir de ce commentaire), mais vous pouvez remplacer a valpar un lazy val. Votre affirmation selon laquelle vous ne seriez pas en mesure de créer F3si barc'était un valn'est pas correcte. Cela dit, les membres abstraits dans les traits devraient toujours être def's
mplis
L'exemple Foo / Fail fonctionne comme prévu si vous remplacez val schoko = bar + barpar lazy val schoko = bar + bar. C'est une façon d'avoir un certain contrôle sur l'ordre d'initialisation. De plus, utiliser lazy valau lieu de defdans la classe dérivée évite le recalcul.
Adrian
2
Si vous passez val bar: Intà def bar: Int Fail.schokoest toujours zéro.
Jasper-M
8

Je préfère ne pas l'utiliser valdans les traits car la déclaration val a un ordre d'initialisation peu clair et non intuitif. Vous pouvez ajouter un trait à la hiérarchie déjà opérationnelle et cela casserait toutes les choses qui fonctionnaient auparavant, voir mon sujet: pourquoi utiliser un val simple dans des classes non finales

Vous devez garder à l'esprit tout ce qui concerne l'utilisation de ces déclarations val qui finissent par vous conduire à une erreur.


Mettre à jour avec un exemple plus compliqué

Mais il y a des moments où vous ne pouvez pas éviter d'utiliser val. Comme @ 0__ l'avait mentionné, vous avez parfois besoin d'un identifiant stable et defne l'est pas.

Je donnerais un exemple pour montrer de quoi il parlait:

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
}

Ce code produit l'erreur:

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

Si vous prenez une minute pour penser, vous comprendriez que le compilateur a une raison de se plaindre. Dans le Access2.accesscas où il ne pourrait en aucun cas dériver le type de retour. def holdersignifie qu'il pourrait être mis en œuvre de manière large. Il pourrait renvoyer différents titulaires pour chaque appel et que les titulaires incorporeraient différents Innertypes. Mais la machine virtuelle Java s'attend à ce que le même type soit renvoyé.

ayvango
la source
3
L'ordre d'initialisation ne devrait pas avoir d'importance, mais au lieu de cela, nous obtenons des NPE surprenants pendant l'exécution, vis-à-vis de l'anti-pattern.
Jonathan Neufeld
scala a une syntaxe déclarative qui cache la nature impérative derrière. Parfois, cette impérativité fonctionne contre-intuitive
ayvango
-4

Toujours utiliser def semble un peu gênant car quelque chose comme ça ne fonctionnera pas:

trait Entity { def id:Int}

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

Vous obtiendrez l'erreur suivante:

error: value id_= is not a member of Entity
Dimitry
la source
2
Pas pertinent. Vous avez également une erreur si vous utilisez val au lieu de def (erreur: réaffectation à val), et c'est parfaitement logique.
volia17
Pas si vous utilisez var. Le fait est que si ce sont des champs, ils doivent être désignés comme tels. Je pense juste avoir tout comme à defcourte vue.
Dimitry
@Dimitry, bien sûr, en utilisant varvous permet de casser l'encapsulation. Mais l'utilisation de a def(ou a val) est préférable à une variable globale. Je pense que ce que vous recherchez est quelque chose comme case class ConcreteEntity(override val id: Int) extends Entitypour que vous puissiez le créer à partir de def create(e: Entity) = ConcreteEntity(1)Ceci est plus sûr que de casser l'encapsulation et de permettre à n'importe quelle classe de changer d'entité.
Jono