Wann sollte val oder def in Scala-Merkmalen verwendet werden?

88

Ich habe die effektiven Scala-Folien durchgesehen und auf Folie 10 wird erwähnt, dass sie niemals valin einem traitfür abstrakte Mitglieder verwendet werden sollen und defstattdessen verwendet werden sollen. In der Folie wird nicht im Detail erwähnt, warum die Verwendung von Abstract valin a traitein Anti-Pattern ist. Ich würde es begrüßen, wenn jemand Best Practices für die Verwendung von val vs def in einem Merkmal für abstrakte Methoden erklären könnte

Mansur Ashraf
quelle

Antworten:

128

A defkann entweder durch a def, a val, a lazy valoder an implementiert werden object. Es ist also die abstrakteste Form, ein Mitglied zu definieren. Da Merkmale normalerweise abstrakte Schnittstellen sind, bedeutet die Aussage, dass Sie eine möchten val, wie die Implementierung funktionieren soll. Wenn Sie nach a fragen val, kann eine implementierende Klasse a nicht verwenden def.

A valwird nur benötigt, wenn Sie eine stabile Kennung benötigen, z. B. für einen pfadabhängigen Typ. Das brauchen Sie normalerweise nicht.


Vergleichen Sie:

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

Wenn du hättest

trait Foo { val bar: Int }

Sie würden nicht definieren können F1oder F3.


Ok, und um Sie zu verwirren und @ om-nom-nom zu beantworten - die Verwendung von Abstracts valkann zu Initialisierungsproblemen führen:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Dies ist ein hässliches Problem, das meiner persönlichen Meinung nach in zukünftigen Scala-Versionen behoben werden sollte, indem es im Compiler behoben wird. Ja, derzeit ist dies auch ein Grund, warum man keine abstrakten vals verwenden sollte.

Bearbeiten (Januar 2016): Sie dürfen eine abstrakte valDeklaration mit einer lazy valImplementierung überschreiben , um auch den Initialisierungsfehler zu verhindern.

0__
quelle
8
Worte über knifflige Initialisierungsreihenfolge und überraschende Nullen?
Om-Nom-Nom
Ja ... ich würde nicht einmal dorthin gehen. Das sind zwar auch Argumente gegen val, aber ich denke, die grundlegende Motivation sollte nur darin bestehen, die Implementierung zu verbergen.
0__
2
Dies hat sich möglicherweise in einer neueren Scala-Version (2.11.4 ab diesem Kommentar) geändert, aber Sie können a valmit a überschreiben lazy val. Ihre Behauptung, dass Sie nicht erstellen könnten, F3wenn bara wäre, valist nicht richtig. Das heißt, abstrakte Mitglieder in Merkmalen sollten immer def's
mplis
Die Foo / Fail Beispiel funktioniert wie erwartet , wenn Sie ersetzen val schoko = bar + barmit lazy val schoko = bar + bar. Dies ist eine Möglichkeit, die Initialisierungsreihenfolge zu steuern. Durch die Verwendung von lazy valanstelle von defin der abgeleiteten Klasse wird außerdem eine Neuberechnung vermieden.
Adrian
2
Wenn Sie zu wechseln val bar: Int, def bar: Int Fail.schokoist immer noch Null.
Jasper-M
8

Ich bevorzuge die Verwendung valin Merkmalen, da die Val-Deklaration eine unklare und nicht intuitive Reihenfolge der Initialisierung aufweist. Sie können der bereits funktionierenden Hierarchie ein Merkmal hinzufügen, das alle zuvor funktionierenden Dinge zerstört. Weitere Informationen finden Sie in meinem Thema: Warum wird in nicht endgültigen Klassen ein einfacher Wert verwendet?

Sie sollten alle Aspekte der Verwendung dieser Wertdeklarationen berücksichtigen, die Sie letztendlich zu einem Fehler führen.


Update mit komplizierterem Beispiel

Aber es gibt Zeiten, in denen Sie die Verwendung nicht vermeiden konnten val. Wie @ 0__ bereits erwähnt hatte, benötigen Sie manchmal eine stabile Kennung und defsind keine.

Ich würde ein Beispiel geben, um zu zeigen, wovon er sprach:

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
}

Dieser Code erzeugt den Fehler:

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

Wenn Sie sich eine Minute Zeit nehmen, um zu glauben, Sie würden verstehen, dass der Compiler einen Grund hat, sich zu beschweren. In diesem Access2.accessFall konnte der Rückgabetyp auf keinen Fall abgeleitet werden. def holderbedeutet, dass es auf breite Weise umgesetzt werden könnte. Es könnten für jeden Anruf unterschiedliche Inhaber zurückgegeben werden, und diese Inhaber würden unterschiedliche InnerTypen einbeziehen . Die virtuelle Java-Maschine erwartet jedoch, dass derselbe Typ zurückgegeben wird.

Ayvango
quelle
3
Die Reihenfolge der Initialisierung sollte keine Rolle spielen, stattdessen erhalten wir zur Laufzeit überraschende NPEs gegenüber Anti-Patterns.
Jonathan Neufeld
scala hat eine deklarative Syntax, die die imperative Natur dahinter verbirgt. Manchmal funktioniert diese Imperativität kontraintuitiv
Ayvango
-4

Die Verwendung von def scheint immer etwas umständlich zu sein, da so etwas nicht funktioniert:

trait Entity { def id:Int}

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

Sie erhalten folgende Fehlermeldung:

error: value id_= is not a member of Entity
Dimitry
quelle
2
Keine relevanten. Sie haben auch einen Fehler, wenn Sie val anstelle von def verwenden (Fehler: Neuzuweisung zu val), und das ist vollkommen logisch.
Volia17
Nicht wenn du benutzt var. Der Punkt ist, wenn es sich um Felder handelt, sollten sie als solche gekennzeichnet werden. Ich denke nur, alles so zu haben, wie defes kurzsichtig ist.
Dimitry
@Dimitry, sicher, mit können varSie die Kapselung unterbrechen . Die Verwendung von a def(oder a val) wird jedoch einer globalen Variablen vorgezogen. Ich denke, was Sie suchen, ist so etwas wie, case class ConcreteEntity(override val id: Int) extends Entitydamit Sie es daraus erstellen können. def create(e: Entity) = ConcreteEntity(1)Dies ist sicherer, als die Kapselung zu brechen und jeder Klasse zu erlauben, die Entität zu ändern.
Jono