2.2 继承、特质、包与导入


2.2 继承、特质、包与导入

trait(特质) 是可带具体实现的接口,一个类只能单继承但可混入多个特质;包与导入决定代码的组织与可见性。本节合并讲完对象系统的剩余骨架。

先看一个问题

日志能力、重试能力、指标上报能力,如果都用继承实现,一个"可重试且可上报且可记录的服务"需要 2 的 3 次方个类组合——继承爆炸。特质就是解法:

trait Logger: def log(msg: String): Unit = println(s"[LOG] $msg") trait Retriable: def maxRetry: Int = 3 def runWithRetry[A](body: => A): A = ??? class SyncService extends Logger, Retriable: def sync(): Unit = log("start sync")

类只能 extends 一个父类,但可以混入任意多个特质(Scala 3 用逗号分隔,替代旧版的 with)。

继承爆炸 vs 特质混入

继承爆炸 vs 特质混入

混入顺序有讲究:特质按声明顺序线性化,后面的特质先初始化、其 super 调用指向更早的特质。设计"过滤链"时这个顺序就是过滤的次序。

trait Base: def render: String trait A extends Base: override def render = s"A(" + super.render + ")" trait B extends Base: override def render = s"B(" + super.render + ")" class Chain extends A, B // 线性化:B 在 A 外层 (new Chain).render // "B(A(Base))"

抽象成员与密封层级

abstract class Shape: def area: Double // 抽象方法 sealed trait Event // 密封:子类型只能在本文件定义 case class Click(x: Int, y: Int) extends Event case class KeyPress(code: Int) extends Event case object Idle extends Event

sealed 告诉编译器"子类型清单是封闭的",于是模式匹配可以穷举检查——漏一个分支直接编译警告(Scala 3 甚至报错)。这是"让非法状态不可表示"的第一件武器,第五章还会升级。

包与导入

package com.shop.order // 文件顶部声明 import com.shop.pay.* // 通配导入 import com.shop.pay.Currency as Cur // 改名,避免冲突 import java.time.{LocalDate, LocalDateTime} // 选择性导入

Scala 的 import 可以出现在任意作用域里,甚至函数体内——只在那段代码可见,缩小名字污染。默认每个文件都已导入 scala.*java.lang.* 与所在包。

可见性默认全公开;private 限本类/伴生对象,protected 限子类,包私有写作 private[order](order 为包名)——比 Java 的默认包可见更精细。

特质 vs 抽象类怎么选

维度 trait abstract class
多重继承 可混入多个 只能单继承
构造参数 不能带 可以
适用 能力组合 有字段的模板基类

我的默认:先用 trait,需要构造参数或强模板时才用抽象类。

图 2-2 特质线性化的堆叠顺序

图 2-2 特质线性化的堆叠顺序

特质叠加:钻石问题的裁决现场

一个类混入多个特质、且多个特质都改写同一方法时,调用顺序由 linearization(线性化)决定。用一段完整代码把过程演出来:

trait Base: def msg: String = "base" trait Upper extends Base: override def msg = s"[${super.msg}.upper]" trait Bang extends Base: override def msg = s"[${super.msg}bang]" class Widget extends Base, Upper, Bang // 混入顺序从左到右 Widget().msg // 结果:[[base.upper]bang]

推演:Bang 最后混入所以最外层,它的 super 指向 Upper,Upper 的 super 指向 Base。于是输出从内到外层层包裹。与 C++ 的虚继承不同,Scala 给出确定性的顺序——线性化把继承图压成一条链,super 沿链上溯。当两段代码库的特质意外碰撞时,读输出就能反推出混入顺序是否写反。

用特质做能力切面

trait Logged: def log(msg: String): Unit = println(s"[log] $msg") trait Audited extends Logged: def audit(action: String): Unit = log(s"audit: $action") class OrderService extends Audited: def place(n: Int): Unit = audit(s"place order $n") OrderService().place(3) // [log] audit: place order 3

这里的能力不是字段而是行为切片:审计依赖日志,服务混入审计即同时获得两者。相比 Java 接口的默认方法,特质可以带状态、可以互相叠加,是组合复用的主战场。

包与导入的三个实用细节

  • 悬挂导入可以出现在任意位置,作用域只到当前块结束——缩小 import 的射程,避免命名冲突。
  • Scala 3 的顶层定义自动属于所在文件声明的包,不需要 package object 这个旧概念了。
  • 重命名与排除:import scala.collection.{MutableSortedMap => MSM, SortedMap => _},前者起别名,后者把某个名字从视野里剔除,专门对付同名冲突。

本节要点回顾

  • trait 解决继承爆炸,混入顺序即线性化顺序,super 链按它解析。
  • sealed 关闭继承层级,让编译器替你穷举检查匹配分支。
  • import 可局部化、可改名、可选择,private[包名] 提供细粒度可见性。
  • 选型:trait 优先,抽象类留給需要构造参数的模板。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U