PAYATHON 2026

Scala 中如何在 trait 或 abstract class 中使用自引用泛型和 implicit?

技术老齐

给 Record 增加类型参数 R,用 self-type 声明当前对象也具有类型 R,再为公共实现提供 Encoder[R],就可以统一实现编码方法。

在 Scala 2.13 中,最直接的做法是使用带 implicit 构造参数的 abstract class。如果要保留 trait,就需要通过抽象成员提供编码器,因为 Scala 2 的 trait 不支持构造参数。

为什么直接移动方法会报错

Encoder.encode 的签名是:

def encode[A](obj: A)(implicit enc: Encoder[A]): String

在原来的非泛型 Record 中:

sealed trait Record {
  def encode: String = Encoder.encode(this)
}

编译器根据 Record 中已知的类型信息检查方法体,因此会寻找 Encoder[Record]。即使运行时对象是 A,也不会重新搜索 Encoder[A]。

这里 Encoder 的类型参数是不变的,所以 Encoder[A] 和 Encoder[B] 都不能直接当作 Encoder[Record] 使用。

使用 abstract class 统一实现

将 Record 改为带类型参数和 implicit 构造参数的抽象类:

import com.github.atais.fixedlength.Encoder
import com.github.atais.fixedlength.Encoder.fixed

object FixedLengthIssue {
  sealed abstract class Record[R <: Record[R]](
      implicit enc: Encoder[R]
  ) { self: R =>
    final def encode: String =
      Encoder.encode[R](self)
  }

  final case class A(a: String, b: Int) extends Record[A]

  final case class B(a: String, b: Int) extends Record[B]

  implicit val encoderA: Encoder[A] =
    (fixed[String](0, 5) <<: fixed[Int](5, 10)).as[A]

  implicit val encoderB: Encoder[B] =
    (fixed[String](0, 6) <<: fixed[Int](6, 10)).as[B]

  def main(args: Array[String]): Unit = {
    val records: Seq[Record[_]] =
      Seq(A("a1", 1), B("b2", 2), B("b3", 3), A("a4", 4))

    val output = records.map(_.encode).mkString("\n")
    println(output)
  }
}

编码器的定义与题目中相同,调整的是记录类型的继承结构。

类型参数与 self-type 各自的作用

R <: Record[R]

这是 F-bounded polymorphism 的写法,要求 R 本身也是对应的记录类型。例如,A 继承 Record[A]。

这个上界还不能证明当前对象具有类型 R,因此需要加上 self-type 声明:

self: R =>

它要求混入或继承该实现的对象同时符合 R。这样,方法体中的 self 就可以作为 R 传给编码器,无需类型转换。

构造参数为公共方法提供所需的编码器:

implicit enc: Encoder[R]

因此,下面的调用:

Encoder.encode[R](self)

等价于显式传入 enc:

Encoder.encode[R](self)(enc)

也可以直接写成 enc.encode(self)。

implicit 在哪里解析

编译 A 的父类构造调用时,R 已确定为 A,编译器会寻找 Encoder[A];对于 B,则寻找 Encoder[B]。选中的编码器会传给父类,供后续调用 encode 时使用。

编码器必须能在子类的父类构造调用处通过 implicit 搜索找到。示例把它们放在同一个外围对象中,也可以放在对应类型的伴生对象中。

Seq[Record[_]] 可以容纳不同的记录类型。每个记录对象已经持有自己的编码器,所以调用 .encode 时无需再查找 implicit。

保留 trait 的写法

Scala 2.13 不支持给 trait 添加构造参数,下面的写法不能使用:

trait Record[R](implicit enc: Encoder[R])

可以改用抽象 implicit 成员。用下面的定义替换前例中的 Record、A 和 B,编码器定义保持原样:

sealed trait Record[R <: Record[R]] { self: R =>
  protected implicit def recordEncoder: Encoder[R]

  final def encode: String =
    Encoder.encode[R](self)
}

final case class A(a: String, b: Int) extends Record[A] {
  override protected implicit def recordEncoder: Encoder[A] =
    encoderA
}

final case class B(a: String, b: Int) extends Record[B] {
  override protected implicit def recordEncoder: Encoder[B] =
    encoderB
}

子类只需提供编码器,编码逻辑仍集中在 Record 中。如果希望子类完全不写方法体,前面的 abstract class 方案更合适。

注意事项

不要只把公共方法改成:

def encode(implicit enc: Encoder[R]): String

这会把 implicit 搜索推迟到每次调用处。对于 Seq[Record[_]],调用方不知道每个元素的具体类型,通常无法提供对应的 Encoder。

self-type 只提供类型约束,不会自动生成编码器,每种具体记录仍需有对应的 Encoder。

示例中的编码器会在进入 main 前完成初始化。如果调整对象的初始化顺序,应避免在编码器尚未初始化时构造记录,也不要在父类构造期间调用编码方法。

备注:内容仅供参考。