前言
本节将介绍Scala中的函数和闭等相关知识。
环境:
Windows + Scala-2.12.8
代码见GitHub:
1. 方法
定义函数的最常用的方式是作为某个对象的成员,这样的函数被称为方法。下面给出一个示例,打印超出指定长度的行。
文件:8.1.scala - 注:该文件属于完整的Scala应用程序(main入口),为了简单操作,依然使用执行脚本的方式
内容包括中文会报错。
// println line which over width
import scala.io.Source
object LongLines {
def processFile(filename: String, width: Int) = {
val source = Source.fromFile(filename)
for (line <- source.getLines())
processLine(filename, width, line)
}
private def processLine(filename: String,
width: Int, line: String) = {
if (line.length > width)
println(filename + ": " + line.trim)
}
}
object FindLongLines {
def main(args: Array[String]) = {
val width = args(0).toInt
for (arg <- args.drop(1))
LongLines.processFile(arg, width)
}
}
以脚本的方式执行:
2. 局部函数
在Scala中,局部函数就和局部变量一样,只允许在定义该局部函数的函数中使用,外部无法使用。并且,局部函数可以使用外层函数的类参数。
修改上面的例子:
文件:8.2
// Local function
import scala.io.Source
object LongLines {
def processFile(filename: String, width: Int) = {
def processLine(line: String) = {
if (line.length > width)
println(filename + ": " + line.trim)
}
val source = Source.fromFile(filename)
for (line <- source.getLines())
processLine(line)
}
}
object FindLongLines {
def main(args: Array[String]) = {
val width = args(0).toInt
for (arg <- args.drop(1))
LongLines.processFile(arg, width)
}
}
3. 一等函数
Scala支持一等函数。不仅可以定义函数并调用它们,还可以使用匿名的字面量来编写函数并将它们作为值传递。
函数字面量被编译成类,并在运行时实例化成函数值。因此,函数字面量和函数值的区别在于,函数字面量存在于源代码,函数值以对象的形式存在于运行时。
函数字面量示例:
(x: Int) => x + 1
函数值示例(increase就是函数值):
var increase = (x: Int) => x + 1
另外,如果想要在函数字面量中包含多条语句,可以使用花括号,每条语句占一行:
再如,Scala很多类库都支持使用函数字面量,如foreach 方法支持传入一个函数作为实参:
4. 函数字面量的简写形式
Scala中提供了多个省去冗余信息,更加简要的编写函数的方式。
如略去参数类型声明,同时根据Scala的自动类型推断省去括号。改写foreach :
5. 占位符语法
为了让函数字面量更加精简,还可以使用下划线作为占位符,用来表示一个或多个参数,只要满足每个参数字面量中出现一次即可。例如:
有时候当你用下划线为占位符时,编译器可能没有足够多的信息来推断缺失的类型,如:
不过,你可以给出类型,如:
多个下划线意味着多个参数。
6. 部分应用的函数
当你使用下划线时,实际上是在编写一个部分应用的函数。在Scala中,当你调用某个函数,传入任何需要的参数时,你实际上是应用那个函数到这些参数上。
如平常的做法:
部分应用的函数是一个表达式,在这个表达式中,并不给出函数需要的所有参数,而是给出部分或不给。
如,基础sum 创建一个部分应用的函数:
上述代码的过程:变量a 指向一个函数值对象。这个函数值是一个从Scala 编译器自动从sum _ 这个部分应用函数表达式生成的类的实例。由编译器生成的这个类有一个接收三个参数的apply 方法。生成的类的apply 方法之所以接收三个参数,是因为表达式sum _ 缺失的参数个数为3。编译器将a(1, 2, 3) 翻译成对函数值的apply方法的调用,即a.apply(1, 2, 3):
部分应用函数之所以叫部分应用函数是因为如sum _ 表示并没有把函数应用到所有的入参。不过,我们可以写一个只有一个参数缺失:
在某些地方,你甚至可以省去下划线,如:
这种方式只在明确需要函数的地方被允许,比如在一个不需要函数的地方:
7. 闭包
到目前为止,所有的函数字面量都只是引用了传入的参数。如:(x: Int) => x + 1,唯一用到的变量是x,不过也可以引用其他地方的变量,称为闭包:
其中 x 称为绑定变量,more称为自由变量。
(x: Int) => x + more
这个函数将more 也作为入参。如:
你可以发现,改变more 后,同时改变了add 方法引用more的值。因为,闭包捕获的是变量本身,而不是变量引用的值。即闭包能够看到闭包之外对more 的修改。同理,闭包对捕获的变量的修改也能在闭包外看到,如:
闭包之外的sum 可以看到闭包对sum 的改变。
8. 特殊的函数调用形式
到目前为止,大多数函数都是固定数量的形参,不过Scala 也支持其他形式的参数。
-
重复参数
简单理解为传入一个可变长度的参数列表。需要在参数类型之后叫上 * :
args 的类型实际是Array[String]。不过,你不能传递一个Array[String]类型的实参。
解决办法:
-
带名字的参数
与平常的定义一样,你可能习惯这样做:
但是,另一种调用方法是在每个实参前加上参数名和等号,这样就可以改变顺序:
-
缺省参数
缺省参数即你可以在实参中不给出值,但意味着你需要在函数的定义出给它一个默认值,如:
8.3.scala
// default parameter
def printTime(out: java.io.PrintStream = Console.out) =
out.println("time = " + System.currentTimeMillis())
printTime()
另一个示例:
8.4.scala
// default parameter
def printTime(out: java.io.PrintStream = Console.out,
divisor: Int = 1) =
out.println("time = " + System.currentTimeMillis() / divisor)
printTime()
printTime(divisor = 1000)
9. 尾递归
尾递归就是一个递归函数,但函数的最后必须是调用自己的函数(单纯调用自己,比如下面有斐波那锲的测试)。
例如一个尾递归函数:
非尾递归函数:
可以看出,尾递归函数抛出异常时只有1个栈帧,非尾递归函数有4个栈帧。是因为,尾递归函数是优化后的递归函数,如果你了解过递归函数会知道,递归函数会开启多个栈(在该层之下,再开启一层栈,类似于下楼),直到最后一个栈计算完后,才会一层一层的返回上层栈(类似于上楼)。而在Scala中,Scala编译器会优化尾递归函数,具体为检测到尾递归并将它跳转到函数的开始,并在跳转前将参数更新为新的值(即不开启新的栈)。
想到这里,猜测斐波那契数列是否是尾递归函数:
结果显示,Scala编译器并不认为它是尾递归函数。