如何在 Scala 中使用可选成员扩展抽象 class?

How can I extend an abstract class with an optional member in Scala?

我有一个抽象基类 class,Foo,我希望它的构造函数有一个可选参数。如果提供 none,我将只给它一个 None 值。

源 Foo 不会有 parents,所以我只想在没有 parents 列表的情况下构造它们(保留 parent 列表的默认值)

派生的 Foo 可能已经提供了 parents,所以我想模仿 Foo 基础的签名 class。

以下是我的尝试:

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

我收到一个编译器错误,指出无法覆盖可变变量(引用 DerivedFoo 构造函数中的 parentIds

此列表可能会更改,因此我不想将其设为 val(这消除了我的编译器问题)。

这是一个非常基本的 OO 问题,所以它一定比我想象的要简单。我怎样才能以惯用的方式实现我想要的行为?

我认为您可以通过如下更改摘要中的参数名称来实现您的目标class。

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

我在阅读 documentation 后设法解决了这个问题:

The constructor parameters of case classes are treated as public values and can be accessed directly.

因为我的基础 class 是抽象的,我可以简单地使用默认的 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) { }

这是另一种可能更好的方法。明确承认 class 参数和 class 成员之间的区别。如果您喜欢遵循此代码块,也可以将它们设为私有成员。

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

对于变异,你可以import scala.collection.mutable并使用mutable.ListBuffer而不是List。 当然,我假设您不会将 DerivedFoo 实例的 parentIdsSome 更改为 None。 这将允许您使用 vals 但仍然具有可变状态。

但我不会说可变状态是惯用的 Scala。

你通常使用不可变的 valList,只要你想改变列表就复制 object。

  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 总是有 parents,所以您不必处理 Option . (不过你仍然需要处理空列表)

还要注意 trait 上的 sealed 关键字,这不是必需的,但如果您想匹配抽象 class 或 trait 的实例,则建议使用。 (只有当您拥有所有 sub-classes 时,您才可以使用 sealed,在您的示例中似乎就是这种情况)