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

⚠️ 注意:按名调用的参数在每次使用时都会重新求值一次,这一点与按值调用不同。

按名调用的主要优势在于:

  • 参数表达式可能不会被使用,从而避免不必要的计算
  • 更适合用于控制结构(比如 ifwhile)或惰性求值的场景

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


原始标题:By-Value and By-Name Parameters in Scala