2.1 类、对象与样例类


2.1 类、对象与样例类

用主构造器直接声明字段与方法;object 是单例;样例类(case class) 是编译器替你生成相等性、拷贝与解构能力的不可变数据载体。本节是 Scala 数据建模的地基。

主构造器就写在类名后面

class Account(val owner: String, var balance: BigDecimal) { def deposit(amount: BigDecimal): Unit = balance = balance + amount def canAfford(price: BigDecimal): Boolean = balance >= price } val acc = new Account("Wang", 100) acc.deposit(50) acc.balance // 150

参数前的 val/var 决定它是否成为字段:val owner 只读,var balance 可变。什么都不写则只是构造参数,类外不可见。相比 Java 的"字段 + getter + setter + 构造器"三件套,一行顶十几行。

类体里还可以有辅助构造器,但 Scala 风格更倾向用工厂方法(见下面伴生对象)。

object:不需要 new 的单例

object RateTable { val standard: BigDecimal = 1.13 def apply(net: BigDecimal): BigDecimal = net * standard } RateTable(200) // 226.0 —— apply 让对象像函数一样调用

与类同名的 object 叫伴生对象,能访问类的私有成员,惯例上放工厂方法与常量。Scala 没有 static 关键字,静态成员全部住在伴生对象里——这让"一切皆对象"在语言层面没有例外。

样例类:数据建模的主力

case class Point(x: Int, y: Int) val p1 = Point(1, 2) val p2 = Point(1, 2) p1 == p2 // true,按值比较 p1.copy(y = 10) // Point(1, 10),改一列其余照抄 val Point(a, b) = p1 // 解构,a=1, b=2

样例类自动获得:按值的 equals 与 hashCode、可读 toString、copy、伴生对象里的 apply(所以不需要 new)与 unapply(供模式匹配解构)。注意 p1 == p2——普通类比的是引用,样例类比的是内容,这个差别在往集合里放、做去重时立刻显现。

什么时候用普通类

场景 选择 原因
纯数据(消息、配置、事件) 样例类 相等性、拷贝、模式匹配白送
有身份的实体(账户、会话) 普通类 判等按引用,状态可变
无数据的类型标记 样例对象 全局唯一,模式匹配可作分支

样例对象示例:case object Idle,常与样例类组成状态机(3.3 节会大量使用)。

枚举的两种写法

Scala 3 的 enum 是样例类的语法糖衣:

enum Color(val rgb: Int): case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF)

图 2-1 class、object 与 case class 的分工

图 2-1 class、object 与 case class 的分工

完整案例:用样例类建模一笔支付

来看一个贴近真实的建模过程。背景是支付网关需要对账,每笔支付可能是成功、失败或待确认三种状态,且要携带不同的附加信息。面向对象的直觉是建一个 Payment 基类加三堆字段,样例类的答案干净得多:

enum PaymentState: case Succeeded(txnId: String, at: Long) case Failed(code: Int, reason: String) case Pending(expiresAt: Long) import PaymentState.* case class Payment(id: Long, amount: BigDecimal, state: PaymentState): def isSettled: Boolean = state match case Succeeded(_, _) => true case _ => false val p1 = Payment(1001, 59.9, Succeeded("TX-8", 1721000000L)) val p2 = p1.copy(amount = 69.9) // 基于旧值造新值,id 与 state 原样保留

操作过程只有三步:声明枚举与样例类、用 match 分支处理状态、用 copy 做无副作用修改。结果是编译器免费送了 equals、hashCode、toString、模式匹配支持与不可变复制;解读一下,样例类的本质是"代数数据类型"——状态空间被类型完整穷尽,少写一个分支编译器直接警告。变式也简单:若状态会随时间流转,把新状态做成新值(p1.copy(state = Pending(...))),旧值留作审计痕迹,这正是第三章不可变思想在对象世界的预演。

编译器送的四件套怎么验证

case class Money(cents: Int) val a = Money(500) val b = Money(500) a == b // true:按值比较,不是按引用 Money(500).hashCode == Money(500).hashCode // true:可安全放进 Set/Map println(a) // Money(500):可直接打印

普通类这三条全部相反:== 比引用、hashCode 按身份、打印是地址。经验法则随之确立——数据用 case class,有身份概念的实体(如活跃的连接对象)才用普通 class。

常见编译错误:unapply 不是无条件可用

把样例类实例 match 到两个字段但只声明了一个,报 pattern mismatch;或者对普通类的实例用样例类风格的模式匹配,直接找不到 extractor。排错方向就一句:模式匹配的字段形状必须与构造形状一致。

本节要点回顾

  • 主构造器写在类名后,val/var 决定字段可见性与可变性。
  • object 承担静态职责,apply 提供函数式调用外观。
  • 样例类按值判等、免 new、可 copy 可解构,是数据建模默认选择。
  • enum(Scala 3)统一了旧 Java 枚举与密封继承层级两种需求。

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