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

#scala #oop #abstract-class #case-class

#scala #ооп #абстрактный класс #case-класс

Вопрос:

У меня есть абстрактный базовый класс 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 конструкторе.

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

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

Ответ №1:

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

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

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

Мне просто нужно указать, что parentIds это var в конструкторе 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) { }
  

Ответ №2:

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

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

Ответ №3:

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

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

Комментарии:

1. Обратите внимание, что тогда DerivedFoo будут оба члена, поэтому убедитесь, что вы получили доступ к нужному. Может быть, вы захотите дать им более описательные имена, чтобы обозначить это — parentIds для конструктора абстрактного класса и что-то вроде preliminaryParentIds конструктора for DerivedFoo ‘s

2. Это единственное решение? 🙁

3. Нет, подождите, я дам вам еще несколько

4. @TzachZohar В любом случае, разве эти два члена не являются причиной проблемы? Когда он пытается переопределить var, если компилятор позволит ему это сделать, тогда все равно будут две переменные. Тот, который находится в подтипе, будет иметь то же имя и будет «затенять» переменную супертипа. Но в любом случае, другой ответ, который я только что опубликовал, возвращается к тому, что, вероятно, является правильным (TM)

Ответ №4:

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

Но я бы не сказал, что изменяемое состояние является идиоматическим 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 , только если у вас есть все подклассы, что, похоже, имеет место в вашем примере)