1. 简介

在 Scala 中,函数(Function)和方法(Method)虽然概念相似,但在使用上存在显著差异。

本篇文章将深入探讨它们的定义方式、使用场景以及关键区别。

2. 函数

函数是可调用的代码单元,它可以接受单个参数、多个参数,或者不接受参数。函数可以执行一条或多条语句,并返回一个值、多个值,或者无返回值。

我们可以通过函数来复用代码逻辑,避免重复编写。

2.1. 匿名函数

匿名函数是没有名称的函数,通常用于将一小段代码作为参数传递给其他函数。

举个例子:

(number: Int) => number + 1

左侧括号中定义了参数列表,右侧箭头 => 后面是表达式或语句块。

参数列表可以为空 (),也可以定义多个参数:

() => scala.util.Random.nextInt
(x: Int, y: Int) => (x + 1, y + 1)

如果函数体包含多个语句,则需要用大括号 {} 包裹。Scala 中函数的返回值由最后一行表达式的值决定。

虽然 Scala 提供了 return 关键字,但很少使用:

(number: Int) => {
    println("We are in a function")
    number + 1
}

上面这个函数接受一个 Int 类型的参数 number,打印一行日志后返回 number + 1。由于类型推断机制,返回值会自动识别为 Int

2.2. 命名函数

在 Scala 中,一切都是对象,因此我们可以将函数赋值给变量:

val inc = (number: Int) => number + 1

现在 inc 就是一个命名函数。我们可以在需要的地方直接调用它:

scala> println(inc(10))
11

⚠️ 背后其实有“魔法”发生:Scala 会自动将函数包装成 Function1, Function2, ..., FunctionN 类型的对象,其中 N 表示参数个数。

这些对象内部包含一个 apply() 方法,用于执行函数逻辑。调用函数时的括号其实是对 apply() 的语法糖:

scala> println(inc(10))
11
scala> println(inc.apply(10))
11

这两个写法效果完全一致。

2.3. 闭包(Closure)

当一个函数的返回值依赖于其外部环境中的变量时,我们就称这个函数为闭包。

闭包本质上是一种模拟对象行为的纯函数式机制,允许开发者将函数连同上下文一起传递。

来看一个例子:

def plot(f: Double => Double) = { // Some implementation }

这里 plot 函数接受一个函数 f(x) 并进行可视化处理。

再看一个二维线性方程的定义:

val lines: (Double, Double, Double) => Double = (a,b,x) => a*x + b

这个函数描述了所有二维空间中的直线。如果我们想画出某一条具体的直线,就需要提供 ab 的值,而 x 是变量。

然而,plot 函数只接受一个参数为 Double 且返回值也为 Double 的函数。此时闭包就派上用场了:

val line: (Double, Double) => Double => Double = (a,b) => x => lines(a,b,x)

这里的 line 是一个高阶函数,它返回另一个函数 Double => Double,正好可以传入 plot

我们可以这样使用:

val a45DegreeLine = line(1,0)

此时 a45DegreeLine 是一个闭包函数,它捕获了 a=1b=0 的上下文,接收 x 值并返回对应的 y 值。

最终我们就可以调用绘图功能来绘制这条直线:

plot(a45DegreeLine())

3. 方法

方法本质上是类结构的一部分,可以被重写,且语法与函数略有不同。Scala 不支持匿名方法。

定义方法时必须使用 def 关键字:

def inc(number: Int): Int = number + 1

方法名后可以定义参数列表,用括号包裹;冒号后可指定返回类型;等号后是方法体。

如果方法体只有一行代码,可以省略花括号:

def randomIntMethod(): Int = scala.util.Random.nextInt
val randomIntFunction = () => scala.util.Random.nextInt

注意:无参方法可以省略括号,但调用时的行为有所不同:

scala> println(randomIntMethod)
1811098264
scala> println(randomIntFunction)
$line12.$read$$iw$$iw..$$iw$$iw$$$Lambda$4008/0x00000008015cac40@767ee25d
scala> println(randomIntFunction())
1292055875

可以看到,直接写方法名会自动调用该方法,而函数名只是一个引用对象。如果要将方法转换为函数,可以使用下划线 _

val incFunction = inc _

此时 incFunction 是一个函数对象,可以直接像其他函数一样使用:

scala> println(incFunction(32))
33

❌ 反过来,无法将函数转换为方法

3.1. 嵌套方法

Scala 允许在一个方法内部定义另一个方法:

def outerMethod() = {
    def innerMethod() = {
        // inner method's statements here
    }

    // outer method's statements
    innerMethod()
}

嵌套方法适用于那些与外层方法紧密耦合的辅助逻辑,可以让代码结构更清晰。

比如递归实现阶乘:

import scala.annotation.tailrec

def factorial(num: Int): Int = {
    @tailrec
    def fact(num: Int, acc: Int): Int = {
        if (num == 0) 
            acc
        else
            fact(num - 1, acc * num)
    }

    fact(num, 1)
}

通过嵌套方法隐藏辅助参数(如累加器),对外暴露简洁接口。

3.2. 泛型参数化

Scala 支持通过类型参数定义泛型方法,提高代码复用性:

def pop[T](seq: Seq[T]): T = seq.head

这里 [T] 表示类型参数。我们可以对任意类型的序列调用该方法:

scala> val strings = Seq("a", "b", "c")
scala> val first = pop(strings)
first: String = a

scala> val ints = Seq(10, 3, 11, 22, 10)
scala> val second = pop(ints)
second: Int = 10

编译器会根据传入的参数自动推断类型,返回值也会相应匹配。

3.3. 扩展方法(Extension Method)

Scala 的隐式转换特性允许我们为已有类型添加新方法。

做法是定义一个包装类并标记为 implicit,然后扩展自 AnyVal 避免额外开销:

implicit class IntExtension(val value: Int) extends AnyVal {
    def isOdd = value % 2 == 0
}

这个类被称为值类(value class),它只有一个参数,并且只能包含方法。

当我们导入该类后,就可以像调用原生方法一样使用新增的方法:

scala> 10.isOdd
res1: Boolean = true
scala> 11.isOdd
res2: Boolean = false

虽然 Int 类型本身没有 isOdd 方法,但编译器会自动查找隐式转换,将其转换为 IntExtension 并调用对应方法。

4. 按名称传递参数(By-Name Parameters)

默认情况下,Scala 使用“按值传递”(by-value),即参数在传入函数前就会被求值。

例如:

def byValue(num: Int) = {
    println(s"First access: $num. Second access: $num")
}
scala> byValue(scala.util.Random.nextInt)
First access: 1705836657. Second access: 1705836657

可以看到两次访问都得到了相同的随机数。

但如果使用“按名称传递”(by-name),则每次访问参数时都会重新求值:

def byName(num: => Int) = {
    println(s"First access: $num. Second access: $num")
}
scala> byName(scala.util.Random.nextInt)
First access: 1349966645. Second access: 236297674

✅ 通过在参数类型前加 => 来启用按名称传递,适用于惰性求值或副作用控制的场景。

5. 总结

本文介绍了 Scala 中函数与方法的定义方式及其主要差异:

  • ✅ 函数是一等公民,支持匿名、闭包、赋值;
  • ❌ 方法属于类成员,不能匿名,可通过 _ 转换为函数;
  • 🔄 闭包用于封装上下文状态;
  • 🔧 扩展方法借助隐式转换增强已有类型;
  • ⏱️ 按名称传递参数可用于延迟求值。

完整示例代码见 GitHub


原始标题:Functions and Methods in Scala