Как я могу расширить абстрактный класс с помощью необязательного члена в Scala?

У меня есть абстрактный базовый класс Foo, конструктор которого я хотел бы иметь необязательный параметр. Если ничего не указано, я просто присвою ему значение None.

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

Производный Foo мог предоставить родителей, поэтому я хотел бы имитировать сигнатуру базового класса Foo.

Ниже моя попытка:

abstract class Foo(val id: String, var parentIds: Option[List[String]]=None) { }

case class SourceFoo(override val id: String)
  extends Foo(id, parentIds=None) { }

case class DerivedFoo(override val id: String, 
                      override var parentIds: Option[List[String]])
  extends Foo(id, parentIds) { }

Я получаю сообщение об ошибке компилятора, что изменяемая переменная не может быть переопределена (ссылка на parentIds в конструкторе DerivedFoo.

Этот список может быть изменен, поэтому я не хочу делать его val (что устраняет мои проблемы с компилятором).

Это очень простой объектно-ориентированный вопрос, поэтому он должен быть проще, чем мне кажется. Как я могу добиться желаемого поведения идиоматически?


person erip    schedule 17.10.2016    source источник


Ответы (4)


Мне удалось исправить это после прочтения документации:

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

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

Мне просто нужно указать, что parentIds является переменной в конструкторе DerivedFoo.

abstract class Foo(id: String, parentIds: Option[List[String]]=None) { }

case class SourceFoo(id: String) extends Foo(id) { }

case class DerivedFoo(id: String, var parentIds: Option[List[String]]=None) 
    extends Foo(id, parentIds) { }
person erip    schedule 17.10.2016

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

abstract class Foo(identifier: String, parentIdentifiers: Option[List[String]]) { 
  val id = identifier
  var parentIds = parentIdentifiers
}

case class SourceFoo(override val id: String) extends Foo(id, parentIdentifiers = None) { }

case class DerivedFoo(identifier: String, parentIdentifiers: Option[List[String]]) extends Foo(identifier, parentIdentifiers) { }

После этого вы можете создать DerivedFoo и обращаться к членам, как вы, вероятно, ожидаете, и у вас не будет двух членов с разными именами.

Выход REPL:

scala> DerivedFoo("1", Some(List("200","201","202")))
res0: DerivedFoo = DerivedFoo(1,Some(List(200, 201, 202)))

scala> res0.parentIds
res1: Option[List[String]] = Some(List(200, 201, 202))

scala> res0.parentIds = Some(List("800", "801", "802"))
res0.parentIds: Option[List[String]] = Some(List(800, 801, 802))
person Scott Shipp    schedule 17.10.2016

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

abstract class Foo(val id: String, var parentIdentifiers: Option[List[String]]) { 
  parentIdentifiers = None
}

case class SourceFoo(override val id: String)
  extends Foo(id, parentIdentifiers = None) { }

case class DerivedFoo(override val id: String, 
                      var parentIds: Option[List[String]])
  extends Foo(id, parentIds) { }
person Scott Shipp    schedule 17.10.2016
comment
Обратите внимание, что тогда DerivedFoo будет иметь оба члена, поэтому убедитесь, что вы обращаетесь к правильному. Возможно, вы захотите дать им более описательные имена, чтобы обозначить это — parentIds для конструктора абстрактного класса и что-то вроде preliminaryParentIds для конструктора DerivedFoo. - person Tzach Zohar; 17.10.2016
comment
Это единственное решение? :( - person erip; 17.10.2016
comment
Нет, держись, я дам тебе еще один через несколько - person Scott Shipp; 17.10.2016
comment
@TzachZohar В любом случае, разве эти два участника не лежат в основе проблемы? Когда он пытается переопределить переменную, если компилятор позволит ему это сделать, тогда все равно будут две переменные. Тот, что в подтипе, будет иметь то же имя и затенит переменную супертипа. Но в любом случае, другой ответ, который я только что опубликовал, возвращается к тому, что, вероятно, является правильным путем (TM) - person Scott Shipp; 17.10.2016

Для мутации вы можете import scala.collection.mutable использовать mutable.ListBuffer вместо List. Я предполагаю, конечно, что вы не будете менять parentIds экземпляра DerivedFoo с Some на None. Это позволит вам использовать vals, но при этом иметь изменяемое состояние.

Но я бы не сказал, что изменяемое состояние — это идиоматический Scala.

Обычно вы используете неизменяемые val и List и просто копируете объект всякий раз, когда хотите изменить список.

  val fooA = SourceFoo("a")
  val fooB = DerivedFoo("b", "a" :: Nil)
  val fooB2 = fooB.copy(parentIds = fooB.parentIds :+ "x")

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

sealed abstract class Foo(val id: String, val parentIdsOpt: Option[List[String]])

case class SourceFoo(override val id: String)
  extends Foo(id, None)

case class DerivedFoo(override val id: String, val parentIds: List[String])
  extends Foo(id, Some(parentIds))

Что очень близко к тому, что у вас было.

Обратите внимание, что DerivedFoo.parentIds больше не Option, потому что у DerivedFoo всегда есть родители, поэтому вам не нужно иметь дело с Option. (Однако вам все равно придется иметь дело с пустым списком)

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

person Gabor Juhasz    schedule 17.10.2016