如何使用Scala的this类型、抽象类型等来实现自身类型?

46

在其他问题中我没有找到答案。假设我有一个抽象超类 Abstract0 和两个子类 Concrete1 和 Concrete1。我想要在 Abstract0 中定义如下内容:

def setOption(...): Self = {...}

其中Self将是具体的子类型。这将允许像这样链接调用setOption:

val obj = new Concrete1.setOption(...).setOption(...)

并仍然将Concrete1作为obj的推断类型。

我不想定义这个:

abstract class Abstract0[T <: Abstract0[T]]

因为这会使客户端处理此类型变得更加困难。我尝试了各种可能性,包括抽象类型:

abstract class Abstract0 {
  type Self <: Abstract0
}

class Concrete1 extends Abstract0 {
  type Self = Concrete1
}

但是在Abstract0中,this没有Self类型,因此无法实现setOption。同时,在Abstract0中使用this: Self =>也不起作用。

那么有哪些解决方案可以解决这个问题呢?


一种选择是定义例如 protected def self = this.asInstanceOf[Self],然后 def setOption(...)= {... ; self },但这看起来有点丑... - Jean-Philippe Pellet
2个回答

66

这就是this.type的作用:

scala> abstract class Abstract0 {
     |   def setOption(j: Int): this.type
     | }
defined class Abstract0

scala> class Concrete0 extends Abstract0 {
     |   var i: Int = 0
     |   def setOption(j: Int) = {i = j; this}
     | }
defined class Concrete0

scala> (new Concrete0).setOption(1).setOption(1)
res72: Concrete0 = Concrete0@a50ea1

正如你所看到的,setOption方法返回的是实际使用的类型,而不是Abstract0。如果Concrete0有setOtherOption方法,那么(new Concrete0).setOption(1).setOtherOption(...)将起作用。

更新:回答JPP在评论中提出的后续问题(如何返回新实例):描述问题的一般方法是正确的(使用抽象类型)。然而,对于每个子类,创建新实例需要显式地进行。

一种方法是:

abstract class Abstract0 {
  type Self <: Abstract0

  var i = 0

  def copy(i: Int) : Self

  def setOption(j: Int): Self = copy(j)
}

class Concrete0(i: Int) extends Abstract0 {
  type Self = Concrete0
  def copy(i: Int) = new Concrete0(i)
}

另一种方法是遵循Scala集合库中使用的建造者模式。也就是说,setOption接收一个隐含的builder参数。这样做的优点是可以使用比'copy'更多的方法来构建新实例,并且可以进行复杂的构建。例如,setSpecialOption可以指定返回的实例必须是SpecialConcrete。

以下是解决方案的示例:

trait Abstract0Builder[To] {
    def setOption(j: Int)
    def result: To
}

trait CanBuildAbstract0[From, To] {
  def apply(from: From): Abstract0Builder[To]
}


abstract class Abstract0 {
  type Self <: Abstract0

  def self = this.asInstanceOf[Self]

  def setOption[To <: Abstract0](j: Int)(implicit cbf: CanBuildAbstract0[Self, To]): To = {
    val builder = cbf(self)
    builder.setOption(j)
    builder.result
  }

}

class Concrete0(i: Int) extends Abstract0 {
  type Self = Concrete0
}

object Concrete0 {
    implicit def cbf = new CanBuildAbstract0[Concrete0, Concrete0] {
        def apply(from: Concrete0) = new Abstract0Builder[Concrete0] {
           var i = 0
           def setOption(j: Int) = i = j
           def result = new Concrete0(i)
        }
    }
}

object Main {
    def main(args: Array[String]) {
    val c = new Concrete0(0).setOption(1)
    println("c is " + c.getClass)
    }
}

更新2: 回复JPP的第二条评论。如果有多层嵌套,请使用类型参数而不是类型成员,并将Abstract0变为特征(trait):

UPDATE 2: 回复JPP的第二次评论。在存在多层嵌套的情况下,请使用类型参数代替类型成员,并将Abstract0改为一个trait:

trait Abstract0[+Self <: Abstract0[_]] {
  // ...
}

class Concrete0 extends Abstract0[Concrete0] {
  // ....
}

class RefinedConcrete0 extends Concrete0 with Abstract0[RefinedConcrete0] {
 // ....
}

非常好的语言特性!我不记得在《Scala编程》一书中看到过它。但现在有一个后续问题:仅当返回this时才有效。如果我想返回另一个相同具体类型的对象,例如用于克隆方法,该怎么办?(我知道在case类中,我可以通过copy获得克隆,但我仍然对这个问题感兴趣。) - Jean-Philippe Pellet
感谢您的跟进解释。最后一个问题。如果Concrete0本身有一个子类RefinedConcrete0,我无法覆盖已在Concrete0中指定的具体类型Self。我假设构建器模式是首选选项?还是还有其他可以替代的方法?我想《More Effective C++》的第33条也可以在这里应用:使非叶类抽象... - Jean-Philippe Pellet
this.typeAbstract0之间的区别是什么?我看不出来。对我来说,这只是按照协变规则工作的原因。 - Debilski
实际上,在我的原始问题中,我并没有打算在Concrete1中实现setOption,而不是Abstract0。 - Jean-Philippe Pellet
@IttayD 感谢您的第二次跟进解释。目前似乎这种方法仍存在一些问题。请参见https://lampsvn.epfl.ch/trac/scala/ticket/4048 - Jean-Philippe Pellet
显示剩余7条评论

7
这正是this.type的确切用法。它会像这样:
def setOption(...): this.type = { 
  // Do stuff ...
  this
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接