Идиоматический способ Scala иметь дело с именами полей базовых и производных классов?

Рассмотрим следующие базовые и производные классы в Scala:

    abstract class Base( val x : String )

    final class Derived( x : String ) extends Base( "Base's " + x )
    {
        override def toString = x
    }

Здесь идентификатор 'x' параметра класса Derived переопределяет поле базового класса, поэтому вызов toString выглядит следующим образом:

    println( new Derived( "string" ).toString )

возвращает производное значение и дает результат "строка".

Таким образом, ссылка на параметр 'x' побуждает компилятор автоматически генерировать поле в Derived, которое обслуживается при вызове toString. Обычно это очень удобно, но приводит к репликации поля (сейчас я храню поле как в Base, так и в Derived), что может быть нежелательно. Чтобы избежать этой репликации, я могу переименовать параметр класса Derived с «x» на что-то другое, например «_x»:

    abstract class Base( val x : String )

    final class Derived( _x : String ) extends Base( "Base's " + _x )
    {
        override def toString = x
    }

Теперь вызов toString возвращает «строку Base», чего я и хочу. К сожалению, теперь код выглядит несколько уродливо, и использование именованных параметров для инициализации класса также становится менее элегантным:

    new Derived( _x = "string" ) 

Существует также риск забыть дать параметрам инициализации производных классов разные имена и непреднамеренно сослаться на неправильное поле (нежелательно, поскольку базовый класс может фактически содержать другое значение).

Есть ли способ лучше?

Редактировать 1. Чтобы уточнить, мне действительно нужны только базовые значения; Производные просто кажутся необходимыми для инициализации полей базового класса. Пример ссылается на них только для иллюстрации возникающих проблем.

Редактировать 2. На самом деле пример был бы понятнее, если бы я использовал vars вместо vals, так как это подчеркивает проблему с изменением значений позже в базовом классе:

    class Base( var x : Int ) { def increment() { x = x + 1 } }
    class Derived( x : Int ) extends Base( x ) { override def toString = x.toString }

    val derived = new Derived( 1 )
    println( derived.toString )     // yields '1', as expected
    derived.increment()
    println( derived.toString )     // still '1', probably unexpected

Изменить 3. Было бы неплохо иметь способ подавить автоматическую генерацию поля, если бы в противном случае производный класс скрыл бы поле базового класса. Может показаться, что компилятор Scala на самом деле мог бы сделать это за вас, но, конечно, это противоречит более общему правилу «более близких» идентификаторов (класс Derived «x»), скрывающих более удаленные (класс Base). 'Икс'). Кажется, что достаточно хорошим решением был бы модификатор типа «новый», может быть, так:

    class Base( var x : Int ) { def increment() { x = x + 1 } }
    class Derived( noval x : Int ) extends Base( x ) { override def toString = x.toString }

    val derived = new Derived( 1 )
    println( derived.toString )     // yields '1', as expected
    derived.increment()
    println( derived.toString )     // still '2', as expected

person Gregor Scheidt    schedule 29.06.2011    source источник
comment
Я бы вставил билет на улучшение, чтобы поймать случай Edit2 и Edit3 и выдать предупреждение. Эта проблема связана не только с конструкторами классов, вы также можете вызвать фанки с именами параметров в подклассах.   -  person jsuereth    schedule 29.06.2011
comment
@jsuereth Было бы неплохо иметь предупреждение даже с val, если значения двух полей могут различаться (т. Е. Если вы просто не передаете параметр конструктора базовому классу). Я не вижу способа ссылаться на Base.x из Derived (даже с использованием аннотации ссылки на себя).   -  person Aaron Novstrup    schedule 29.06.2011
comment
Если x является val, вы можете ссылаться на super[Base].x в Derived. В противном случае это не совсем поле, а что-то вроде того, что «заключено в область видимости класса», поэтому оно автоматически добавляется как поле. Я знаю, что это различие немного глупо, но представьте себе тот же механизм, который поднимает переменные в конструкторы анонимных функций.   -  person jsuereth    schedule 30.06.2011


Ответы (7)


Идиоматическим способом избежать дублирования поля было бы написать

abstract class Base { val x: String }

final class Derived(val x: String) extends Base {
   def toString = x
}

Однако в вашей версии похоже, что вам действительно нужно второе поле, поскольку у вас есть два разных значения. Как вы правильно заметили, присвоение этим полям одного и того же имени может привести к путанице.

Поскольку вам на самом деле не нужен аргумент конструктора вне конструктора, вы можете использовать этот подход (частный конструктор с сопутствующим модулем, который действует как фабрика):

abstract class Base { val x: String }

final class Derived private (val x: String) extends Base {
   def toString = x
}
object Derived {
   def apply(x: String) = new Derived("Base " + x)
}
person Aaron Novstrup    schedule 29.06.2011
comment
Ну, мне действительно нужны только базовые значения; производные просто кажутся необходимыми для инициализации базовых. Было бы неплохо иметь способ подавить автоматическую генерацию поля, если бы в противном случае производный класс скрыл бы поле базового класса. - person Gregor Scheidt; 29.06.2011
comment
@Gregor Я обновил свой ответ, чтобы показать, как вы можете использовать частный конструктор с сопутствующим модулем, чтобы получить нужное вам поведение. - person Aaron Novstrup; 29.06.2011
comment
Меня беспокоило непреднамеренное затенение полей «Базовые» автоматически сгенерированными полями в классе «Производный», что все еще происходит в вашем примере: «def toString = x» вернет значение «Производное», а не значение «Базовое» ( см. мои Edit2 и Edit3 выше). Я думаю, что мой вопрос намекает на непреднамеренный источник ошибок, присущий тому, как компилятор Scala автоматически генерирует поля. Избежать этого легко (выберите другое имя параметра в «Производный»), но мне просто интересно, есть ли лучший способ. - person Gregor Scheidt; 04.07.2011
comment
@Gregor Scheidt В обеих моих версиях поле является абстрактным в базе, поэтому нет никакой разницы между значением, которое возвращает toString, и значением, которое возвращает obj.x. - person Aaron Novstrup; 04.07.2011
comment
Я отредактировал второй пример, чтобы было более понятно, что затененного поля нет. - person Aaron Novstrup; 04.07.2011
comment
Хорошо, мне потребовалось некоторое время, чтобы понять это, но это действительно тот ответ, который я искал. Мне нужно сделать поле базового класса абстрактным, а не использовать инициализацию параметров класса; поле Derived в основном становится полем базового класса, и я не получаю ни затенения, ни дублирования полей. Спасибо! - person Gregor Scheidt; 05.07.2011

Поскольку базовый класс является абстрактным, не похоже, что вам действительно нужен val (или var) в классе Base с соответствующим вспомогательным полем. Вместо этого вы просто хотите гарантировать, что такая вещь будет доступна в конкретных подклассах.

В Java вы бы использовали метод доступа, такой как getX, чтобы добиться этого.

В Scala мы можем пойти еще дальше: vals, vars и defs занимают одно и то же пространство имен, поэтому val можно использовать для реализации абстрактного def (или для переопределения конкретного def, если вас это устраивает). Более формально это известно как «принцип единообразного доступа».

abstract class Base{ def x: String }

class Derived(val x: String) extends Base {
    override def toString = x
}

Если вам нужно, чтобы x можно было установить через ссылку на Base, вам также необходимо объявить "сеттер" (который затем можно реализовать с помощью var):

abstract class Base {
  def x: String
  def x_=(s: String): Unit
}

class Derived(var x: String) extends Base {
    override def toString = x
}

(не то чтобы я когда-либо поощрял изменчивость в каком-либо дизайне, если только для этого не было особенно убедительного обоснования. Слишком много веских причин отдавать предпочтение неизменности по умолчанию)

ОБНОВЛЕНИЕ

Преимущества этого подхода:

  • x может быть полностью синтетическим значением, реализованным полностью с точки зрения других значений (например, площади круга, для которого вы уже знаете радиус)
  • x может быть реализован на любой произвольной глубине в иерархии типов, и его не нужно явно передавать через каждый промежуточный конструктор (каждый раз с другим именем)
  • Требуется только одно вспомогательное поле, поэтому память не тратится впустую.
  • Поскольку теперь ему не нужен конструктор, Base можно реализовать как трейт; если ты так хочешь
person Kevin Wright    schedule 29.06.2011
comment
В исходной задаче конструктор класса Derived преобразует аргумент (с помощью функции "Base's " + _) перед тем, как присвоить его полю. - person Aaron Novstrup; 29.06.2011
comment
@Aaron - Конечно, но, по-видимому, это просто для устранения неоднозначности двух вспомогательных полей при отслеживании проблемы. Я указываю, что на самом деле не должно быть двух, во-первых... - person Kevin Wright; 29.06.2011
comment
Смотрите комментарий ОП к моему ответу. Я думаю, проблема возникает из-за того, что аргумент конструктора отличается от значения, хранящегося в поле. В противном случае, по крайней мере, с val не имело бы значения создание второго поля. - person Aaron Novstrup; 29.06.2011
comment
@Aaron - Насколько я понимаю, параметр в Derived когда-либо существовал только для передачи значения в Base, но это вызывало проблемы с теневым копированием имени, если только не использовалось другое имя (чего ОП вполне разумно не хотел). Решение сделать его полностью абстрактным в базовом классе не только позволяет повторно использовать имя, но и фактически требует этого. - person Kevin Wright; 29.06.2011
comment
@Kevin Спасибо за подробный обзор. :-) Я полностью согласен с неизменяемостью и вижу ценность использования аксессора вместо поля во многих ситуациях. В конечном счете, возникает вопрос: как я могу не выстрелить себе в ногу с помощью одного вида синтаксического сахара (автоматическая генерация поля Scala в «Производном», что может привести к непреднамеренному затенению) при использовании другого вида синтаксического сахара (удобная генерация поля с помощью «val ' в списке параметров класса 'Base', чтобы сгенерировать действительно желаемое поле)? - person Gregor Scheidt; 04.07.2011
comment
Если x в Base является val, то просто передайте его производному как обычный (не val или var) параметр конструктора. Он по-прежнему будет теневым, но затененное значение будет идентичным, так что проблем нет. Если Derived является классом case, то применяется та же логика, хотя у вас будут дополнительные затраты на второе резервное поле. если x является изменяемым, то единственное действительно чистое решение — сделать его абстрактным в Base - person Kevin Wright; 04.07.2011
comment
В отличие от абстрактного val, который должен быть реализован с помощью val, вы можете кратко определить abstract class Base { var x: String } и при этом реализовать собственный геттер и сеттер. ИМО выглядит гораздо красивее. - person 0__; 10.06.2013

Вы можете попробовать это:

abstract class Base( val x : String )

final class Derived( _x : String ) extends Base( _x ) {
  override val x  = "Base's " + _x
  override def toString = x
}

потом

println(new Derived("string").toString)

печатает именно то, что вы хотите

person Yuriy Zubarev    schedule 29.06.2011
comment
Хорошо спасибо. Как я впоследствии пояснил выше, мне действительно нужны только базовые значения, а не производные. - person Gregor Scheidt; 29.06.2011

вы уже предоставили ответ, который работает

abstract class Base( val x : String )

final class Derived( _x : String ) extends Base( "Base's " + _x )
{
    override def toString = x
}

Если проблема в том, что _x не очень красивое имя, вам следует использовать осмысленное. В качестве альтернативы вы можете объявить свои классы следующим образом

abstract class Base( val _x : String )

final class Derived( x : String ) extends Base( "Base's " + x )
{
    override def toString = _x
}

И теперь у вас будет "хороший" синтаксис для инициализации Derived экземпляров.

Если бы scala позволяла

способ подавить автоматическую генерацию поля, если в противном случае производный класс скрыл бы поле базового класса.

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

person Mirco Dotta    schedule 29.06.2011
comment
Правильно — к сожалению, то, что имеет значение для Base, вероятно, также имеет значение и для Derived. Как правило, вы получаете надуманные имена, такие как theX, myX, localX или initialX, не намного лучше, чем _x. - person Gregor Scheidt; 29.06.2011

По предложению @jsuereth я создал улучшение, отмеченное галочкой для Scala, просто для записи, которое, я надеюсь, правильно суммирует содержание обсуждений здесь. Спасибо за ваш вклад! Билет можно найти здесь, содержание ниже: https://issues.scala-lang.org/browse/SI-4762

Непреднамеренное затенение полей базового класса в производных классах, желательно предупреждение

Проблема возникает всякий раз, когда (а) параметр класса в производном классе использует тот же символ, что и поле или функция в базовом классе, и (б) этот символ базового класса впоследствии используется в производном классе. Параметр производного класса заставляет компилятор автоматически генерировать поле с тем же именем, которое скрывает символ базового класса. Любая ссылка на этот символ в производном классе, предназначенная для ссылки на поле базового класса, затем непреднамеренно (а) вызывает дублирование определения поля и (б) неожиданно (но правильно) ссылается на автоматически сгенерированное поле в производном классе. .

Пример кода:

class Base( val x : String )
class Derived( x : String ) extends Base( x ) { override def toString = x }

Поскольку в классе «Base» есть «val» для генерации поля, а в производном классе нет, разработчик явно намеревается использовать «Derived» «x» только для передачи в Base, и поэтому ожидает ссылку в «toString». ', чтобы получить базовое значение. Вместо этого ссылка в «toString» заставляет компилятор автоматически генерировать поле «Derived.x», которое затеняет поле «Base.x», что приводит к ошибке. Компилятор ведет себя правильно, но в результате возникает ошибка программирования.

Сценарий использования (с полями var для наглядности рисков):

class Base( var x : Int ) { def increment() { x = x + 1 } }
class Derived( x : Int ) extends Base( x ) { override def toString = x.toString }

val derived = new Derived( 1 )
println( derived.toString )     // yields '1', as expected
derived.increment()
println( derived.toString )     // still '1', probably unexpected

Поскольку эта проблема возникает всякий раз, когда кто-либо использует способ по умолчанию для инициализации полей базового класса из производного класса в Scala, сценарий может показаться чрезвычайно распространенным и привести к множеству ошибок программирования (легко исправимых, но все же) для новых пользователей.

Существует простой обходной путь для этой проблемы (используйте разные имена для параметра производного класса, например '_x', 'theX', 'initialX' и т. д.), но это приводит к нежелательным дополнительным символам.

Решение A (минимальное): выдавать предупреждение всякий раз, когда компилятор делает вывод о том, что для параметра класса требуется автоматически сгенерированное поле в производном классе, которое затеняет символ, уже определенный в базовом классе.

Решение B: обходной путь, по-прежнему необходимый для решения A, состоит в том, чтобы придумывать новое имя символа каждый раз, когда вы инициализируете поле базового класса. Этот сценарий возникает постоянно, и засорение пространства имен именами обходных полей, такими как «_x» и «theX», кажется нежелательным. Вместо этого было бы неплохо иметь способ подавить автоматическую генерацию полей, если разработчик решит, что в противном случае символы производного класса в конечном итоге скроют символ базового класса (например, после предупреждения решения A). Возможно, полезным дополнением к Scala был бы такой модификатор, как «noval» (или «passthrough», или «temp», или что-то еще — в дополнение к «val» и «var»), например:

class Base( var x : Int ) { def increment() { x = x + 1 } }
class Derived( noval x : Int ) extends Base( x ) { override def toString = x.toString }

val derived = new Derived( 1 )
println( derived.toString )     // yields '1', as expected
derived.increment()
println( derived.toString )     // still '2', as expected
person Gregor Scheidt    schedule 04.07.2011

Хорошо, прежде всего я хотел бы отметить, что ответ от @Yuriy Zubarev, вероятно, то, что вы действительно хотите получить. Во-вторых, я думаю, проблема может заключаться в вашем дизайне. Проверь это. Это часть вашего кода:

extends Base( "Base's " + _x )

Таким образом, некоторое значение x входит в ваш производный класс и модифицируется информацией (в данном случае "Base's " + ...). Ты видишь проблему? Почему ваш производный тип знает что-то, что должен был знать ваш базовый тип? Вот решение, которое я предлагаю.

abstract class Base {
    // this works especially well if you have a var
    // which is what you wanna have as you pointed out later.
    var x: String
    x = "Base's " + x
}

final class Derived(override var x: String ) extends Base{
   override def toString = x
}

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

person agilesteel    schedule 29.06.2011
comment
Спасибо за ответ. Сборка строки в вызове конструктора класса «Base» была просто быстрым хаком, чтобы сделать «x» и «_x» различимыми. Единственное, что меня действительно беспокоило, так это непреднамеренное затенение. В более общем контексте ваше предложение действительно было бы лучше. - person Gregor Scheidt; 04.07.2011
comment
Спасибо за комментарий. Я уже начал переживать, что мой ответ полный кусок **** ;) - person agilesteel; 04.07.2011

@Gregor Scheidt, ваш код не работает, если я переведу toString() в Derived, как показано ниже:

object Test {
  abstract class Base ( val x: String)

  final class Derived(x: String) extends Base(x + " base") {
    override def toString() = x
  }

  def main(args: Array[String]): Unit = {
    val d  = new Derived( "hello")
    println( d) // hello
  }
}

В сообщении с официального сайта говорилось,

Такой параметр, как класс Foo(x : Int), превращается в поле, если на него ссылаются в одном или нескольких методах.

И ответ Мартина подтверждает его истинность:

Это все верно, но к этому следует относиться как к технике реализации. Поэтому спецификация об этом умалчивает.

поскольку нет способа предотвратить действие компилятора, мой вариант заключается в том, что более надежным способом ссылки на поле базового класса является использование другого имени, например, с использованием символа подчеркивания «_» в качестве префикса, как показано ниже:

object Test {
  abstract class Base ( val x: String)

  final class Derived(_x: String) extends Base(_x + " base") {
    override def toString() = x
  }

  def main(args: Array[String]): Unit = {
    val d  = new Derived( "hello")
    println( d) // hello
  }
}
person soulmachine    schedule 28.02.2013