1. 概述
在本教程中,我们将深入探讨 Scala 中 按值调用(Call By-Value) 和 按名调用(Call By-Name) 的特性、求值策略,并通过实际示例展示它们的使用方式。
2. 按值调用(Call By-Value)
默认情况下,Scala 函数参数是按值调用的。也就是说,在函数调用时,参数表达式会先被求值,然后将结果传递给函数体。
来看一个简单的函数定义:
def test(a: Int) = a * a
按值调用的行为可以总结为以下几点:
- 参数从左到右依次求值
- 每个参数只被求值一次
- 然后将这些值代入函数体中进行运算
✅ 这种方式在大多数情况下是高效且安全的,特别是对于纯函数来说。
3. 按名调用(Call By-Name)
按名调用则是延迟求值的策略,参数不会在函数调用时立即计算,而是等到在函数体中真正使用时才进行求值。
要定义一个按名调用的参数,只需要在类型前加上 =>
符号(也就是“火箭符号”):
def test(a: => Int) = a * a
⚠️ 注意:按名调用的参数在每次使用时都会重新求值一次,这一点与按值调用不同。
按名调用的主要优势在于:
- 参数表达式可能不会被使用,从而避免不必要的计算
- 更适合用于控制结构(比如
if
、while
)或惰性求值的场景
4. 示例对比
我们来定义一个函数 addFirst
,它接受两个参数:
- 第一个参数
x
是按值调用 - 第二个参数
y
是按名调用
def addFirst(x: Int, y: => Int) = x + x
示例 1:按值调用优先求值
assert(addFirst(3+5, 7) == 16)
✅ 执行过程:
3+5
先被求值得到8
- 然后代入函数体中:
x + x = 8 + 8 = 16
示例 2:按名调用延迟求值
assert(addFirst(7, 3+5) == 14)
✅ 执行过程:
7
直接传入3+5
并未被求值,因为y
没有在函数体中使用- 所以最终结果是
7 + 7 = 14
示例 3:避免无限递归的陷阱
我们定义一个无限递归函数:
def infinite(): Int = 1 + infinite()
如果按值调用传入它:
assertThrows[StackOverflowError] {
addFirst(infinite(), 4)
}
❌ 会直接抛出栈溢出异常,因为 infinite()
在传入前就被执行了。
但如果按名调用传入它:
assert(addFirst(4, infinite()) == 8)
✅ 正常执行!因为 infinite()
没有被真正使用,所以不会触发递归。
5. 按值调用 vs 按名调用
虽然按名调用看起来更“聪明”,但并不总是最优选择:
特性 | 按值调用(Call By-Value) | 按名调用(Call By-Name) |
---|---|---|
求值时机 | 函数调用前 | 函数体内使用时 |
求值次数 | 仅一次 | 每次使用都重新求值 |
性能 | 更稳定、通常更快 | 可能更慢(重复求值) |
适用场景 | 大多数普通函数 | 控制结构、惰性求值场景 |
副作用处理 | 可预测 | 可能导致副作用重复发生 |
⚠️ 所以,除非你明确需要延迟求值的语义,否则建议使用按值调用。
6. 小结
在这篇教程中,我们介绍了 Scala 中按值调用和按名调用的基本概念、语法形式、执行机制以及适用场景。这两种求值策略虽然看起来简单,但在函数式编程中扮演着重要角色。
- 按值调用适合大多数常规函数调用
- 按名调用则适合实现惰性求值、控制结构等高级特性
完整代码示例可以在 GitHub 上找到:Baeldung/scala-tutorials