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
这个函数描述了所有二维空间中的直线。如果我们想画出某一条具体的直线,就需要提供 a
和 b
的值,而 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=1
和 b=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。