Когда использовать val или def в трейтах Scala?

90

Я просматривал эффективные слайды scala, и на слайде 10 упоминается, что никогда не следует использовать valв a traitдля абстрактных элементов и использовать defвместо этого. На слайде не упоминается подробно, почему использование аннотации valв a traitявляется анти-шаблоном. Я был бы признателен, если бы кто-нибудь мог объяснить лучшие практики использования val vs def в трейте для абстрактных методов.

Мансур Ашраф
источник

Ответы:

130

A defможет быть реализован с помощью a def, a val, a lazy valили an object. Так что это наиболее абстрактная форма определения члена. Поскольку черты обычно являются абстрактными интерфейсами, то, что вы хотите, valозначает, как должна работать реализация. Если вы запрашиваете a val, реализующий класс не может использовать def.

A valнужен только в том случае, если вам нужен стабильный идентификатор, например, для типа, зависящего от пути. Это то, что вам обычно не нужно.


Сравните:

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

Если у тебя есть

trait Foo { val bar: Int }

вы не сможете определить F1или F3.


Хорошо, и чтобы запутать вас, ответьте @ om-nom-nom - использование абстрактных vals может вызвать проблемы с инициализацией:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Это уродливая проблема, которая, по моему мнению, должна исчезнуть в будущих версиях Scala, исправив ее в компиляторе, но да, в настоящее время это также причина, по которой не следует использовать абстрактные vals.

Изменить (январь 2016 г.): вам разрешено переопределить абстрактное valобъявление с помощью lazy valреализации, чтобы также предотвратить сбой инициализации.

0__
источник
8
слова о хитром порядке инициализации и неожиданных нулях?
om-nom-nom
Да ... Я бы даже не пошел туда. Да, это тоже аргументы против val, но я думаю, что основная мотивация должна заключаться в том, чтобы просто скрыть реализацию.
0__
2
Это могло измениться в последней версии Scala (2.11.4 на момент этого комментария), но вы можете переопределить a valс помощью lazy val. Ваше утверждение, что вы не смогли бы творить, F3если бы barбыло a, valневерно. Тем не менее, абстрактные члены в traits всегда должны быть defs
mplis
Пример Foo / Fail работает должным образом, если вы замените его val schoko = bar + barна lazy val schoko = bar + bar. Это один из способов контроля над порядком инициализации. Кроме того, использование lazy valвместо defв производном классе позволяет избежать повторного вычисления.
Адриан
2
Если вы измените val bar: Intна def bar: Int Fail.schoko, все равно ноль.
Джаспер-М
8

Я предпочитаю не использовать valв трейтах, потому что объявление val имеет нечеткий и неинтуитивный порядок инициализации. Вы можете добавить черту к уже работающей иерархии, и это нарушит все, что работало раньше, см. Мою тему: зачем использовать простой val в не финальных классах

Вы должны помнить обо всем, что касается использования этих объявлений val, что в конечном итоге приведет к ошибке.


Обновите более сложным примером

Но бывают случаи, когда не избежать употребления val. Как упоминал @ 0__, иногда вам нужен стабильный идентификатор, а defне он.

Я бы привел пример, чтобы показать, о чем он говорил:

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
}

Этот код вызывает ошибку:

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

Если вы на минуту задумаетесь, то поймете, что у компилятора есть причина жаловаться. В этом Access2.accessслучае он никаким образом не может получить возвращаемый тип. def holderозначает, что это может быть реализовано в широком смысле. Он может возвращать разных держателей для каждого вызова, и эти держатели будут включать разные Innerтипы. Но виртуальная машина Java ожидает возврата того же типа.

Айванго
источник
3
Порядок инициализации не имеет значения, но вместо этого мы получаем неожиданные NPE во время выполнения, по сравнению с анти-шаблоном.
Джонатан Нойфельд
scala имеет декларативный синтаксис, за которым скрывается императивный характер. Иногда эта императивность работает
вопреки
-4

Всегда использовать def кажется немного неудобным, так как что-то вроде этого не сработает:

trait Entity { def id:Int}

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

Вы получите следующую ошибку:

error: value id_= is not a member of Entity
Димитрий
источник
2
Не имеет значения. У вас тоже возникает ошибка, если вы используете val вместо def (ошибка: переназначение на val), и это совершенно логично.
volia17
Нет, если вы используете var. Дело в том, что если это поля, то они должны быть обозначены как таковые. Я просто считаю, что иметь все так defже недальновидно.
Dimitry
@Dimitry, конечно, с помощью varдавайте сломаем инкапсуляцию. Но использование def(или val) предпочтительнее глобальной переменной. Я думаю, что то, что вы ищете, похоже на то, case class ConcreteEntity(override val id: Int) extends Entityчто вы можете создать его из. def create(e: Entity) = ConcreteEntity(1)Это безопаснее, чем нарушение инкапсуляции и разрешение любому классу изменять Entity.
Jono