引言
Scala 类型系统是 JVM 上最强类型系统之一,也是「为什么写 Scala 不容易出错」的底气。它允许你用类型本身来表达领域约束:Option[Int] 告诉你「可能没有值」、Either[String, Order] 告诉你「失败带原因」、case class 建模出的 ADT 让非法状态根本编译不过。
本文从四个层次系统理解 Scala 类型系统:泛型(抽象出通用的类型结构)、ADT 与 case class(用类型建模领域)、协变/逆变(为什么 List[Int] 是 List[AnyVal] 的子类型)、类型类与隐式(为类型附加行为)。每一层都用「如何帮你写出更安全的代码」来落地。
前置:函数式基础(https://plumephp.com/scala-functional-programming/)。本文的类型类模式与隐式推导是该文的深化。
目录
- 1. 类型系统的价值:用类型阻止 bug
- 2. 泛型:参数化类型与方法
- 3. ADT:用代数数据类型建模领域
- 4. case class 与模式匹配
- 5. 协变、逆变与不变
- 6. 类型边界:上下界约束
- 7. 类型类:为类型附加行为
- 8. 路径依赖与内部类型(进阶)
- 9. 总结:类型系统实战清单
- 延伸阅读
1. 类型系统的价值:用类型阻止 bug
1.1 类型即文档、类型即契约
// 类型声明就说明了一切
def processOrder(orderId: Long): Either[OrderError, Order]
// 读签名即知:可能失败、失败带原因、成功返回 Order
1.2 编译时 vs 运行时
| 维度 | 弱类型 | Scala 强类型 |
|---|---|---|
| 检查时机 | 运行时报错 | 编译期拦截 |
| 空指针 | 常见 | Option 消除 |
| 类型混用 | 隐式转换出错 | 编译拒绝 |
1.3 让非法状态无法表达
// ❌ 用 Boolean 表达状态:可漏
def validate(x: Int): Boolean
// ✅ 用密封类型:状态穷尽
sealed trait ValidationResult
case object Valid extends ValidationResult
case class Invalid(reason: String) extends ValidationResult
2. 泛型:参数化类型与方法
2.1 泛型类
case class Box[A](value: A)
val intBox: Box[Int] = Box(42)
val strBox: Box[String] = Box("hello")
2.2 泛型方法
def headOr[A](list: List[A], default: A): A =
list.headOption.getOrElse(default)
headOr(List(1,2), 0) // 1
headOr(List.empty, 0) // 0
2.3 泛型约束:让类型更精确
// 约束 A 必须有序,才能 max
def maxOf[A: Ordering](list: List[A]): A =
list.max
// 约束 A 是 Number 的子类
def sumNumbers[A <: Number](list: List[A]): Double =
list.map(_.doubleValue).sum
3. ADT:用代数数据类型建模领域
3.1 什么是 ADT
ADT(Algebraic Data Type)由「和类型(sealed trait)」与「积类型(case class)」组合而成:
sealed trait Payment
case class Card(number: String, exp: String) extends Payment
case class BankTransfer(account: String) extends Payment
case object Cash extends Payment
- 和类型:
Payment是 Card、BankTransfer、Cash 之一。 - 积类型:
Card同时持有 number 与 exp 两个字段。
3.2 ADT 的价值
- 穷尽匹配:
match编译器检查所有 case。 - 非法状态不可表达:不能造出「既是 Card 又是 Cash」的值。
- 可扩展:新增一种支付方式,编译器强制你处理。
3.3 模式匹配 + sealed 的穷尽检查
def fee(p: Payment): Double = p match {
case Card(_, _) => 0.5
case BankTransfer(_) => 1.0
case Cash => 0.0
// sealed + match 穷尽:少一个 case 编译报错
}
4. case class 与模式匹配
4.1 case class 自动提供的能力
case class Order(id: Long, items: List[Item])
// 自动:equals/hashCode/toString/copy/解构/构造器
val o1 = Order(1, Nil)
val o2 = o1.copy(id = 2) // 复制并改部分字段
val Order(id, items) = o2 // 解构
4.2 值相等 vs 引用相等
case class 默认值相等:Order(1, Nil) == Order(1, Nil) 为 true。普通 class 是引用相等。这让 case class 天然适合值对象建模。
4.3 在集合与匹配中使用
val orders = List(Order(1, List("a")), Order(2, Nil))
orders.collect {
case Order(id, items) if items.nonEmpty => s"订单$id 有商品"
}
5. 协变、逆变与不变
5.1 三种变化
| 类型 | 定义 | 直觉 |
|---|---|---|
协变 +A | List[Int] 是 List[AnyVal] 子类型 | 只读容器适合 |
逆变 -A | Function1[A,B] 参数逆变 | 输入方向 |
不变 A | 严格类型,MutableList[Int] ≠ MutableList[AnyVal] | 可变容器 |
5.2 协变的例子
// List 是协变的:List[Int] <: List[AnyVal]
val ints: List[Int] = List(1, 2)
val anys: List[AnyVal] = ints // ✅ 合法
// Array 是不变的:不能这样赋值
val arrInt: Array[Int] = Array(1)
// val arrAny: Array[AnyVal] = arrInt // ❌ 编译错误
5.3 为什么可变容器必须不变
// 若 Array 协变:
val arrInt: Array[Int] = Array(1)
val arrAny: Array[AnyVal] = arrInt // 假想合法
arrAny(0) = "hello" // 把字符串放进 Int 数组 → 运行崩溃
// 所以 Scala 让 Array 不变,编译期就拦住
5.4 方差与类型安全
要点:协变安全的前提是「只读不写」。凡可写入的类型参数必须不变。
6. 类型边界:上下界约束
6.1 上界(A <: B)
// A 必须是 Animal 的子类型
def feed[A <: Animal](a: A): Unit = a.eat()
6.2 下界(A >: B)
// 协变容器里写值要用下界避免类型漏洞
class Container[+A](private val value: A) {
def set[B >: A](newVal: B): Container[B] = new Container[B](newVal)
}
6.3 上下界结合
// 经典案例:排序时允许更宽的类型
def sortPets[A <: Pet](pets: List[A]): List[A] = ...
7. 类型类:为类型附加行为
7.1 传统面向对象的多态 vs 类型类
// OO 方式:类型自身实现方法
trait JsonSerializable { def toJson: String }
case class User(n: String) extends JsonSerializable
// 类型类方式:行为与类型分离,可扩展已有类型
trait JsonWriter[A] { def write(a: A): String }
given userWriter: JsonWriter[User] = u => s"""{"name":"${u.n}"}"""
given intWriter: JsonWriter[Int] = i => i.toString
7.2 类型类的优势
| 维度 | 继承多态 | 类型类 |
|---|---|---|
| 给已有类型加行为 | ❌ 无法修改 | ✅ given 即可 |
| 类型间自由组合 | 受限 | 灵活 |
| 对类型约束 | 侵入 | 无侵入 |
7.3 实战:泛型函数约束类型类
def toJsonString[A](a: A)(using w: JsonWriter[A]): String =
w.write(a)
toJsonString(User("alice")) // 自动找 given userWriter
toJsonString(42) // 自动找 given intWriter
7.4 类型类生态:Cats、Circe
// Circe:JSON 编解码的类型类
import io.circe._, io.circe.syntax._
val json = User("alice").asJson // 自动编码
8. 路径依赖与内部类型(进阶)
8.1 路径依赖类型
class Database {
class Connection
def connect: Connection = new Connection
}
val db1 = new Database
val db2 = new Database
// db1.Connection 与 db2.Connection 是不同类型
// val c: db2.Connection = db1.connect // ❌ 类型不匹配
8.2 用途
路径依赖类型用于表达「某个实例相关的类型」:如类型安全的资源句柄、依赖注入的精确类型。
8.3 何时值得用
大多数业务代码用不到;但理解它有助于读懂框架源码(如 Akka 内部)。
9. 总结:类型系统实战清单
9.1 类型系统四层武器
| 层 | 工具 | 解决的 bug |
|---|---|---|
| 泛型 | 参数化类型 | 重复代码、类型混用 |
| ADT | sealed trait + case class | 非法状态 |
| 方差 | 协变/逆变 | 可变容器写穿类型 |
| 类型类 | given/using | 行为扩展与解耦 |
9.2 实战建议
| 建议 | 理由 |
|---|---|
| 领域建模用 sealed trait | 穷尽匹配、非法状态不可表达 |
| DTO 用 case class | 值相等、copy、解构 |
| 可变容器注意方差 | 只读用协变、可变必须不变 |
| 行为扩展用类型类 | 无侵入、可组合 |
9.3 一句话心法
Scala 类型系统的目标是「让编译期替你挡掉大部分 bug」:学会用 ADT 建模、用类型类扩展、理解方差约束,你写出的代码会「不合法就不编译」。
延伸阅读
- https://plumephp.com/scala-functional-programming/ — Option/Either 的类型结构与模式匹配
- https://plumephp.com/scala-collections/ — 协变在集合库中的体现
- Scala 类型系统官方文档 与 Cats 文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。