Kiedy używać val lub def w cechach Scala?

90

Przeglądałem efektywne slajdy scala i na slajdzie 10 wspomina się, że nigdy nie używaj valich traitjako abstrakcyjnych członków i używaj defzamiast tego. Slajd nie wspomina szczegółowo, dlaczego użycie abstrakcji valw a traitjest anty-wzorcem. Byłbym wdzięczny, gdyby ktoś mógł wyjaśnić najlepsze praktyki dotyczące używania wartości val vs def w cechach metod abstrakcyjnych

Mansur Ashraf
źródło

Odpowiedzi:

130

A defmożna zaimplementować za pomocą a def, a val, a lazy vallub an object. Jest to więc najbardziej abstrakcyjna forma definiowania członka. Ponieważ cechy są zwykle abstrakcyjnymi interfejsami, stwierdzenie, że chcesz, valto powiedzenie, jak powinna działać implementacja. Jeśli poprosisz o a val, klasa implementująca nie może użyć def.

A valjest potrzebne tylko wtedy, gdy potrzebujesz stabilnego identyfikatora, np. Dla typu zależnego od ścieżki. To jest coś, czego zwykle nie potrzebujesz.


Porównać:

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

Gdybyś miał

trait Foo { val bar: Int }

nie byłbyś w stanie zdefiniować F1lub F3.


OK, i żeby zmylić Cię i odpowiedzieć @ om-nom-nom - użycie abstrakcyjnych vals może powodować problemy z inicjalizacją:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Jest to brzydki problem, który moim zdaniem powinien zniknąć w przyszłych wersjach Scali poprzez naprawienie go w kompilatorze, ale tak, obecnie jest to również powód, dla którego nie należy używać abstrakcyjnych vals.

Edycja (styczeń 2016 r.): Możesz zastąpić abstrakcyjną valdeklarację lazy valimplementacją, aby również zapobiec niepowodzeniu inicjalizacji.

0__
źródło
8
słowa o trudnej kolejności inicjalizacji i zaskakujących zerach?
om-nom-nom
Tak ... Nawet bym tam nie poszedł. To prawda, że ​​są to również argumenty przeciwko val, ale myślę, że podstawową motywacją powinno być po prostu ukrycie implementacji.
0__
2
Mogło się to zmienić w najnowszej wersji Scali (od tego komentarza 2.11.4), ale możesz zastąpić valplik lazy val. Twoje twierdzenie, że nie byłbyś w stanie stworzyć, F3gdyby barbyło a, valnie jest poprawne. Powiedział, że członkowie w abstrakcyjnych cech powinna być zawsze def„s
mplis
Przykład Foo / Fail działa zgodnie z oczekiwaniami, jeśli zastąpisz val schoko = bar + bargo lazy val schoko = bar + bar. To jeden ze sposobów uzyskania pewnej kontroli nad kolejnością inicjalizacji. Ponadto użycie lazy valzamiast defw klasie pochodnej pozwala uniknąć ponownego obliczania.
Adrian
2
Jeśli zmienisz val bar: Intna, def bar: Int Fail.schokonadal wynosi zero.
Jasper-M
8

Wolę nie używać valw cechach, ponieważ deklaracja val ma niejasną i nieintuicyjną kolejność inicjalizacji. Możesz dodać cechę do już działającej hierarchii, a to zepsułoby wszystkie rzeczy, które działały wcześniej, zobacz mój temat: dlaczego używać zwykłego val na zajęciach niedokończonych

Powinieneś mieć na uwadze wszystkie rzeczy związane z używaniem tych deklaracji val, które ostatecznie prowadzą do błędu.


Zaktualizuj za pomocą bardziej skomplikowanego przykładu

Ale są chwile, kiedy nie można było uniknąć używania val. Jak wspomniał @ 0__, czasami potrzebujesz stabilnego identyfikatora, a nim defnie jest.

Podałbym przykład, aby pokazać, o czym mówił:

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
}

Ten kod powoduje błąd:

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

Jeśli zastanowisz się przez chwilę, zrozumiesz, że kompilator ma powód do narzekania. W Access2.accessprzypadku, gdy nie może w żaden sposób wyprowadzić typu zwracanego. def holderoznacza, że ​​można by go wdrożyć na szeroką skalę. Mógłby zwrócić różnych posiadaczy dla każdego połączenia, a posiadacze mogliby zawierać różne Innertypy. Ale maszyna wirtualna Java oczekuje zwrócenia tego samego typu.

ayvango
źródło
3
Kolejność inicjalizacji nie powinna mieć znaczenia, ale zamiast tego otrzymujemy zaskakujące NPE w czasie wykonywania, w porównaniu z anty-wzorcem.
Jonathan Neufeld
scala ma deklaratywną składnię, która ukrywa imperatywną naturę. Czasami ta imperatywność działa sprzecznie z intuicją
ayvango
-4

Zawsze używanie def wydaje się trochę niezręczne, ponieważ coś takiego nie zadziała:

trait Entity { def id:Int}

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

Otrzymasz następujący błąd:

error: value id_= is not a member of Entity
Dimitry
źródło
2
Nie dotyczy. Jeśli używasz val zamiast def (błąd: ponowne przypisanie do val), również masz błąd i jest to całkowicie logiczne.
volia17
Nie, jeśli używasz var. Chodzi o to, że jeśli są to pola, to powinny być tak oznaczone. Po prostu myślę, że wszystko defjest krótkowzroczne.
Dimitry
@Dimitry, jasne, używając varlet's you break encapsulation. Ale użycie a def(lub a val) jest preferowane zamiast zmiennej globalnej. Myślę, że to, czego szukasz, jest czymś w rodzaju case class ConcreteEntity(override val id: Int) extends Entity, abyś mógł to utworzyć z def create(e: Entity) = ConcreteEntity(1)To jest bezpieczniejsze niż zerwanie hermetyzacji i zezwolenie dowolnej klasie na zmianę Entity.
Jono