Scala ADT、模式匹配、错误与泛型
前置:表达式和函数。目标是让错误和状态有可检查结构,而不是学会尽可能多的符号。
case class、enum与模式匹配
case class表示包含若干字段的乘积类型,例如Entry(name,amount),每项同时存在;enum各case表示和类型,例如Invalid(line)或TooMany只取其中一种。模式匹配按构造器分解,编译器可以对封闭类型检查是否漏分支。不要用一个Map[String,Any]和约定字符串替代有限的领域状态,否则字段拼错、分支遗漏都拖到运行时。
以下完整示例可保存Model.scala:
enum Load:
case Ready(count: Int)
case Failed(reason: String)
object Model:
def describe(value: Load): String = value match
case Load.Ready(n) if n > 0 => s"loaded=$n"
case Load.Ready(_) => "empty"
case Load.Failed(reason) => s"failed=$reason"
def main(args: Array[String]): Unit =
println(describe(Load.Ready(3)))输出loaded=3。带guard的分支只覆盖符合条件的一部分,所以仍要Ready(_)处理剩余情况。case class提供字段值相等、copy等方法,但字段若含可变Array并不会变成深度不可变,而且Array相等也不能假定自动逐元素。==是空安全的相等调用,eq用于引用身份;领域比较要按实际字段类型设计。
Option、Either、Try和异常
Option[A]表达可能没有值,没有原因;Either[E,A]表达失败原因E或成功A;Try[A]捕获可恢复异常并包装Success/Failure。它们不能互换名称却声称语义不变。数据库未查到可用None,格式不符合需要行号可用Left(Invalid(line)),文件系统抛异常可在边界Try或Using捕获;不要将所有失败都压成None。
Either右偏,map/flatMap只处理成功路径,for推导式可顺序组合多个可能失败步骤。对于独立字段需要累计多个错误,应另定聚合结构;顺序flatMap默认首错停止。本项目summarize返回Either[Problem,String],第一次格式错误立即停止迭代,Tests用计数器确认坏首行后没有读取第二行。避免遍历全部后只取第一个错误却仍称“首错停止”。
不要对Option随意.get,None将抛NoSuchElementException;使用match、fold或有语义的默认值。异常恢复使用NonFatal类别或Try/Using的标准约定,不把JVM致命错误等同于一条坏数据。资源关闭失败与正文失败可能同时发生,Using会按标准规则选择主异常并保留被抑制异常,诊断时需看整个异常链。
泛型、变型、opaque与given
List[A]让集合元素类型可重用;协变+A允许List[String]当List[Any]只读使用,但可变容器通常不能如此宽化,否则写入会破坏元素类型。逆变-A适合只消费A的接口;变型只说明子类型关系,不让任意泛型运行时验证更强。JVM泛型擦除意味着不能依靠isInstanceOf[List[String]]验证所有元素,外部数据仍需逐项检查。
opaque type可在作用域边界隐藏底层表示,例如定义Amount而只暴露校验构造与求和,避免普通Int被误当金额。它不是加密、不是自动范围校验;工厂必须执行检查。本课程项目用Entry配parse保证外部输入,公开构造器仍能被内部代码直接调用,因此不宣称所有Entry在任何调用路径都合法。
given/using提供类型相关的上下文能力;例如Show[Int]决定如何渲染Int,render[A](value)(using Show[A])明确要求能力而不继承数据类。JVM实验的Lab.scala给出可运行trait、given与render,不需额外库。若编译器报告没有given,先检查能力类型及导入作用域;不要用隐式转换强行把不相关类型揉在一起。过度抽象会让错误信息更难懂,一次性解析不必引入完整类型类框架。
练习与独立解答
- 给Load新增Pending,观察没有对应case的警告;为什么只有带guard的Ready分支不足?2. 将查找结果从Option改Either时需要补什么信息?3. 解释List协变和可变Array的差别。4. 对所有行先map(parse).toList再取错误,会有什么运行与内存差异?
参考解答
新增状态需补穷尽分支,guard只满足部分值。Either必须设计失败类型而非仅把None改为Left("")。只读生产类型允许协变,能写的容器要保护元素不变量。提前toList会消费全部输入、分配所有结果并延长资源占用;我们的循环按首错停止,只保留分类聚合。若任务需要收集所有错误,应明确换成全量校验契约,并限制错误数量,而不是误标原契约。
继续阅读
- 开发实践
- 类型、表达式与建模
- 类型、表达式与建模