抽象类型成员用名字在类/特质里声明类型、子类里再具体化;结构类型按"长什么样"(有哪些成员)而不是"叫什么名字"来匹配类型。两者是泛型之外的另两条类型参数化路径。
trait Storage: type Key // 抽象类型成员 type Value def put(k: Key, v: Value): Unit def get(k: Key): Option[Value] class UserStore extends Storage: type Key = String // 具体化 type Value = User def put(k: String, v: User): Unit = ??? def get(k: String): Option[User] = ???
与泛型对比:Storage[Key, Value] 把参数挂在外面,type Key 把名字藏在里面。当类型成员多、或类型属于类型的"内在属性"时,抽象成员签名更干净;类型从外面注入、组合关系复杂时,泛型更直接。标准库里 Iterable 的 Elem 就是抽象类型成员的真实应用:
trait Iterable[+A]: type Elem // 实际由 A 别名化
两者可以互相表达,选择是可读性之争,不是能力之争。
"只要有 greet 方法的都能传",不要求实现任何接口:
def welcome(x: { def greet(): String }): String = s"welcome, ${x.greet()}" class Person def greet(): String = "hi" class Robot def greet(): String = "beep" welcome(Person()) // OK welcome(Robot()) // OK,Robot 没继承任何共同父类
Scala 3 给了更清晰的别名语法 ({ def greet(): String }) 可写作结构类型。代价不便宜:调用通过反射分派,性能敏感处别用。合理定位是"外部类型的临时缝合"——给两个改不动的第三方类型写一段共用逻辑,而不是日常建模工具。
| 手段 | 声明处 | 使用处 | 适用 |
|---|---|---|---|
| 泛型 | class Box[A] |
Box[Int] |
类型从外部注入、组合 |
| 抽象类型成员 | type A 子类填 |
obj.Key |
类型是家族内在属性、成员较多 |
| 结构类型 | 方法签名里现写 | 任何满足形状的 | 改不动的第三方类型的临时统一 |


背景:一个绘图库要求每个形状类型自带同族的画笔与句柄,泛型参数会传染到所有签名,抽象类型成员能把契约收进类型内部。过程如下:
trait Shape: type Canvas // 抽象类型成员:家族内的"名额",由实现类指定 type Handle def canvas: Canvas def draw(c: Canvas): Handle case class RasterCanvas(w: Int, h: Int) case class RasterHandle(id: Long) case class Square(size: Int) extends Shape: type Canvas = RasterCanvas // 名额落实 type Handle = RasterHandle def canvas = RasterCanvas(800, 600) def draw(c: RasterCanvas) = RasterHandle(c.w.toLong + size)
Square 的使用方拿到的 draw 签名是具体类型,无需再写 Square[RasterCanvas, RasterHandle] 这种泛型雪球。解读:泛型参数是"外部传入",抽象类型成员是"内部声明",适合类型只在该家族内部流转的场景;Spark 的 Dataset 内部正是用抽象类型成员承载编码器信息。
type Closable = { def close(): Unit } def withResource[R <: Closable, T](r: R)(body: R => T): T = val t = body(r) r.close() t
结构类型按"长得像"匹配,不必继承任何公共接口——Java 互操作时对没有接口的老类特别顺手。代价是每次调用走反射,且 Scala 3 默认需要打开语言特性开关(import scala.language.reflectiveCalls),官方态度已经很明确:能用类型类(5.4 节)就别用结构类型。
// 泛型版 trait Repo1[A]: def get(id: Long): Option[A] // 抽象类型成员版 trait Repo2: type E def get(id: Long): Option[E] class UserRepo extends Repo2: type E = User def get(id: Long) = Some(User(id))
两者表达力等价,选择标准看"类型由谁指定":调用方传入用泛型,实现方指定用抽象成员。经验倾向:暴露给外部的库 API 用泛型(调用方一眼看懂签名);内部模块边界与家族契约用抽象成员(签名干净、不扩散类型参数)。Scala 集合库的老版本曾把两种写法混用导致签名爆炸,这段历史是最好教材。
顺带一个边界提醒:抽象类型成员不能像泛型参数那样自由标注型变,需要协变逆变时优先回到泛型参数——两种工具各管一段,别强行互相替代。
结构类型的另一个边界是编译期检查只看签名匹配,重名方法语义不同也会误命中,跨团队共用时务必配文档说明预期形状。
Iterable 的 Elem 是抽象类型成员的标准库活例。