Коли використовувати val або def в рисах Scala?


90

Я переглядав ефективні слайди Scala, і на слайді 10 згадується ніколи не використовувати valв a traitдля абстрактних членів і використовувати defзамість них. У слайді детально не згадується, чому використання абстрактних valв a traitє анти-шаблоном. Буду вдячний, якщо хтось зможе пояснити найкращі практики використання val проти def як риси для абстрактних методів

Відповіді:


130

A defможе бути реалізований будь-яким з a def, a val, a lazy valабо an object. Отже, це найбільш абстрактна форма визначення члена. Оскільки риси, як правило, є абстрактними інтерфейсами, сказати, що ви хочете, valце сказати, як має бути реалізація. Якщо ви просите a val, клас, що реалізує, не може використовувати adef .

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реалізацією, щоб це також запобігло помилці ініціалізації.


8
слова про хитрий порядок ініціалізації та дивовижні нульові значення?
om-nom-nom

Так ... я навіть не ходив би туди. Правда, це також аргументи проти val, але я думаю, що основною мотивацією має бути просто приховування реалізації.
0__

2
Можливо, це змінилося в останній версії Scala (2.11.4 станом на цей коментар), але ви можете замінити a valза допомогою lazy val. Ваше твердження про те, що ви не змогли б створити, F3якби це barбуло, valє неправильним. Тим не менш, абстрактними членами за рисами завжди повинні бути 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, звичайно, використовуючи varдозвольте вам розірвати інкапсуляцію. Але використання def(або a val) переважно над глобальною змінною. Я думаю, що ви шукаєте щось на зразок case class ConcreteEntity(override val id: Int) extends Entityтого, щоб ви могли створити його з def create(e: Entity) = ConcreteEntity(1)Це безпечніше, ніж розбиття інкапсуляції та дозволяючи будь-якому класу змінювати сутність.
Джоно
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.