想象你入职了一家古怪的“数据料理餐厅”。这家餐厅有三条铁律:第一,每道菜谱都可以像食材一样被装进信封、传给任何一位厨师;第二,任何一道菜一旦端上桌,就永远不许回锅改动,想吃新口味就另做一盘;第三,每位厨师做菜时只准看自己的菜谱和配菜,不许碰别人灶台上的东西。这些规矩听起来很约束人,却让整个后厨秩序井然——没有厨师会担心同事偷偷改了自己的菜,所有菜品都可追溯,任何一道菜换任何一个人来做,味道都一模一样。 这就是 Scala 函数式编程的世界观。函数(菜谱)是一等公民,可以被传递、组合;数据不可变(菜不上桌即成历史),更新永远产生新值;纯函数像标准化菜谱,同样的原料必然得出同样的菜。再加上模式匹配这双“分拣手”和尾递归这个“套娃拆解术”,你会发现:限制,反而带来了安全与自由。这篇指南会带你从后厨的每个角落走一遍,把高阶函数、不可变数据、模式匹配这些核心概念,用看得见的画面讲明白。
掌握 Scala 函数式编程,意味着你能写出天然线程安全、易于测试和重构的数据管道代码——这是大数据与 AI 工程岗位的核心竞争力。
目录
可视化演示
展示高阶函数管道组合、柯里化多参拆解变换、不可变数据更新复制机制,切换模式掌握函数式编程核心思维。
本节配有交互动效(进入视口后自动加载,约 3KB)。
高阶函数与函数组合管道 函数是一等公民:可赋值给变量、作为参数传递、作为返回值,map/filter/fold等组合子构建声明式管道 andThen和compose实现函数组合:f andThen g = g(f(x)),将简单函数串联为复杂计算流 list.map(f).filter(g).foldLeft(z)(h) | declarative pipeline 高阶函数是函数式编程的基石,实现计算的声明式组合
函数即值:菜谱是可以传递的物件
在数据料理餐厅,最值钱的东西不是某道招牌菜,而是菜谱本身。主厨可以把一份菜谱抄在卡片上,装进信封,递给任何一位学徒;学徒拿到后不必追问“这菜谱是什么职位”,直接照着执行就行。在这里,菜谱和食材一样,是一种可以随身携带的“东西”。这就是函数式编程的第一块基石:函数即值。
传统语言里的函数更像“规则”而不是“东西”:你只能站在固定的灶台前调用它,却很难把它递给别人。而 Scala 把函数提升为“一等公民”——函数可以像 Int、String 一样被赋值给变量、塞进集合、作为参数传进另一个函数。你在餐厅里能对食材做的任何事,几乎都能对函数做。
Scala 中声明函数有两种姿势。一是用 def 定义方法,二是用 val 把匿名函数绑定成一个值:val double = (x: Int) => x * 2。这里的 (x: Int) => x * 2 就是一个匿名函数,它没有名字,却能直接参与运算。每个函数都有类型签名,Int => Int 读作“接收一个整数、返回一个整数”,就像菜谱封面上写的“原料→成品”。
现在再看餐厅里的场景:主厨把写着 (x: Int) => x * 2 的卡片递给学徒,学徒不用关心它是谁写的,只需要知道“给它整数,它翻倍”。函数变成了可传递的物件,这正是后续所有函数式魔法——批量加工、流水线组合——的起点。
x、输出 2x 的无名函数,它的类型是“整数进、整数出”。- λ 记号λ 读作 lambda,数学中用它表示“无名函数”——一种只给出输入到输出规则的映射,好比一张没有署名的菜谱卡片。
- 参数与结果
.之前的x是入参,之后的2x是返回结果,整体读作“给定 x,返回 2 倍的 x”。 - 类型签名
\text{Int} \to \text{Int}标出函数类型——整数进、整数出,像菜谱封面写着“食材→菜品”的标签。
高阶函数:让菜谱加工菜谱
有了能传递的菜谱,主厨发明了“批量加工流水线”:把一筐食材交给一条写着菜谱的流水线,机器会自动对每颗食材应用一次菜谱,最后吐出一筐新菜。更妙的是,流水线本身也是一份“加工菜谱的菜谱”——它接收“菜谱”作为参数。这种“函数吃函数”的函数,就叫高阶函数。
高阶函数是函数式编程的引擎。最常用的三兄弟是 map、filter 和 fold:map 对集合里每个元素应用同一个函数,得到等长的新集合,相当于每颗食材都走一遍同一条菜谱流水线;filter 按条件留下合格元素,好比分拣员把不够新鲜的食材挑出去;fold 则把整个集合“揉”成一个值,比如把一盘菜的成本逐项加总。Scala 中还有 flatMap——先 map 再摊平,专门处理“一个元素展开成一串元素”的场景。
柯里化则是另一种高阶技巧:一个接收两个参数的函数 (x: Int, y: Int) => x + y,可以被改写成“先接收 x,返回一个等待 y 的菜谱”。用 curried 或手动写嵌套函数即可实现。从此,你可以在不同时间逐步投喂参数,还能预先固定第一个参数,得到一份“特制版菜谱”。
回到餐厅,主厨宣布:“以后不需要有人站在灶台前一道菜一道菜地炒了!把菜谱挂上流水线,一筐食材自动变成一筐新菜。” 高阶函数让程序员从“循环遍历”的重复劳动中解放出来,把注意力放在“每颗食材该经历什么变化”上——这正是函数式编程极具生产力的秘密。
map 的类型签名:把“一个把 A 变成 B 的函数”和一整列 A 元素组合起来,返回一整列 B 元素。- 函数参数
f: A \to B表示 map 的第一份输入是一个函数,它把任意类型 A 变成 B——这就是“把菜谱交给流水线”。 - 原始集合
\text{List}[A]是装着 A 类型元素的列表,相当于一筐尚未加工的食材。 - 结果集合返回
\text{List}[B],即每个元素都经过 f 处理后的新列表,长度不变、内容焕然一新。
不可变数据:历史账本不改旧页
数据料理餐厅最古怪也最智慧的规矩是:菜品一旦完成,就绝不允许回锅。想调整口味?厨师不会去动那盘已经上桌的菜,而是照着它重新做一盘新的。餐厅角落有一本巨大的历史账本,每一道菜从诞生那一刻起就被记录,之后任何人的修改都不会落到旧页上,只会新增一页。这个“历史账本”哲学,就是不可变数据。
Scala 中,val x = 1 声明一个不可重新绑定的引用,而 List、Vector、Map 默认都是不可变结构。当你写 xs :+ 4 时,并不是往 xs 尾部硬塞一个元素,而是构造出一个“包含原元素加新元素”的崭新列表,xs 本身纹丝不动。就像账房先生做新账时,只是另起一页抄上旧账加一笔新的,旧页分毫不变。
全部复制岂不是很浪费?这里有个聪明机制叫“结构共享”:新列表和旧列表会共用那些没变化的部分,只有变化的路径才新建节点。就像餐厅把账本做成“洋葱式追加页”——新页与旧页共享前面的纸张,只是多出几页新记录。这样一来,不可变更新的成本通常只有 O(log n),而不是把整本账都抄一遍的 O(n)。
账本哲学带来的红利是“安心”。多线程并发时,多个厨师可以同时读同一本账本,不用担心谁撕了谁的页;出问题时,每一版数据都还在历史里,随时可以回滚复盘。在 AI 数据管道中,这意味着你的原始数据集永远受保护,任何清洗、增强操作都只产生新的数据集。
- 追加运算
\operatorname{append}(a, 4)表示把 4 追加到列表 a 末尾,Scala 里写作a :+ 4,理解成“基于旧账生成一笔新账”。 - 旧值不变含义号
\implies右侧第一项:a 依旧是\text{List}(1,2,3),历史账本的旧页没有被涂改。 - 新值诞生右侧第二项:b 是
\text{List}(1,2,3,4),它是一页全新的记录,并和 a 共享前面的三个元素。
纯函数思想:灶台规矩与引用透明
餐厅还有一条铁律:每位厨师做菜时,只能使用自己手里的菜谱和配菜,不许偷看同事的灶台、不许悄悄改动公共调料柜。因为一旦某道菜的味道取决于“当时谁从旁边经过”,这道菜就永远没法复现了。这条铁律,对应的正是纯函数。
纯函数的定义很严格:同样的输入,永远得到同样的输出,并且不产生任何外部可观察的副作用。它不操作全局变量、不读写文件、不打印日志(在函数体内),只通过参数进、通过返回值出。你可以把它想成一台封死的榨汁机:投进去同样的橙子,永远出来同样的橙汁,机壳外面干干净净,不会滴油、不会冒烟。
纯函数带来的第一个好处是“引用透明”:任何一次调用,都可以用它的返回值原地替换,而程序行为不变。这极大简化了推理和测试——测纯函数就像验算菜谱:同样的食材、同样的手法,味道一定相同。你不需要准备复杂的运行环境、mock 一堆外部依赖,直接喂输入、断言输出即可。
有了纯函数这条“灶台规矩”,餐厅的后厨才能并行开火:十位厨师同时做菜互不干扰,任何一道菜都能追溯还原。在真实项目中,把核心计算抽成纯函数,是并发安全与可测试性的基石——这也是 Spark 等大数据框架选择 Scala 作为实现语言的重要原因。
- 一次调用
f(x) = y表示纯函数 f 收到输入 x 后返回结果 y。 - 无副作用
f不触碰外部状态,只依赖参数计算,因此同样的 x 永远得到同样的 y。 - 全局替换含义号
\implies之后的式子表示:在任何位置上,f(x)都等价于y,这就是引用透明的完整含义。
模式匹配:快递分拣台
每天下午,餐厅会收到五花八门的食材包裹:有的是整箱鸡蛋、有的是冷链海鲜、还有一些写着“易碎”的玻璃瓶。分拣员不靠 if-else 一条条问“是鸡蛋吗?是海鲜吗?”,而是看一眼包裹的形状与标签,直接把它丢进对应的传送带,同时把包裹里的订单小票顺手抽出来登记。这个动作,就是模式匹配。
Scala 的 match 表达式就是这个分拣台。它能把一个值和一系列“模式”比对,命中后执行对应分支,还能在匹配的同时“解构”——把对象的内部字段拆出来直接用。比如处理“可能没有结果”的值时,Scala 用 Option 类型表示:有值就是 Some(v),没值就是 None。写成 x match { case Some(v) => v + 1; case None => 0 },一条语句同时完成了判空、拆箱和兜底。
模式匹配不止能匹配字面量,还能匹配类型、集合形状,甚至支持“守卫条件”:case n if n > 0 => "正数" 在模式后面再加一道检查。配合 case class,它还能自动拆字段:case Point(x, y) => ... 直接拿到坐标。更重要的是,编译器会检查所有可能的分支是否被覆盖——漏掉一种情况,编译期就提醒你,这比 if-else 的“运行时才踩坑”安全得多。
分拣台让包裹处理又快又稳。以前用 if-else 检查“是不是空、是不是某种类型、内部值是多少”,现在一个 match 全部搞定,还自带解构与穷尽性检查。在 AI 工程里,处理可缺失的特征值、解析模型返回的“有结果/无结果”,正是模式匹配最舒服的主场。
- 被分拣的值
x是待分拣的“包裹”,它的类型是\text{Option}[\text{Int}]——要么装着值,要么是空的。 - Some 模式
\text{case Some}(v)匹配“包裹里确实有货”的情况,同时把货物解构出来绑到变量v上。 - None 模式
\text{case None}匹配“空包裹”的情况,返回兜底值 0——两个分支覆盖了所有可能,编译器放心,你也放心。
函数式集合:食材传送带
餐厅的食材柜从不散乱堆放,所有食材都按顺序排成一列列传送带。每一条传送带上的食材都可以整体被推过任何一台“菜谱机器”——这就是函数式集合。List 是链表式传送带,擅长顺序访问与头部操作;Vector 是树状传送带,任何位置的取放都快,适合随机访问和尾端追加。
集合操作的威力在于“链式组合”。xs.filter(isValid).map(transform).foldLeft(0)(combine) 一行代码,把筛选、变换、汇总串成整条流水线。每一步都返回新集合、不污染中间结果,读起来就像在描述“食材怎么被加工”,而不是“循环变量怎么递增”。这种声明式写法让代码的意图一目了然,也更容易并行化。
还有一招叫惰性求值:LazyList 只有在真正需要某个元素时才计算它。好比传送带尽头有个人逐个取菜,机器只在取到的时候才加工当前这一颗,而不是一次性把整筐都做完。处理无限序列(比如源源不断的日志流、模型分数流)时,惰性求值让“无穷集合”也能被安全地操作。
传送带和账本配合,让“遍历、加工、汇总”这类最常见的编程动作有了极简表达。你在 AI 数据处理里做的归一化、去重、聚合,本质都是 filter、map、fold 的组合——与其写一堆 for 循环和临时变量,不如把数据放上传送带,让集合函数替你跑完全程。
foldLeft 拿着初始值 z 从左到右,依次和每个元素做一次 f 运算,最终把整列食材汇总成一个结果。- 初始值
z是 fold 的起点,相当于账本上最开始的总数 0。 - 两两叠加
f是一个双参数函数,把当前累加结果与下一个元素结合;f(z, x_1)处理第一个元素。 - 层层嵌套从左边一路嵌套到最后一个元素,
f(f(f(z, x_1), x_2), x_3)正是“拿着账本从左到右逐条入账”的数学表达。
递归与尾递归:俄罗斯套娃
餐厅里有一组俄罗斯套娃摆件。想知道总共有几层,你不会去数外壳,而是:先记下最外面这一层(1),然后打开它,对剩下的套娃“再来一次同样的数法”。这个过程就是递归——一个函数在处理问题时调用它自己,只是每次面对的是更小的同类问题。
很多函数式程序员用递归代替循环:比如用 def sum(n: Int): Int = if (n <= 0) 0 else n + sum(n - 1) 求和。递归的代码贴近数学定义,读起来优雅直观。但它有个隐患:每次自调用都要压一层调用栈,递归太深就会栈溢出——套娃拆到一万层,厨房的存货架就塌了。
解决办法是尾递归。如果递归调用是函数里的“最后一步”,并且结果直接返回,编译器就能把它改写成循环,不再增长调用栈。Scala 用 @tailrec 注解让编译器替你校验:写错了会直接编译报错。把累加值作为参数一路传递,sum 的尾递归版是 def sum(n: Int, acc: Int): Int = if (n <= 0) acc else sum(n - 1, acc + n),无论 n 多大都不会爆栈。
有了尾递归,套娃可以拆一万层也不怕。函数式编程并不是“禁止循环”,而是给了你一个既优雅又安全的替代品。处理树形结构(如决策树遍历)、深度嵌套的 JSON 时,递归配尾递归,就是那件得心应手的拆解工具。
- 终止条件当
n = 0时不再递归,直接把累积结果\text{acc}作为最终答案——这是套娃拆到最里层的情形。 - 递归步骤当
n > 0时,把当前的 n 先加进 acc,然后对n - 1调用同样的函数——每次套娃都变小一层。 - 尾部位置递归调用发生在函数最后一步、结果直接返回,编译器才能安全地把它改写成循环,栈空间恒定。
AI 应用场景
数据管道与特征工程
阶段:数据处理与特征工程
解决的问题:传统方式用 for 循环加可变累加器清洗数据,代码耦合、中途出错难以回放。用函数式思路,map/filter/fold 构建纯函数式管道,每一版数据都是不可变快照,原始数据永远安全。
具体案例:某推荐系统团队每天清洗千万级点击日志,用 Scala 的 logs.filter(_.valid).map(parse).groupBy(userId) 完成过滤、解析与分组,配合不可变集合保证多节点并行处理互不干扰;出问题时直接按版本回放日志,错误不可能悄悄覆盖原始数据。
高并发 AI 服务中的不可变消息
阶段:模型服务与并发调度
解决的问题:线程间共享可变状态是并发 bug 的温床。把请求建模成不可变 case class,演员模型靠消息传递通信,配合模式匹配分发,天然线程安全。
具体案例:某 AI 推理服务用 Akka 处理推理请求,请求被建模为 case class InferenceRequest(id: String, features: Vector[Double]),Actor 用 match 区分请求、超时、重试等不同消息类型,失败重试与熔断都走独立分支,压测下零数据竞争。
模型结果的可失败处理
阶段:推理结果解析
解决的问题:模型可能返回空值或异常结果,传统 if-else 层层判空容易遗漏。用 Option 包裹结果,再配合模式匹配穷尽处理“有值/无值”两种情况,编译期就堵住漏洞。
具体案例:某 OCR 系统对每张图片返回 Option[String],代码里 case Some(text) 时送入后处理,case None 时把图片丢回重试队列,两种路径都被编译器强制覆盖,线上空指针异常率降为零。
Python 演示
"""Demo: functional programming ideas in Python.Show functions as values, higher-order functions,immutable records, and tail-recursion style."""from dataclasses import dataclassfrom typing import Callable, List, TypeVar A = TypeVar('A')B = TypeVar('B') def double(x: int) -> int: """Pure function: double the input.""" return x * 2 def my_map(f: Callable[[A], B], xs: List[A]) -> List[B]: """Apply f to every element, produce a new list.""" return [f(x) for x in xs] def my_filter(pred: Callable[[A], bool], xs: List[A]) -> List[A]: """Keep elements that satisfy pred.""" return [x for x in xs if pred(x)] def my_fold(f: Callable[[B, A], B], init: B, xs: List[A]) -> B: """Fold the list into one value from left to right.""" acc = init for x in xs: acc = f(acc, x) return acc @dataclass(frozen=True)class Sample: """Immutable record: a sample with score and label.""" score: float label: str def scaled(self, rate: float) -> "Sample": """Return a NEW sample instead of mutating self.""" return Sample(self.score * rate, self.label) def sum_tailrec(n: int, acc: int = 0) -> int: """Tail-recursion style sum (Python won't optimize it).""" if n <= 0: return acc return sum_tailrec(n - 1, acc + n) def main() -> None: samples = [Sample(1.5, "A"), Sample(-2.0, "B"), Sample(3.0, "C")] scaled = my_map(lambda s: s.scaled(2.0), samples) print("original unchanged:", samples[0]) print("scaled version:", scaled[0]) xs = [1, 2, 3, 4, 5, 6] evens = my_filter(lambda x: x % 2 == 0, xs) squares = my_map(double, evens) total = my_fold(lambda acc, x: acc + x, 0, squares) print("evens:", list(evens), "doubled:", list(squares), "total:", total) print("tailrec sum 1..100:", sum_tailrec(100)) if __name__ == "__main__": main()
def my_map(f: Callable[[A], B], xs: List[A]) -> List[B]:函数签名里直接写着“吃进一个函数和一列数据,吐出一列新数据”,函数成了名副其实的参数。return [f(x) for x in xs]对每个元素套用传入的函数 f,生成全新列表,原列表没有被触碰。@dataclass(frozen=True)frozen=True 让 dataclass 生成的实例不可修改,模拟 Scala case class 的不可变语义。def scaled(self, rate: float) -> "Sample":返回新对象而不是修改旧对象,这就是“更新即新建”的账本哲学。def sum_tailrec(n: int, acc: int = 0) -> int:累加器 acc 作为附加参数一路携带,是尾递归的标志性写法。return sum_tailrec(n - 1, acc + n)递归调用位于函数最后一步,Scala 的@tailrec会把这种写法编译成循环。total = my_fold(lambda acc, x: acc + x, 0, squares)fold 把一个列表整体“揉”成单个数值,替代手写累加循环。
常见误区
误区我以为 `val` 声明的任何东西都绝对不可变,所以 `val buf = ListBuffer(1,2,3)` 之后如果还有人 `append` 元素,编译器一定会阻止。
正解val 锁住的只是“这个引用不能重新绑定”,不代表对象内部不可变。val buf = ListBuffer(...) 依然可以加元素,因为 ListBuffer 是可变集合。要真正不可变,得选 List、Vector、immutable.Map 这类不可变集合。这就像“账本封面写了不许换”,但账本里的活页纸仍能被撕掉——换不换封面和纸能不能撕是两回事。
为何常见:教程常强调“val 不可变”,初学者容易把“引用不可变”和“数据结构不可变”混为一谈。
误区我听说函数式编程排斥循环,所以连 `for` 都不能写,遇到遍历只能手写递归,结果代码又难读又容易爆栈。
正解函数式编程反对的是“带可变累加器的循环”,而不是循环本身。绝大多数遍历工作交给 map/filter/fold 这类高阶函数即可,它们内部就是循环;只有处理树、嵌套 JSON 这类递归结构时才需要写递归,此时用尾递归保证栈安全。餐厅不是不让人走路,只是让你坐传送带。
为何常见:把“避免可变状态”过度解读成“禁止一切循环结构”,忽略了高阶函数内部恰恰是循环。
误区我觉得模式匹配不过是把 if-else 换个语法,反正都是比较,不值得单独学。
正解模式匹配能“按形状拆包”:case Some(v) 在判断类型的同时直接提取内部的 v;case class 模式还能自动解构字段。此外编译器会做穷尽性检查,漏分支会在编译期报错,这是 if-else 给不了的静态安全保障。分拣台不只是比大小,它还能顺便拆包裹。
为何常见:初看 match 的语法和 switch 相似,容易低估解构与穷尽性检查的价值。
误区每次更新都要复制一份完整数据,数据量大时肯定慢得没法用,所以生产项目里还是用可变集合靠谱。
正解不可变集合普遍采用结构共享:新旧版本共用未变化部分,单次更新的平均代价接近 O(log n),而且无需加锁即可并发读。Spark 等大数据框架大量使用不可变数据结构处理 TB 级数据,证明它在真实工程里完全可行。这不是“把整本账重抄一遍”,而是“在旧账上追加共享的新页”。
为何常见:从“复制”的字面意思理解性能,忽略了结构共享与持久化数据结构的现代实现。
面试题
Q1. 在 Scala 中 val xs = List(1, 2, 3),随后执行 xs :+ 4,此时 xs 的内容是?
- A.变成 List(1,2,3,4)
- B.仍是 List(1,2,3)← 正确答案
- C.编译阶段就会报错
- D.变成空列表 ()
答案:B。:+ 是“追加生成新列表”的操作,基于 xs 创建新列表,xs 本身是 val 绑定的不可变 List,原封不动。这正像历史账本:新页记上新账,旧页永不涂改。
Q2. 下列哪个写法能正确定义一个“将整数翻倍的匿名函数”?
- A.int double(x){2*x}
- B.(x: Int) => x * 2← 正确答案
- C.lambda x: return x*2
- D.2 *: Int => Int
答案:B。Scala 匿名函数的标准语法是 (参数列表) => 表达式,所以 B 正确。A 是 C 风格函数声明,C 是伪 Python 写法,D 的运算符风格并不是定义函数。这道题考的是“菜谱卡片”的标准写法。
Q3. List(1, 2, 3).map(_ * 2) 的运算结果是?
- A.List(1, 4, 9)
- B.List(1, 2, 3)
- C.List(2, 4, 6)← 正确答案
- D.List(2, 3, 4)
答案:C。map 对每个元素应用“乘以 2”的菜谱,1→2、2→4、3→6,得到长度不变的新列表 List(2,4,6)。A 是误把 map 当平方,B 是没做变换,D 是偏移而非翻倍。
Q4. 关于纯函数,下列说法正确的是?
- A.可以随时修改全局变量状态
- B.同样的输入必然有同样的输出← 正确答案
- C.纯函数一定不允许使用局部变量
- D.纯函数执行时总是异步进行的
答案:B。纯函数的核心是“同输入同输出、无外部可观察副作用”。它允许使用局部 val 辅助计算,只是不碰外部状态;B 准确描述了确定性,A、C、D 都与纯函数定义相悖。
Q5. List(1, 2, 3).foldLeft(0)(_ + _) 的返回值是?
- A.返回 0
- B.返回 3
- C.返回 6← 正确答案
- D.返回 123
答案:C。foldLeft 从初始值 0 开始,依次与 1、2、3 求和:0+1=1,1+2=3,3+3=6。就像账房先生拿着账本从 0 开始逐条入账,最终总额是 6。
Q6. Scala 中 @tailrec 注解的作用是?
- A.让所有递归都自动变成循环
- B.编译期校验函数确为尾递归← 正确答案
- C.给函数自动添加日志记录
- D.运行时动态优化内存使用
答案:B。@tailrec 是编译期检查器:如果标注的函数不是尾递归,编译直接报错。编译器只在实际是尾递归时才把它改写成循环。它不能把普通递归强行变循环,只是“校秤”,不是“变魔术”。
Q7. 模式匹配 case n if n > 0 => ... 中的 if n > 0 是?
- A.类型参数约束
- B.守卫条件额外过滤← 正确答案
- C.隐式转换规则
- D.默认参数的值
答案:B。模式后面的 if n > 0 叫守卫条件(guard),在模式命中的基础上再追加一层布尔判断。好比分拣时先看包裹形状,再称一下重量是否达标,两者都满足才走这条传送带。
Q8. 下列哪些是 Scala 标准库中的不可变集合?
- A.List 不可变链表← 正确答案
- B.Vector 随机访问← 正确答案
- C.immutable.Map 映射← 正确答案
- D.ArrayBuffer 缓冲数组
答案:ABC。List、Vector 和 immutable.Map 都是不可变集合,更新操作返回新版本且共享结构。ArrayBuffer 是可变缓冲数组,支持原地增删,属于“活页账本”,所以不选。
Q9. 关于不可变数据的结构共享,下列说法正确的有?
- A.每次更新都要完整复制全部数据
- B.新旧版本会共享未变化的部分← 正确答案
- C.结构共享让更新代价通常较低← 正确答案
- D.不可变数据无法被并发安全读取
答案:BC。结构共享让新旧版本共用未变化节点,只有变化的路径新建节点,更新代价通常接近 O(log n),所以 B、C 正确,A 是朴素复制的误解。不可变数据天然适合无锁并发读,D 错误。
Q10. 模式匹配与 if-else 相比,优势体现在哪些方面?
- A.可自动解构提取对象内部字段← 正确答案
- B.编译器能检查分支是否穷尽← 正确答案
- C.只能用来匹配基础的数据类型
- D.匹配时不支持额外守卫条件
答案:AB。模式匹配在判断的同时能解构出内部字段(如 case Some(v) 直接拿到 v),且编译器会检查穷尽性,漏分支编译报错,所以 A、B 正确。它能匹配 case class 和集合等复杂结构,也支持 if 守卫条件,C、D 错误。