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
嗯,这里要注意。调用时可以按位置传参,也可以按名称传参。按名称传参在参数很多时特别有用,可读性高。
避坑指南:我曾经在写一个工具类时,给默认参数传了可变对象。结果多个调用共享了同一个对象,导致数据污染。记住:默认参数在每次调用时都会重新求值,但如果是 val 或 var 定义的常量,只会初始化一次。
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 真正发力的地方。