Scala编程(第4版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

8.6 部分应用的函数

虽然前面的例子用下画线替换掉单独的参数,也可以用下画线替换整个参数列表。例如,对于println(_),也可以写成println _。参考下面的例子:

Scala会将这个简写形式当作如下完整形式看待:

因此,这里的下画线并非是单个参数的占位符,它是整个参数列表的占位符。注意你需要保留函数名和下画线之间的空格,否则编译器会认为你引用的是另一个符号,比如一个名为println_的方法,这个方法很可能并不存在。

当你这样使用下画线时,实际上是在编写一个部分应用的函数partially applied function)。在Scala中,当你调用某个函数,传入任何需要的参数时,实际上是应用那个函数这些参数上[4]。例如,给定如下的函数:

可以像这样对入参123应用函数sum

部分应用的函数是一个表达式,在这个表达式中,并不给出函数需要的所有参数,而是给出部分,或完全不给。举例来说,要基于sum创建一个部分应用的函数,假如你不想给出三个参数中的任何一个,可以在“sum”之后放一个下画线。这将返回一个函数,可以被存放到变量中。参考下面的例子:

有了这些代码,Scala编译器将根据部分应用函数sum _实例化一个接收三个整数参数的函数值,并将指向这个新的函数值的引用赋值给变量a。当你对三个参数应用这个新的函数值时,它将转而调用sum,传入这三个参数:

背后发生的事情是:名为a的变量指向一个函数值对象。这个函数值是一个从Scala编译器自动从sum _这个部分应用函数表达式生成的类的实例。由编译器生成的这个类有一个接收三个参数的apply方法。[5]生成的类的apply方法之所以接收三个参数,是因为表达式sum _缺失的参数个数为3。Scala编译器将表达式a(1, 2, 3)翻译成对函数值的apply方法的调用,传入这三个参数123。因此,a(1, 2, 3)可以被看作是如下代码的简写形式:

这个由Scala编译器从表达式sum _自动生成的类中定义的apply方法只是简单地将三个缺失的参数转发给sum,然后返回结果。在本例中,apply方法调用了sum(1, 2, 3),并返回sum的返回值,即6

我们还可以从另一个角度来看待这类用下画线表示整个参数列表的表达式,即这是一种将def变成函数值的方式。举例来说,如果你有一个局部函数,比如sum(a: Int, b: Int, c: Int): Int,可以将它“包”在一个函数值里,这个函数值拥有相同的参数列表和结果类型。当你应用这个函数值到某些参数时,它转而应用sum到同样的参数,并返回结果。虽然不能将方法或嵌套的函数直接赋值给某个变量,或者作为参数传给另一个函数,可以将方法或嵌套函数打包在一个函数值里(具体来说就是在名称后面加上下画线)来完成这样的操作。

至此,我们已经知道sum _是一个不折不扣的部分应用函数,可能你仍然感到困惑,为什么我们会这样称呼它。部分应用函数之所以叫作部分应用函数,是因为你并没有把那个函数应用到所有入参。拿sum _来说,你没有应用任何入参。不过,完全可以通过给出一些必填的参数来表达一个部分应用的函数。参考下面的例子:

在本例中,提供了第一个和最后一个参数给sum,但没有给出第二个参数。由于只有一个参数缺失,Scala编译器将生成一个新的函数类,这个类的apply方法接收一个参数。当我们用那个参数来调用这个新的函数时,这个生成的函数的apply方法将调用sum,依次传入1、传给当前函数的入参和3。参考下面的例子:

这里的b.apply调用了sum(1, 2, 3)

而这里的b.apply调用了sum(1, 5, 3)

如果你要的部分应用函数表达式并不给出任何参数,比如println _sum _,可以在需要这样一个函数的地方更加精简地表示,连下画线也不用写。例如,可以不用像这样来打印someNumbers(146页)中的每个数:

而是简单地写成:

最后这种形式只在明确需要函数的地方被允许,比如本例中的foreach调用。编译器知道这里需要的是一个函数,因为foreach要求一个函数作为入参。在那些并不需要函数的场合,尝试使用这样的形式会引发编译错误。参考下面的例子: