4. 控制结构与函数:if表达式、while与for循环、函数定义与默认参数、过程与惰性函数

好,咱们今天聊聊 Scala 里的控制结构和函数。这些东西,说白了就是写代码的骨架和肌肉。我刚开始从 Java 转 Scala 时,最不习惯的就是 if 居然有返回值。嗯,咱们一个一个来看。

4.1 if 表达式:不只是分支,更是值

在 Java 里,if 是个语句,干完活就完了。但在 Scala 里,if 是个表达式——它会产生一个值。你想想看,这意味着什么?

// Java 风格:先声明,再赋值
int result;
if (x > 0) {
    result = 1;
} else {
    result = -1;
}

// Scala 风格:直接赋值
val result = if (x > 0) 1 else -1

我个人习惯用第二种写法。代码短了,而且 result 直接就是 val,不可变,心里踏实。

核心要点:Scala 的 if/else 有返回值,类型取两个分支的公共父类型。

我在项目中遇到过一个问题:两个分支返回不同类型怎么办?比如:

val result = if (flag) 42 else "hello"
// result 的类型是 Any

嗯,这里要注意。类型推断会推到 Any,后面用起来就得做类型匹配。所以,我建议尽量让两个分支类型一致,别偷懒。

4.2 while 与 for 循环:老伙计与新玩法

while 循环,跟 Java 一模一样。没什么好说的,就是个老伙计。

var i = 0
while (i < 10) {
    println(i)
    i += 1
}

for 循环就不一样了。Scala 的 for 其实叫 for 推导式,功能强得多。

4.2.1 基本 for 循环

for (i <- 1 to 5) {
    println(i)
}

这里 1 to 5 生成一个 Range,<- 表示“从...中提取”。

4.2.2 带守卫条件的 for

我曾经在写数据处理任务时,需要过滤掉一些脏数据。用守卫条件特别方便:

for (i <- 1 to 10 if i % 2 == 0) {
    println(i)  // 只打印偶数
}

这比在循环体里写 if 判断清爽多了。

4.2.3 for 推导式与 yield

这才是 Scala 的杀手锏。for 加上 yield,能生成一个新的集合。

val evens = for (i <- 1 to 10 if i % 2 == 0) yield i
// evens: IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)

你想想看,一行代码就完成了过滤和转换。我在做 ETL 时经常这么用,把原始数据清洗成目标格式。

小技巧:yield 返回的集合类型取决于你遍历的集合。遍历 Range 返回 Vector,遍历 List 返回 List。

4.3 函数定义与默认参数

Scala 的函数定义很灵活。咱们从最基础的开始。

4.3.1 基本函数定义

def add(x: Int, y: Int): Int = {
    x + y
}

注意,这里没有 return 关键字。函数体最后一个表达式的值就是返回值。我刚开始写时总习惯加 return,后来发现 Scala 社区基本不用它。

4.3.2 默认参数

这个特性我特别喜欢。写 Spark 作业时,很多配置参数都有默认值,调用时只需要传需要改的。

def connectToDB(host: String = "localhost", port: Int = 3306, db: String = "test"): Unit = {
    println(s"Connecting to $host:$port/$db")
}

connectToDB()                          // 全用默认值
connectToDB(db = "production")         // 只改 db
connectToDB("192.168.1.1", 5432)       // 指定 host 和 port

嗯,这里要注意。调用时可以按位置传参,也可以按名称传参。按名称传参在参数很多时特别有用,可读性高。

避坑指南:我曾经在写一个工具类时,给默认参数传了可变对象。结果多个调用共享了同一个对象,导致数据污染。记住:默认参数在每次调用时都会重新求值,但如果是 valvar 定义的常量,只会初始化一次。

4.4 过程:没有返回值的函数

在 Scala 里,如果函数体用花括号括起来,但没有 = 号,那它就是个过程(Procedure)。过程的返回值类型是 Unit

// 这是函数,有返回值
def add(x: Int, y: Int): Int = x + y

// 这是过程,没有返回值(返回 Unit)
def printSum(x: Int, y: Int) {
    println(x + y)
}

我个人习惯,只要函数体有副作用(比如打印、写文件),就写成过程。这样看代码时一眼就能区分:有 = 的是纯函数,没有的是带副作用的。

不过,Scala 官方其实不推荐用过程语法。更推荐显式声明返回类型为 Unit

def printSum(x: Int, y: Int): Unit = {
    println(x + y)
}

这样更清晰,也符合函数式编程的风格。

4.5 惰性函数:用到时才算

惰性求值是 Scala 的一个高级特性。用 lazy 关键字修饰的 val,只有在第一次被访问时才会计算。

lazy val expensiveResource = {
    println("Loading expensive resource...")
    // 模拟耗时操作
    Thread.sleep(5000)
    "Resource loaded"
}

println("Before access")
println(expensiveResource)  // 这里才真正执行
println(expensiveResource)  // 第二次访问,直接返回缓存的值

输出结果:

Before access
Loading expensive resource...
Resource loaded
Resource loaded

你想想看,这有什么用?我在做大数据项目时,经常需要加载一些配置或模型文件。如果程序启动时全部加载,启动时间会很长。用 lazy 可以做到按需加载,只有真正用到时才去初始化。

应用场景:

  • 加载大型配置文件或模型
  • 建立数据库连接(可能用不上)
  • 初始化日志系统
  • Spark 中的广播变量(虽然 Spark 有自己的机制)

小提示:lazy 是线程安全的。多个线程同时访问时,只有一个线程会执行初始化代码,其他线程会等待。但要注意,如果初始化代码有副作用,可能会产生意想不到的结果。

4.6 实战经验总结

好,咱们把今天的内容串一下。Scala 的控制结构和函数,跟 Java 比,有几个关键差异:

特性 Java Scala
if 语句,无返回值 表达式,有返回值
for 仅循环 推导式,支持 yield
函数定义 方法必须属于类 独立函数,支持默认参数
惰性求值 不支持 lazy val

我在实际项目中,最常用的组合是:for + yield 做数据转换,lazy val 做资源懒加载,默认参数减少重载方法数量。这些特性用好了,代码量能减少 30% 以上。

嗯,今天就到这儿。下一章咱们聊聊集合操作,那才是 Scala 真正发力的地方。