第2章 变量、类型与表达式

val / var 的区别、类型推断、基本类型、字符串插值,以及“一切皆表达式”这个 Scala 的底层思维方式。

语法第2-3周3 讲9 段示例代码

本章学习目标

  • 能解释为什么 Scala 优先用 val(不可变)
  • 会写 s"" / f"" / raw"" 三种字符串插值
  • 能看懂并写出块表达式、if 表达式的返回值
  • 知道 Unit 与 () 的含义

本章导览

核心内容

2.1

val 与 var:不可变优先与类型推断

分清 val 与 var,让类型推断替你写类型。

用 val 定义不可变值,只在必要时用 var能根据字面量判断推断出的类型能修掉重新赋值和整数除法两类问题

知识点

val:一次赋值,永久只读

  • val 类似 Java 的 final 变量,引用不能指向新值。
  • 重新赋值是编译错误,不是运行时才报。
  • 优先用 val,代码更容易推理,也天然适合并发。

var:需要改才用

  • var 可以重新赋值,但类型在第一次赋值时就定死。
  • 常见场景是循环计数、累加器;能用集合操作替代就替代。
  • var 的变量类型不能从 Int 变成 String。

类型推断:编译器从右值猜类型

  • val n = 1 推断为 Int,val big = 1L 推断为 Long,val pi = 3.14 推断为 Double。
  • 字符串、字符、布尔分别推断为 String、Char、Boolean。
  • 公共 API 和拿不准时写显式类型:val n: Long = 1。

不可变不等于集合内容不可变

  • val 锁的是引用;如果指向 mutable.Map,内容仍然可以增删。
  • Scala 默认集合不可变,val 加不可变集合才是真正安全的组合。

代码示例

▶ REPL 看类型推断的结果
SCALA
scala> val count = 3
count: Int = 3

scala> val big = 3L
big: Long = 3

scala> val price = 19.9
price: Double = 19.9

scala> val letter = 'S'
letter: Char = S

scala> val message = "hi"
message: String = hi
👀 运行结果
count: Int = 3
big: Long = 3
price: Double = 19.9
letter: Char = S
message: String = hi
▶ 全部用 val:算出一个新值而不是改旧值
SCALA
object ValOnly {
  def main(args: Array[String]): Unit = {
    val name = "小新"
    val age = 18
    val nextYear = age + 1
    println(s"$name 明年 $nextYear 岁")
  }
}
👀 运行结果
小新 明年 19 岁
▶ 确实需要变化时:var 记录余额变化
SCALA
object Balance {
  def main(args: Array[String]): Unit = {
    var balance = 100
    balance = balance - 30
    balance = balance + 50
    println(s"balance = $balance")
  }
}
👀 运行结果
balance = 120

常见错误

⚠️ 踩坑与解决
  • val x = 1; x = 2 报 reassignment to val
    → val 是只读引用,Scala 在编译期就禁止重新赋值。;改用 var x = 1;更好的做法是保留 val,新建 val y = x + 1。
  • val half = 5 / 2 得到 2 而不是 2.5
    → 两个 Int 相除执行整数除法,结果仍然是 Int。;把至少一边写成浮点:5 / 2.0 或 5.toDouble / 2。

💡 默认写 val,编译器逼你改成 var 时再改;这个小习惯能挡掉大量共享状态 bug。

2.2

基本类型与字符串插值 s/f/raw

掌握基本类型与 s/f/raw 三种插值写法。

分清 Int、Long、Double、Char、Boolean 的字面量用 s 插值拼字符串,用 f 控制小数位用 raw 保留反斜杠,写路径和正则不踩坑

知识点

基本类型都是一等公民

  • Int、Long、Double、Float、Short、Byte、Char、Boolean、Unit 都是对象,能调用方法。
  • 整数默认推断为 Int;想明确拿 Long 就加 L,例如 42L。
  • Char 用单引号,String 用双引号,两者不能混。

数值转换要显式

  • Int 到 Long、Double 用 toLong、toDouble;小数转整数会截断,不是四舍五入。
  • 字符串转数值用 toInt、toDouble,格式不对会抛 NumberFormatException。
  • 字面量后缀:L 表示 Long,f 表示 Float,小数点字面量默认 Double。

s、f、raw 各管一件事

  • s"..." 做变量替换,表达式放 ${...}。
  • f"..." 复用 Java Formatter,用 %.2f 控制小数位,格式符和值的类型要匹配。
  • raw"..." 不处理转义,反斜杠原样保留;普通字符串要写双反斜杠才能得到一个反斜杠。

三引号适合多行文本

  • """...""" 保留换行,配合 stripMargin 和 | 去掉缩进。
  • 字符串不可变,任何“修改”都会返回新字符串,原值不变。

代码示例

▶ REPL 里的基本类型
SCALA
scala> val n = 42
n: Int = 42

scala> val big = 42L
big: Long = 42

scala> val ratio = n / 2.0
ratio: Double = 21.0

scala> val ch = 'S'
ch: Char = S

scala> val ok = n > 10 && ch == 'S'
ok: Boolean = true
👀 运行结果
n: Int = 42
big: Long = 42
ratio: Double = 21.0
ch: Char = S
ok: Boolean = true
▶ s / f / raw 三种插值同台
SCALA
object Interp {
  def main(args: Array[String]): Unit = {
    val name = "Scala"
    val version = 2.13
    val pi = math.Pi

    println(s"$name $version")
    println(f"pi = $pi%.3f")
    println(raw"path C:\temp\new")
    println(s"1 + 2 = ${1 + 2}")
  }
}
👀 运行结果
Scala 2.13
pi = 3.142
path C:\temp\new
1 + 2 = 3
▶ 三引号 + stripMargin 写多行文本
SCALA
object TextBlock {
  def main(args: Array[String]): Unit = {
    val sql =
      """select *
        |from users
        |where age > 18""".stripMargin

    println(sql)
    println(sql.linesIterator.size)
  }
}
👀 运行结果
select *
from users
where age > 18
3

常见错误

⚠️ 踩坑与解决
  • println("Hello, $name") 输出 Hello, $name
    → 普通字符串不解析 $ 变量,只有 s 插值器才做替换。;写成 s"Hello, $name"。
  • println("C:\temp\new") 的输出被拆成两行
    → 普通字符串把 \t 当制表符、\n 当换行,路径被转义吃掉了。;用 raw"C:\temp\new",或者把每个反斜杠写成两个。

💡 写 Windows 路径、正则、JSON 片段时优先用 raw 或三引号,普通字符串的反斜杠最容易漏。

2.3

运算符与块表达式:一切皆有值

把代码写成表达式,理解块与 Unit 的返回值。

能说出运算符调用的真实形式会用块表达式给 val 赋值能判断一个块的值是结果还是 Unit

知识点

运算符就是方法

  • 1 + 2 实际调用 1.+(2),Scala 没有给“运算符”特殊地位。
  • 方法名是符号时可以用中缀写法,a max b 等价 a.max(b)。
  • 运算符优先级由首字符决定,不确定就加括号。

块表达式有值

  • 大括号里最后一个表达式的值就是整个块的值。
  • val x = { val a = 1; a + 2 } 中 x 是 3,a 出了块就不可见。
  • 块里可以有多个 val、var、def,最后一行负责“交答案”。

Unit 是“没有有用值”

  • println、赋值语句的返回类型是 Unit,值是 ()。
  • 块最后一行是 println 时,块的值就是 (),别指望它带回字符串。
  • 函数副作用写在 Unit 方法里,纯计算写成有返回值的方法。

没有 i++ 和 ? :

  • 自增写 i += 1,并且 i 必须声明成 var。
  • 三目运算用 if (cond) a else b,它本身是表达式,能直接赋值。

代码示例

▶ REPL:块的值就是最后一行
SCALA
scala> val x = { val a = 10; val b = 20; a + b }
x: Int = 30

scala> val y = { println("计算中"); 6 * 7 }
计算中
y: Int = 42
👀 运行结果
x: Int = 30
计算中
y: Int = 42
▶ 运算符只是方法调用的语法糖
SCALA
object Ops {
  def main(args: Array[String]): Unit = {
    println(1 + 2 * 3)
    println((1 + 2) * 3)
    println((1).+(2))
    println(List(1, 2) ::: List(3, 4))
    println("ab" * 3)
  }
}
👀 运行结果
7
9
3
List(1, 2, 3, 4)
ababab
▶ 块的值不一定是你想要的值
SCALA
object BlockUnit {
  def main(args: Array[String]): Unit = {
    val product = {
      val a = 6
      val b = 7
      a * b
    }
    val printed = {
      println("只打印这一行")
    }
    println(product)
    println(printed)
    println(printed == ())
  }
}
👀 运行结果
只打印这一行
42
()
true

常见错误

⚠️ 踩坑与解决
  • var i = 0; i++
    → Scala 的 Int 没有 ++ 方法,自增不是语言内置语法。;写 i += 1,并且 i 要声明成 var。
  • val r = { println("hi") } 之后 r 是 ()
    → 块的值取最后一行,println 返回 Unit,打印出来的内容没有被保留。;先算值再打印:val r = "done"; println(r),别把打印语句当返回值。

💡 读 Scala 代码时先找“最后一个表达式”,它决定这个块、函数、if 或 match 的值。