Scala 11:Iterator、View 与 LazyList,延迟之后发生什么
同一份变换读两次,为什么有时没数据,有时又算一遍 把订单行转换成金额,只取前两行,然后再次读取。Iterator 可能从第三行继续,View 可能重新执行前两行的变换,LazyList 则可能复用已经计算的结果。它们都能写成 map 后接 take,但“延迟”没有说明消费位置、缓存和对象保留的差异。 这个问题不能只用返回列表是否相等来回答。两个结果一样,回调可能执行两次;第二次结果为空,可能是游标已经耗尽;回调没有重跑,可能是结果被缓存并继续占用内存。需要同时观察值、执行次数和引用关系,才能判断某个接口是否适合订单预览、重复报价或持续流式读取。 本章固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21。测试用有限消费控制无限来源,用计数器观察计算,用对象身份观察记忆化。本章没有堆转储或 GC 回收实验,因此不报告释放字节数,也不把保留引用的推导冒充实测泄漏规模。 Iterator 保存消费位置 12345678910var calls = 0val it = List(1, 2, 3).iterator.map { n => calls += ...
Scala 10:不可变集合,更新如何保留旧版本
修改一行数量,旧报价为什么还应该有效 订单编辑页保存了一份数量列表,报价计算正在使用它。用户又修改第二行数量,如果这两个操作共享可变数组,报价读到的内容可能取决于修改时点。不可变集合提供另一种接口:更新返回新集合,旧集合继续表示修改前的版本。 这不意味着每次更新都完整复制,也不意味着集合里的对象不会改变。它只先回答一个结构问题:对集合执行 updated、添加或删除后,旧集合的元素排列与索引关系是否仍保持原样。进一步的共享范围、元素可变性和操作代价,需要分别判断。 本文固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21,比较 List、Vector、Map、Set。实验检查结果、版本独立性和能够直接观察的共享关系,不把对象身份断言扩展成完整内存测量,也不从小输入的运行速度推导性能排名。 先看访问方式,再选集合形状 List 适合不断在头部增加元素,并顺序消费头尾。Vector 提供索引访问与持久更新,适合需要按位置处理的序列。Map 用键寻找值,Set 表达成员关系和去重。这些差异比“它们都支持 map”更能决定订单数据怎样组织。 如果订单行总是按顺序扫描,L...
Scala 09:Option、Either 与 Try,给失败保留含义
同一个 bad,为什么会得到三种失败 订单数量字段收到字符串 “bad”。转换成整数当然失败,但业务接口究竟应返回“没有值”、“数量格式不合法”,还是一个 NumberFormatException?这不是三个容器之间的语法偏好,而是调用方需要获得什么信息,以及失败发生在哪个边界。 Option[Int] 只有存在与缺失两种情况;Either[Rejection, Int] 可以保存结构化业务原因;Try[Int] 保存成功结果或被捕获的异常。三个类型都能参与 map、flatMap 组合,不代表它们表达相同契约。过早互转还可能不可逆地删除信息:把任何 Left 变成 None 后,调用方无法恢复它原来是缺失、格式错误还是非正数。 本章固定 Scala 3.3.7、标准库 2.13.16 和 JDK 21,使用同一数量解析过程比较三个选择。实验不使用 get 作为正常消费方式,也不把异常捕获当作程序正确性的替代。类型保证成功与失败的形状,具体哪类失败应被翻译成哪种业务结果,仍需要接口设计。 Option 表达没有值,不替缺失说明原因 12val parsed: Option[In...
Scala 08:模式匹配与提取器,分支覆盖检查了什么
新增已发货状态后,旧展示函数还能通过吗 订单状态原本只有 Draft 和 Paid,页面展示函数也只有两个分支。随后模型增加了 Shipped,但展示代码没有变化。程序应在构建时发现遗漏,还是等用户打开已发货订单时才抛出 MatchError?这取决于匹配器能看到的类型、现有分支和编译选项。 模式匹配同时涉及几种容易混在一起的工作:根据类型或值选择分支,把数据拆成局部绑定,执行守卫条件,以及检查已知输入是否都被覆盖。它们使用同一种语法,不代表提供相同强度的保证。一个可以编译的 match 可能仍然有运行时失败,一个看起来写全类型的模式也可能只检查了擦除后的外层形状。 本章使用 Scala 3.3.7、标准库 2.13.16 和 JDK 21。订单状态承接前篇的三个分支,输入数量则来自字符串。实验把遗漏分支和泛型元素匹配分别放进独立编译反例,把提取成功、失败和元素逐个检查放进正常程序。 分支由上到下选择,解构之后才执行守卫 12345678910enum State: case Draft case Paid(receipt: String) case Shipped(tra...
Scala 07:ADT 与 enum,把订单状态变成有限分支
未付款订单为什么会带着物流单号 考虑一份用字符串表示状态的订单: 12case class Order(status: String, receipt: Option[String], tracking: Option[String]) 这个模型可以表示已经付款但没有收据、尚未付款却带着物流单号,以及状态拼错成 "paidd" 的记录。构造器只检查三个参数各自的类型,没有检查它们之间的关系。增加三个条件判断可以拦住一部分输入,但下一处直接调用构造器的代码仍可能重新制造矛盾。 问题不只是字符串容易拼错。三个字段是彼此独立的选择:状态可以取很多字符串,两个可选字段又各有存在与不存在两种形状,组合空间远大于业务允许的状态空间。修复的方向是先列出有效形状,再决定每种形状必须携带哪些信息。本文使用草稿、已付款、已发货三个阶段;取消、退款和部分发货暂不属于这份模型,遇到相关命令时明确拒绝。 实验固定 Scala 3.3.7、标准库 2.13.16、JDK 21 和 Scala CLI 1.9.1。这里讨论的是有限分支建模与状态转换;订单存在...
Scala 06:泛型边界与型变
一条子类型关系能传播到哪里 创建订单命令 Create 是命令 Command 的一种,因而一个 Create 值可以传给需要 Command 的方法。但这不能直接推出 Cell[Create] 可以传给需要 Cell[Command] 的方法。若那个单元格允许写入取消订单命令,原先持有它的代码再按 Create 读取,就会得到类型承诺之外的值。 泛型型变讨论的正是这种传播关系:已知 Create <: Command,把两者放入同一个泛型类型后,替换方向保持、反转,还是没有由元素关系导出的替换关系。+A、-A 是对整个接口如何使用类型参数的承诺,编译器会检查这些承诺能否成立。Scala 官方型变教程 本篇使用订单命令的生产者、消费者、函数与批次容器,逐个检查调用者实际能做什么。冻结环境为 Scala 3.3.7、Scala CLI 1.9.1、Amazon Corretto JDK 21.0.11。实验状态为 LAB_VERIFIED:本篇断言已在 Scala CLI 和 sbt 两条入口通过,五个独立反例均按对应类型错误拒绝。批次证据位于 examples/scala-...
Scala 05:trait 线性化与初始化
两种顺序决定不同的问题 订单金额处理器同时混入“结果加一”和“结果翻倍”两个 trait。两种写法只交换 with 后的名字,输入同样的整数 10,结果却可能分别是 22 和 21。构造时哪个 trait 先执行,与调用方法时先进入哪个实现,也不能靠同一条“从左到右”口诀回答。 这里用整数作为顺序实验的载体,不建立金额计算模型。真实订单金额仍须声明币种、精度与舍入规则。实验的核心问题是:一个组合对象上执行的 super.total 到底连接哪个实现,以及执行那个实现时对象的字段是否已经赋值。 Scala 的线性化给继承图排出一个确定序列,用于成员解析和 trait 中的 super 连接。初始化还要遵守父类构造与 trait 初始化规则。两者有关,却回答不同问题;继承次序正确,不能推出对象构造期间读取的字段已就绪。Scala 3.3.7 规范中的类线性化与模板求值 本篇冻结 Scala 3.3.7、Scala CLI 1.9.1、Amazon Corretto JDK 21.0.11。实验状态为 LAB_VERIFIED:Scala CLI 与 sbt 两条入口均运行了本篇断言,...
Scala 04:类、对象与值相等
copy 之后,修改为何仍然影响原对象 两个账户都写着编号 A-1、余额 100,是否就应该相等?一张订单复制后修改备注,原订单的备注是否应该保持不变?这两个问题都无法仅凭字段内容回答。账户可能表示带生命周期的实体,订单可能表示某个时刻的数据值;而“复制”还必须交代复制了哪一层。 Scala 的 class 提供实例结构,object 定义单例对象,case class 为数据建模提供生成的方法。选择哪种声明,会影响构造方式、公开接口和相等约定,却不会替业务决定哪些数据必须保持独立。 本文固定 Scala 3.3.7、JDK 21、Scala CLI 1.9.1,示例仅使用标准库。完整程序位于 examples/scala-lab/snippets/04/Chapter04.scala,实验状态为 LAB_VERIFIED:本章断言已在 Scala CLI 与 sbt 的批次运行中通过,输出与下文一致。证据位于仓库 examples/scala-lab/evidence/20261002-batch01/all.log、sbt.log,对应 JSON 保存实际命令与零退出码。附件...
Scala 03:方法、函数与闭包
同一个价格策略为什么会算出不同结果 一件商品标价 200 分,九折后应付 180 分。若先创建价格策略,再把外部折扣从 10 改为 20,同一个策略调用应返回 180,还是 160?答案取决于函数体读取的是哪个绑定。把一段计算保存为函数值,并不会自动把它使用的全部数据保存成快照。 实验限定为 Scala 3.3.7、JDK 21,Scala CLI 1.9.1;没有第三方依赖。完整源码位于仓库的 examples/scala-lab/snippets/03/Chapter03.scala。实验状态为 LAB_VERIFIED:本章断言已在 Scala CLI 与 sbt 的批次运行中通过,输出与下文一致。证据位于仓库 examples/scala-lab/evidence/20261002-batch01/all.log、sbt.log,对应 JSON 保存实际命令与零退出码。运行方法见本文“实验入口”及附件 RUN.md。 这里使用小整数价格,专门观察函数语义。整数除法会截去小数,乘法也有溢出范围;这些代码不承担通用金额库的舍入规则和输入校验。把价格改成真实结算领域类型时,必须...
Scala 02:求值次数与状态
实验状态:LAB_VERIFIED。Scala 3.3.7、JDK 21 下,Scala CLI 与 sbt 均运行本章断言,另核验局部 lazy 的字节码;原始记录在 examples/scala-lab/evidence/20261002-batch01/。成员字段源码分析标为 SOURCE_VERIFIED,没有对应运行实验。 两次读取为何产生不同的金额 报价服务每次调用可能产生新结果。即使暂时没有网络,用一个递增计数器也能观察同一问题:一个名字出现两次,右侧计算到底执行几次?把 val 换成 def,把普通参数换成传名参数,程序仍可能通过类型检查,得到的值和副作用次数却已经改变。 求值讨论需要先选定观察对象。对 2 + 3 重算多少次通常不会改变结果;对读取时间、消耗迭代器或增加计数器的表达式,次数直接影响业务行为。本文使用确定性计数,不请求实际报价端,也不以墙上时钟判断先后。完整程序在 snippets/02/Chapter02.scala,每个阶段同时断言结果与计数。 val 固定绑定,def 保留计算 先定义一个每次调用都递增的来源: 123456789var ...

