Scala编程(第八章:函数和闭包)

it2022-05-05  52

1.方法:定义函数最常用的方式是作为某个对象的成员,这样的函数称为方法。

 

2.局部函数:程序应该被分解成许多小函数,每个函数都只做明确定义的任务。每个构建单位都应该足够简单,简单到能够单独理解的程度。这种方式的一个问题是助手函数的名称会污染整个程序的命名空间。它们离开了类和对象单独存在时通常都没什么意义,而且通常你都希望在后续采用其他方式重写该类时,保留删除助手函数的灵活性。Scala可以在某个函数内部定义函数:

def addThree(a:Int,b:Int,c:Int): Int={ def addTwo()=a+b addTwo()+c }

作为局部函数,addTwo在addThree内有效,但不能从外部访问。局部函数可以访问包含它们的函数的参数。

 

3.一等函数:Scala支持一等函数。不仅可以定义函数并调用它们,还可以用匿名的字面量来编写函数并将它们作为值进行传递。函数字面量被编译成类,并在运行时实例化成函数值。因此,函数字面量和函数值的区别在于,函数字面量存在于源码,而函数值以对象形式存在于运行时。以下是一个对某个数加1的函数字面量的简单示例:

(x:Int)=>x+1

函数值是对象,所以可以将它们存放在变量中。它们同时也是函数,所以也可以用常规的圆括号来调用它们:

scala> var increase=(x:Int)=>{x+1} increase:Int=>Int = <function1> scala> increase(10) res0: Int=11

 

4.函数字面量的简写形式:Scala提供了多个省去冗余信息,更简要地编写函数的方式。如:

someNumbers=List(1,2,3,4) scala> someNumbers.filter(x=>x>2) res5:List[Int] = List(3,4)

Scala编译器知道x必定是整数,因为它看到你立即用这个函数来过滤一个由整数组成的列表。这被称作目标类型,因为一个表达式的目标使用场景可以影响该表达式的类型。随着时间的推移,你会慢慢有感觉,什么时候编译器能帮你推断出类型,什么时候不可以。

 

5.占位符语法:为了让函数字面量更加精简,还可以使用下划线作为占位符,用来表示一个或多个参数,只要满足每个参数只在函数字面量中出现一次即可。例如,_>2是一个非常短的表示法,表示一个检查某个值是否大于0的函数:

scala> someNumbers.filter(_>2) res7:List[Int]=List(3,4)

可以将下划线当成是表达式中的需要被“填”的“空”。函数每次调用,这个“空”都会被一个入参“填”上。someNumbers中的元素依次填入“_”进行筛选,等价于x=>x>2。有时候当你用下划线为参数占位时,编译器可能并没有足够多的信息来推断缺失的参数类型。例如,假定你只是写了_+_:

val f=_+_

会报错。在这类情况下,可以用冒号来给出类型,就像这样:

val f=(_:Int)+(_:Int) println(f(5,10)) //打印:15

注意,_+_将会展开成一个接收两个参数的函数字面量。多个下划线意味着多个参数,而不是对单个参数的重复使用。第一个下划线代表第一个参数,第二个下划线代表第二个参数,以此类推。

 

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

someNumbers.foreach(println _)

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

someNumbers.foreach(x=>println(x))

因此这里的下划线并非是单个参数的占位符,它是整个参数列表的占位符。注意你需要保留函数名和下划线之间的空格,否则编译器会认为你引用的是另一个符号,比如一个名为println_的方法,这个方法很可能并不存在。当这样使用下划线的时,实际上是在编写一个部分应用的函数。在Scala中,当你调用某个函数,传入任何需要的参数时,你实际上是应用那个函数到这些参数上。例如,给定如下函数:

scala> def sum(a:Int,b:Int,c:Int)=a+b+c sum:(a:Int,b:Int,c:Int)Int

可以像这样对入参1、2和3应用函数sum:

scala> sum(1,2,3) res10: Int=6

部分应用的函数是一个表达式,在这个表达式中,并不给出函数需要的所有参数,而是给出部分,或完全不给:

scala> val a=sum _ a:(Int,Int,Int)=>Int= <function3>

当你对三个参数应用这个新的函数值时,它将转而调用sum:

scala> a(1,2,3) res12:Int=6

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

scala> a.apply(1,2,3) res12:Int=6

这个由Scala编译器从表达式sum _自动生成的类中定义的apply方法只是简单地将三个缺失的参数转发给sum,然后返回结果。这是一种将def变成函数值的方式。虽然不能将方法或嵌套的函数直接赋值给某个变量,或者作为参数传给另一个函数,可以将方法或嵌套函数打包在一个函数值里(具体来说就是在名称后面加上下划线)来完成这样的操作。之所以叫部分应用函数,可以部分入参:

scala>val b=sum(1,_:Int,3) b:Int=>Int= <function1>

由于只有一个参数缺失,Scala编译器将生成一个新的函数类,这个类的apply方法接收一个参数。当我们用那个参数来调用这个新的函数时,这个生成的函数的apply方法将调用sum,依次传入1、传给当前函数的入参和3.如:

scala> b(5) res12:Int=9

这里的b.apply调用了sum(1,5,3)。如果你要的部分应用函数表达式并不给出任何参数,比如println _或sum _,可以连下划线也不用写:

someNumbers.foreach(println)

这里的foreach,编译器知道要传的是一个函数,因为foreach要求一个函数作为入参。在那些并不需要函数的场合,尝试使用这样的形式会引发编译错误。如:

val c=sum

会报错。

 

7.闭包:本章到目前为止,所有的函数字面量示例,都只是引用了传入的参数。也可以引用自由变量:

scala> var more=1 more:Int=1 scala> val addMore=(x:Int)=>x+more addMore:Int=>Int=<function1> scala>addMore(10) res16:Int=11

运行时从这个函数字面量创建出来的函数值(对象)被称作闭包。该名称源于“捕获”其自由变量从而“闭合”该函数字面量的动作。运行时从任何带有自由变量的函数字面量,要求捕获到它的自由变量more的绑定,相应的函数值结果(包含指向被捕获的more变量的引用)就称作闭包,因为函数值是通过闭合这个开放语的动作产生的。如果改变more会发生什么:

scala> more=9999 more:Int=9999 scala>addMore(10) res17:Int=10009

很符合直觉的是,Scala的闭包捕获的是变量本身,而不是变量引用的值。正如示例,创建的闭包能够看到闭包外对more的修改。反过来也是成立的:

scala> val someNumbers =List(-11,-10,-5,0,5,10) someNumbers:List[Int]=List(-11,-10,-5,0,5,10) scala> var sum=0 sum:Int=0 scala>someNumbers.foreach(sum+=_) scala>sum res19:Int=-11

闭包对捕获到的变量的修改也能在闭包外被看到。闭包引用的实例是在闭包被创建时最活跃的那一个。如:

def makeIncreaser(more:Int)=(x:Int)=>x+more

该函数每调用一次,就会创建一个闭包。每个闭包都会访问那个在它创建时活跃的变量more:

scala> val inc1=makeIncreaser(1) inc1:Int=>Int=<function1> scala> val inc9999=makeIncreaser(9999) inc9999:Int=>Int=<function1> scala> inc1(10) res20:Int=11 scala> inc9999(10) res21:Int=10009

这里more是某次方法调用的入参,而方法已经返回了,不过这并没有影响。Scala编译器会重新组织和安排,让被捕获的参数在堆上继续存活。

 

8.特殊的函数调用形式:由于函数调用在Scala编程中的核心地位,对于某些特殊的需求,一些特殊形式的函数定义和调用方式也被加到了语言当中。Scala支持重复参数、带名字的参数和缺省参数。

重复参数:Scala允许你标识出函数的最后一个参数可以被重复。这让我们可以对函数传入一个可变长度的参数列表。要表示这样一个重复参数,需要在参数的类型之后加上一个星号(*)。例如:

scala> def echo(args:String*)=for (arg<-args) println(arg) echo:(args:String*)Unit scala> echo("one") one scala>echo("hello","world!") hello world!

在函数内部,这个重复参数的类型是一个所声明的参数类型的Array。因此,在echo内部,args的类型其实是Array[String]。如果要传入一个数组,你需要在数组实参的后面加上冒号和一个_*符号,就像这样:

val arr=Array("hello","world!") scala> echo(arr:_*) hello world!

带名字的参数:在一个普通的函数调用中,实参是根据被调用的函数的参数定义,逐个匹配起来的。带名字的参数让你可以用不同的顺序将参数传给函数。其语法是简单地在每个实参前加上参数名和等号。如:

def speed(distance:Float,time:Float)= distance/time scala> speed(time=10,distance=100) res28:Float=10.0

带名字的参数最常见的场合是跟缺省参数值一起使用。 

缺省参数值:Scala允许你给函数参数指定缺省值。这些有缺省值的参数可以不出现在函数调用中,对应的参数将会被填充为缺省值。如:

def printTest(a:Int=3,b:Int)= a/b println(printTest(b=1)) //打印:3

 

9.尾递归:尾递归函数并不会在每次调用时构建一个新的栈帧,所有的调用都会在同一个栈帧中执行。尾递归优化仅适用于某个方法或嵌套函数在最后一步直接调用自己,并且没有经过函数值或其他中间环节的场合。如求阶乘:

def tailRecursion(i:BigInt,sum:BigInt=1):BigInt={ if (i<=1) sum else tailRecursion(i-1,sum*i) }

Scala编译器能够检测到尾递归并将它替换成跳转到函数的最开始,并在跳转之前将参数更新为新的值。


最新回复(0)