Facebook Twitter LinkedIn E-mail
magnify
Home 2013 十月

Scala开发教程(23): 函数–可变参数,命名参数,缺省参数

前面我们介绍的函数的参数是固定的,本篇介绍Scala函数支持的可变参数列表,命名参数和参数缺省值定义。
重复参数
Scala在定义函数时允许指定最后一个参数可以重复(变长参数),从而允许函数调用者使用变长参数列表来调用该函数,Scala中使用“*”来指明该参数为重复参数。例如:

scala> def echo (args: String *) =
     |   for (arg <- args) println(arg)
echo: (args: String*)Unit

scala> echo()

scala> echo ("One")
One

scala> echo ("Hello","World")
Hello
World

在函数内部,变长参数的类型,实际为一数组,比如上例的String * 类型实际为 Array[String]。 然而,如今你试图直接传入一个数组类型的参数给这个参数,编译器会报错:

scala> val arr= Array("What's","up","doc?")
arr: Array[String] = Array(What's, up, doc?)

scala> echo (arr)
<console>:10: error: type mismatch;
 found   : Array[String]
 required: String
              echo (arr)
                    ^

为了避免这种情况,你可以通过在变量后面添加 _*来解决,这个符号告诉Scala编译器在传递参数时逐个传入数组的每个元素,而不是数组整体。

scala> echo (arr: _*)
What's
up
doc?

命名参数
通常情况下,调用函数时,参数传入和函数定义时参数列表一一对应。

scala> def  speed(distance: Float, time:Float) :Float = distance/time
speed: (distance: Float, time: Float)Float

scala> speed(100,10)
res0: Float = 10.0

使用命名参数允许你使用任意顺序传入参数,比如下面的调用:

scala> speed( time=10,distance=100)
res1: Float = 10.0

scala> speed(distance=100,time=10)
res2: Float = 10.0

缺省参数值
Scala在定义函数时,允许指定参数的缺省值,从而允许在调用函数时不指明该参数,此时该参数使用缺省值。缺省参数通常配合命名参数使用,例如:

scala> def printTime(out:java.io.PrintStream = Console.out, divisor:Int =1 ) =
     | out.println("time = " + System.currentTimeMillis()/divisor)

printTime: (out: java.io.PrintStream, divisor: Int)Unit

scala> printTime()
time = 1383220409463

scala> printTime(divisor=1000)
time = 1383220422

 

Scala开发教程(22): 函数–闭包

到目前为止我们介绍的函数都只引用到传入的参数,假如我们定义如下的函数:

(x:Int) => x + more

这里我们引入一个自由变量more.它不是所定义函数的参数,而这个变量定义在函数外面,比如:

var more =1

那么我们有如下的结果:

scala> var more =1
more: Int = 1

scala> val addMore = (x:Int) => x + more
addMore: Int => Int = <function1>

scala> addMore (100)
res1: Int = 101

这样定义的函数变量addMore 成为一个“闭包”,因为它引用到函数外面定义的变量,定义这个函数的过程是将这个自由变量捕获而构成一个封闭的函数。有意思的是,当这个自由变量发生变化时,Scala的闭包能够捕获到这个变化,因此Scala的闭包捕获的是变量本身而不是当时变量的值。

比如:

scala> more =  9999
more: Int = 9999

scala> addMore ( 10)
res2: Int = 10009

同样的,如果变量在闭包在发生变化,也会反映到函数外面定义的闭包的值。比如:

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
res4: Int = -11

可以看到在闭包中修改sum的值,其结果还是传递到闭包的外面。

如果一个闭包所访问的变量有几个不同的版本,比如一个闭包使用了一个函数的局部变量(参数),然后这个函数调用很多次,那么所定义的闭包应该使用所引用的局部变量的哪个版本呢? 简单的说,该闭包定义所引用的变量为定义该闭包时变量的值,也就是定义闭包时相当于保存了当时程序状态的一个快照。比如我们定义下面一个函数闭包:

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

scala> val inc1=makeIncreaser(1)
inc1: Int => Int = <function1>

scala> val inc9999=makeIncreaser(9999)
inc9999: Int => Int = <function1>

scala> inc1(10)
res5: Int = 11

scala> inc9999(10)
res6: Int = 10009

当你调用makeIncreaser(1)时,你创建了一个闭包,该闭包定义时more的值为1, 而调用makeIncreaser(9999)所创建的闭包的more的值为9999。此后你也无法修改已经返回的闭包的more的值。因此inc1始终为加一,而inc9999始终为加9999.

 

Scala开发教程(21): 函数–部分应用的函数

前面例子中我们使用“_” 来代替单个的参数,实际上你也可以使用“_”来代替整个参数列表,比如说,你可以使用 print _ 来代替 println (_).

someNumbers.foreach(println _)

Scala编译器自动将上面代码解释成:

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

因此这里的“_” 代表了Println的整个参数列表,而不仅仅替代单个参数。
当你采用这种方法使用“_”,你就创建了一个部分应用的函数(partially applied function)。 在Scala中,当你调用函数,传入所需参数,你就把函数“应用”到参数。 比如:一个加法函数。

scala> def sum = (_:Int) + (_ :Int) + (_ :Int)
sum: (Int, Int, Int) => Int

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

一个部分应用的函数指的是你在调用函数时,不指定函数所需的所有参数,这样你就创建了一个新的函数,这个新的函数就称为原始函数的部分应用函数,比如说我们固定sum的第一和第三个参数,定义如下的部分应用函数:

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

scala> b(2)
res1: Int = 6

变量b的类型为一函数,具体类型为Function1 (带一个参数的函数),它是由sum 应用了第一个和第三个参数,构成的。
调用b(2),实际上调用sum (1 ,2,3)。

再比如我们定义一个新的部分应用函数,只固定中间参数:

scala> val c = sum (_:Int, 2, _:Int)
c: (Int, Int) => Int = <function2>

scala> c(1,3)
res2: Int = 6


变量c的类型为Function2,调用c(1,3) 实际上也是调用sum (1,2,3)。

在Scala中,如果你定义一个部分应用函数并且能省去所有参数,比如println _ ,你也可以省掉“_”本身,比如:

someNumbers.foreach(println _)

可以写成:

someNumbers.foreach(println)
 

Scala开发教程(20): 函数–函数字面量的一些简化写法

Scala提供了多种方法来简化函数字面量中多余的部分,比如前面例子中filter方法中使用的函数字面量,完整的写法如下:

 (x :Int ) => x +1

首先可以省略到参数的类型,Scala可以根据上下文推算出参数的类型,函数定义可以简化为:

 (x) => x +1

这个函数可以进一步去掉参数的括号,这里的括号不起什么作用:

 x => x +1

Scala 还可以进一步简化,Scala允许使用”占位符”下划线”_”来替代一个或多个参数,只要这个参数值函数定义中只出现一次,Scala编译器可以推断出参数。比如

scala> val someNumbers = List ( -11, -10, - 5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> someNumbers.filter(_ >0)
res0: List[Int] = List(5, 10)

可以看到简化后的函数定义为 _ > 0 ,你可以这样来理解,就像我们以前做过的填空题,“_”为要填的空,Scala来完成这个填空题,你来定义填空题。

有时,如果你使用_来定义函数,可能没有提供足够的信息给Scala编译器,此时Scala编译器将会报错,比如,定义一个加法函数如下:

scala> val f = _ + _
<console>:7: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2))
       val f = _ + _
               ^
<console>:7: error: missing parameter type for expanded function ((x$1: <error>, x$2) => x$1.$plus(x$2))
       val f = _ + _

Scala 编译器无法推断出_的参数类型,就报错了,但如果你给出参数的类型,依然可以使用_来定义函数,比如:

scala> val f = (_ :Int ) + ( _ :Int)
f: (Int, Int) => Int = <function2>

scala> f (5,10)
res1: Int = 15

因为_替代的参数在函数体中只能出现一次,因此多个“_”代表多个参数。第一个“_”代表第一个参数,第二个“_”代表第二个参数,以此类推。

 

Scala开发教程(19): 函数–头等公民

Scala中函数为头等公民,你不仅可以定义一个函数然后调用它,而且你可以写一个未命名的函数字面量,然后可以把它当成一个值传递到其它函数或是赋值给其它变量。下面的例子为一个简单的函数字面量(参考整数字面量,3 为一整数字面量)。

(x :Int ) => x +1

这是个函数字面量,它的功能为+1. 符好 => 表示这个函数将符号左边的东西(本例为一个整数),转换成符号右边的东西(加1)。
函数字面量为一个对象(就像3是个对象),因此如果你愿意的话,可以把这个函数字面量保持在一个变量中。这个变量也是一函数,因此你可以使用函数风格来调用它,比如:

scala> var increase = (x :Int ) => x +1
increase: Int => Int = <function1>

scala> increase(10)
res0: Int = 11

注意函数字面量 (x:Int) => x + 1的类型为 ,它在Scala内部表示为带有一个参数的类Function1的一个对象,其它如FunctionN代表带有N个参数的函数,Function0代表不含参数的函数类型。

如果函数定义需要多条语句,可以使用{},比如:

scala> var increase = (x :Int ) => {
     |    println("We")
     |    println("are")
     |    println("here")
     |    x + 1
     |    }
increase: Int => Int = <function1>

scala> increase (10)
We
are
here
res0: Int = 11

什么我们了解到了函数字面量的基本概念,它可以作为参数传递个其它函数,比如很多Scala的库允许你使用函数作为参数,比如foreach方法,它使用一个函数参数,为集合中每个运算调用传入的函数。例如:

scala> val someNumbers = List ( -11, -10, - 5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> someNumbers.foreach((x:Int) => println(x))
-11
-10
-5
0
5
10

再比如,Scala的集合也支持一个filter方法用来过滤集合中的元素,filter的参数也是一个函数,比如:

scala> someNumbers.filter( x => x >0)
res1: List[Int] = List(5, 10)

使用 x => x >0 ,过滤掉小于0 的元素。 如果你熟悉lambda表达式, x => x >0 为函数的lambda表达式。

 

Scala开发教程(18): 函数–局部函数

上个例子中ProcessFile使用了一个非常重要的设计原则–应用程序可以分解成多个小的函数,每个小的函数完成一个定义完好的功能。使用这程序设计风格可以使得程序员有相当数量的程序构造模块,通过这些小的构造模块的组合来完成较复杂的功能。每个小的构造模块一个应该足够简洁以帮助理解。
这样带来的一个问题是,这些小的辅助函数的名称可能会影响到程序空间,你不能在同个程序员使用两个相同名称的函数,即使你定义私有函数。如果你设计函数库,你也不希望有些辅助函数为库函数的用户直接调用,对于Java来说,你可以通过私有成员函数。而Scala除了支持私有成员函数外,还支持局部函数(其作用域和局部变量类似)。
也就是说可以在函数的内部再定义函数,如同定义一个局部变量,例如,修改前面的ProcessFile的例子如下:

import scala.io.Source
object LongLines {
  def processFile(filename: String, width: Int) {
    def processLine(filename:String,
     width:Int, line:String){
     if(line.length > width)
       println(filename + ":" +line.trim)
   }
 
    val source= Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename,width,line)
   }
 

}

这个例子不私有成员函数processLine移动到processFile内部,成为一个局部函数,也正因为processLine变成了processFile的一个局部函数,因此processLine 可以直接访问到processFile的参数 filename和width,因此代码可以进一步优化如下:

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)
   }
}

代码变得更简洁,是不是,局部函数的作用域和局部变量作用域一样,局部函数访问包含该函数的参数是非常常见的一种嵌套函数的用法。