1.2 val、var与表达式:语法第一阶


1.2 val、var 与表达式:语法第一阶

val 声明不可变绑定,var 声明可变变量;在 Scala 里 if、for、代码块都是表达式,它们求值出一个值和一个类型。这是全路线最基础也最容易被 Java 老手忽略的一阶。

一切皆表达式

Java 程序员习惯把 if 当"控制流",Scala 里 if 是表达式——有值、有类型:

val status = if (score > 60) "及格" else "不及格" // status: String = 及格

没有三元运算符,因为 if 本身就是。代码块同样求值为最后一个表达式的值:

val area = { val r = 3.0 math.Pi * r * r } // area: Double = 28.274...

类型阶梯的最底层

Scala 的类型不是平铺的原始类型,而是一棵继承树,这在后面各章会反复用到:

类型金字塔:从 Nothing 到 Any

类型金字塔:从 Nothing 到 Any

Unit 对应其他语言的 void,但它有一个真实值 ()Nothing 现在看着抽象,等第三章讲抛异常与空集合的类型时会回来感谢这张图。

val 优先,var 需要理由

val name = "Alice" // 不可变,类型推断为 String var count = 0 // 可变 count = count + 1 // OK // name = "Bob" // 编译错:reassignment to val

我的习惯是默认 val,写 var 时给自己一句注释说明为什么必须可变。不可变不是洁癖:第五章的类型推导、第六章的并发安全,全都建立在"值不会在我脚下变"这个前提上。攀岩时你希望每个岩点是死的,程序里同理。

类型标注能省则省、该写就写。公共 API 的签名上写明类型是给读者的文档;局部绑定交给推断即可:

val greeting: String = "hi" // 冗余,推断即可 def parse(raw: String): Either[String, Int] = ??? // API 上类型即文档

字符串与基本运算

插值字符串用 s 前缀,格式化用 f 前缀:

val user = "Alice" val n = 42 s"hello $user, answer is $n" f"$n%04d" // "0042" raw"a\nb" // 原样保留反斜杠

数值运算与 Java 基本一致,但注意 Int 与 Double 混合运算会统一提升到 Double;/ 对整数是整除。

图 1-2 语句与表达式的求值差异

图 1-2 语句与表达式的求值差异

if 与 for 都是表达式:一段完整推演

初学者最容易把 Scala 的 if 写回 Java 的老习惯。下面这段在 REPL 里跑一遍,观察每个结果的类型:

val hour = 14 val phase = if hour < 12 then "上午" else "下午" // phase: String = 下午 —— if 有值,不需要三元运算符 val scores = Map("alice" -> 92, "bob" -> 61, "carol" -> 78) val verdict = for (name, s) <- scores yield name -> (if s >= 60 then "及格" else "补考") // verdict: Map[String, String] = Map(alice -> 及格, bob -> 补考, carol -> 及格)

for 表达式里的 yield 是关键:它把循环变成一次"映射",产出新集合而不是副作用。没有 yield 的 for 相当于 foreach,只执行动作、返回 Unit——看到 Unit 基本可以断定代码里藏了副作用。

Unit、Null 与 Nothing 的边界

类型 含义 什么时候遇到
Unit 没有有意义的值 过程式副作用代码的返回
Null 引用类型的特例空值 与 Java 互操作时的返回值
Nothing 位于类型层级最底端 抛异常、永不出返回值的方法

Nothing 之所以是所有类型的子类型,是为了让 throw 表达式能出现在任何位置:val x = if ok then 1 else throw Err() 能通过编译,正是靠 else 分支的 Nothing 服从任何期望类型。这个细节在第五章讲型变时会再回来。

var 的两个正当场景

把 var 定罪之前记住它仍然合法的位置:局部循环中的性能热路径缓冲,以及封装在类内部、外部不可见的状态(如前面 Stack 的 elems)。真正的坏味道是"跨方法共享的 var"——那意味着状态在时间上到处流动,出问题时无从下杆。

字符串插值与块表达式

Scala 3 的字符串插值有三种前缀,最常用的是 s 与 f:

val name = "alice" val score = 92.5 println(s"$name 得了 $score 分") // s 插值:直接嵌值 println(f"$name%s 占比 ${score / 100}%.1%%") // f 插值:带格式化 val grade = val ratio = score / 100 // 块表达式:缩进即作用域 if ratio >= 0.9 then "优" else "良"

块表达式是 Scala 3 的标志语法:一个缩进块整体是一个表达式,最后一个表达式的值就是块的值,中间可以放临时的 val。很多从 Java 过来的人会在块里写 return,其实完全不需要——让值流出来,而不是跳出去。

常见排错:分号推断的坑

换行符会被当作语句分隔符,下面这行想链式调用却断了:

val xs = List(1, 2, 3) .map(_ * 2) // 编译错误:Int 没有 map

解决办法是把点操作符写在行尾而非行首:List(1,2,3).map(_ * 2).filter(...)。这是三天内几乎人人会撞一次的墙,撞过就长记性。

顺带一提 while 循环:它在语言里存在但几乎绝迹,因为 for 与递归覆盖了全部场景。偶尔在性能极端敏感的热循环里能看到它,日常代码中出现 while 通常意味着作者还没完成思维方式换挡。

本节要点回顾

  • 表达式语义:if、块、for 都有值和类型,这是类型推断可用的根基。
  • 类型金字塔:Any 在顶,Nothing 在底,Unit 有真实值。
  • val 优先:不可变是后面所有高级特性的地基。
  • 推断有度:局部靠推断,公共签名写明类型。

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