scala编程(八)——函数和闭包

江枫思渺然 提交于 2020-02-24 15:25:28

当程序变得庞大时,你需要一些方法把它们分割成更小的,更易管理的片段。为了分割控制流,Scala 提供了所有有经验的程序员都熟悉的方式:把代码分割成函数。实际上,Scala 提供了许多 Java 中没有的定义函数的方式。除了作为对象成员函数的方法之外,还有内嵌在函数中的函数, 函数文本和函数值。本章带你体会所有 Scala 中的这些函数的风味。

方法

定义函数最通用的方法是作为某个对象的成员。这种函数被称为方法:method。

作为例子,示例代码  展示了两个可以合作根据一个给定的文件名读文件并打印输出所有长度超过给定宽度的行的 方法。每个打印输出的行前缀它出现的文件名:

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

  def main(args: Array[String]): Unit = {
    processFile("E:/idea/scala/data/demo",10)
  }
}

打印结果如下(局部):

E:/idea/scala/data/demo: println(filename+": "+line.trim)
E:/idea/scala/data/demo: def main(args: Array[String]): Unit = {
E:/idea/scala/data/demo: processFile("E:/idea/scala/data/demo.scala",10)

 

本地函数

Scala 提供了另一种方式:你可以把函数定义在另一个函数中。就好象本地变量那样,这种本地函数仅在包含它的代码块中可见。

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


  def main(args: Array[String]): Unit = {
    processFile("E:/idea/scala/data/demo",10)
  }
}

,因为本地函数可以访问包含它们的函数的参数。你可以直接使用外部 processLine 函数的参数

 

函数是第一类值

Scala 拥有第一类函数:first-class function。你不仅可以定义函数和调用它们,还可以把函数写 成没有名字的文本:literal 并把它们像值:value 那样传递。

 

函数文本被编译进一个类,类在运行期实例化的时候是一个函数值:function value。任何函数值都是某个扩展了若干 scala 包的 FunctionN 特质之一的类的实例,如 Function0 是没有参数的函数, Function1 是有一个参数的函数等等。每个 FunctionN 特质有一个 apply 方法用来调用函数。 因此函数文本和值的区别在于函数文本存在于源代码,而函数值存在于运行期对象。这个区别很像类(源 代码)和对象(运行期)的那样。

以下是对数执行递增操作的函数文本的简单例子:

(x: Int) => x + 1

=>指明这个函数把左边的东西(任何整数 x)转变成右边的东西(x + 1)。所以,这是一个把任 何整数 x 映射为 x + 1 的函数。

函数值是对象,所以如果你愿意可以把它们存入变量。它们也是函数,所以你可以使用通常的括 号函数调用写法调用它们。

  def main(args: Array[String]): Unit = {
    var increase = (x:Int) => x+1
    println(increase(10))
  }

打印结果为11;

如果你想在函数文本中包括超过一个语句,用大括号包住函数体,一行放一个语句,就组成了一 个代码块。与方法一样,当函数值被调用时,所有的语句将被执行,而函数的返回值就是最后一 行产生的那个表达式。

 def main(args: Array[String]): Unit = {
    var increase = (x:Int) => x+1
    println(increase(10))

    increase = (x:Int) => {
      println("we")
      println("are")
      println("here")
      x+100
    }

    println(increase(100))
  }

打印结果如下:

11
we
are
here
200

 

函数文本的短格式

一种让函数文本更简短的方式是去除参数类型

  def main(args: Array[String]): Unit = {
    val inclusives = List(1,2,3,4,5,6,7,8,9,10)
    val ints: List[Int] = inclusives.filter((x) => x > 5)
    ints.foreach(println)
  }

Scala 编译器知道 x 一定是整数,因为它看到你立刻使用了这个函数过滤整数列表(由 someNumbers 暗示)。这被称为目标类型化:target typing。因为表达式的目标使用——本例中 someNumbers.filter()的参数——影响了表达式的类型化——本例中决定了 x 参数的类型

 第二种去除无用字符的方式是省略类型是被推断的参数之外的括号。前面例子里,x 两边的括号 不是必须的

  def main(args: Array[String]): Unit = {
    val inclusives = List(1,2,3,4,5,6,7,8,9,10)
    val ints: List[Int] = inclusives.filter(x => x > 5)
    ints.foreach(println)
  }

 

占位符语法

如果想让函数文本更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数文本内仅出现一次。(这种情况下可以根据调用者推断出参数的类型)

  def main(args: Array[String]): Unit = {
    val inclusives = List(1,2,3,4,5,6,7,8,9,10)
    val ints: List[Int] = inclusives.filter(_ > 5)
    ints.foreach(println)
  }

有时你把下划线当作参数的占位符时,编译器有可能没有足够的信息推断缺失的参数类型。例如, 假设你只是写_ + _:

 val f = _ + _

这种情况下,你可以使用冒号指定类型,如下:

    val add = (_:Int)+(_:Int)

请注意_ + _将扩展成带两个参数的函数文本。这也是仅当每个参数在函数文本中最多出现一次的情况下你才能使用这种短格式的原因。多个下划线指代多个参数,而不是单个参数的重复使用。 第一个下划线代表第一个参数,第二个下划线代表第二个,第三个……,如此类推。

偏应用函数

尽管前面的例子里下划线替代的只是单个参数,你还可以使用一个下划线替换整个参数列表。例 如,写成 println(_),或者更好的方法你还可以写成 println _。下面是一个例子:

ints.foreach(println _)

这个例子中的下划线不是单个参数的占位符。它是整个参数列表的占位符。请记住要在函 数名和下划线之间留一个空格,因为不这样做编译器会认为你是在说明一个不同的符号,比方说 是,似乎不存在的名为 println_的方法。

以这种方式使用下划线时,你就正在写一个偏应用函数:partially applied function。Scala 里, 当你调用函数,传入任何需要的参数,你就是在把函数应用到参数上。

给定下列函数:

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

你就可以把函数 sum 应用到参数 1,2 和 3 上,如下:

sum(1, 2, 3)

偏应用函数是一种表达式,你不需要提供函数需要的所有参数。代之以仅提供部分,或不提供所 需参数。比如,要创建不提供任何三个所需参数的调用 sum 的偏应用表达式,只要在“sum”之 后放一个下划线即可。然后可以把得到的函数存入变量。举例如下:

 val a = sum _

有了这个代码,Scala 编译器以偏应用函数表达式,sum _,实例化一个带三个缺失整数参数的函 数值,并把这个新的函数值的索引赋给变量 a。当你把这个新函数值应用于三个参数之上时,它 就转回头调用 sum,并传入这三个参数:

def main(args: Array[String]): Unit = {
    def sum(a:Int,b:Int,c:Int) = a+b+c;

    var a = sum _;

    println(a(1,2,3))
  }

输出6;

实际发生的事情是这样的:名为a的变量指向一个函数值对象。这个函数值是由Scala编译器依照 偏应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带三个参 数。4 如果你正在写一个省略所有参数的偏应用程序表达式,如 println _或 sum _,而且在代码的那 个地方正需要一个函数,你可以去掉下划线从而表达得更简明。例如,代之以打印输出 someNumbers 里的每一个数字(定义在第 之所以带三个参数是因为sum _表达式缺少的参数数量为三。Scala编译器把表达式a(1,2,3) 翻译成对函数值的apply方法的调用,传入三个参数 1,2,3。因此 a(1,2,3)是下列代码的短格式:

 a.apply(1, 2, 3)

Scala 编译器根据表达式 sum _自动产生的类里的 apply 方法,简单地把这三个缺失的参数前转 到 sum,并返回结果。

这种一个下划线代表全部参数列表的表达式的另一种用途,就是把它当作转换 def 为函数值的方式。例如,如果你有一个本地函数,如 sum(a: Int, b: Int, c: Int): Int,你可以把它“包装”在 apply 方法具有同样的参数列表和结果类型的函数值中。当你把这个函数值应用到某些参数上时,它依次把 sum 应用到同样的参数,并返回结果。尽管不能把方法或嵌套函数赋值给变量, 或当作参数传递给其它方法,但是如果你把方法或嵌套函数通过在名称后面加一个下划线的方式 包装在函数值中,就可以做到了。(理解为把函数彻彻底底地作为一个参数)

如果你正在写一个省略所有参数的偏应用程序表达式,如 println _或 sum _,而且在代码的那 个地方正需要一个函数,你可以去掉下划线从而表达得更简明。

    ints.foreach(println)

 

闭包

函数文本也可以参考定义在其它地方的变量:

(x: Int) => x + more // more 是多少?

函数把“more”加入参考,但什么是 more 呢?从这个函数的视点来看,more 是个自由变量:free variable,因为函数文本自身没有给出其含义。相对的,x 变量是一个绑定变量:bound variable, 因为它在函数的上下文中有明确意义:被定义为函数的唯一参数,一个 Int。如果你尝试独立使 用这个函数文本,范围内没有任何 more 的定义,编译器会报错。

另一方面,只要有一个叫做 more 的什么东西同样的函数文本将工作正常:

 def main(args: Array[String]): Unit = {
    val more = 1;
    val increase = (x:Int) => x+more
    println(increase(1))
  }

结果为2;

依照这个函数文本在运行时创建的函数值(对象)被称为闭包:closure(在这个例子中就是increase)。名称 源自于通过“捕获” 自由变量的绑定对函数文本执行的“关闭”行动。不带自由变量的函数文本,如(x: Int) => x + 1,被称为封闭术语:closed term,这里术语:term 指的是一小部分源代码。因此依照这个函数 文本在运行时创建的函数值严格意义上来讲就不是闭包,因为(x: Int) => x + 1 在编写的时候 就已经封闭了。

这个例子带来一个问题:如果 more 在闭包创建之后被改变了会发生什么事?Scala 里,答案是闭包看到了这个变化。

def main(args: Array[String]): Unit = {
    var more = 1;
    val increase = (x:Int) => x+more
    println(increase(1))
    more = 100;
    println(increase(1))
  }

 

第二次的结果为101;

 

重复参数

Scala 允许你指明函数的最后一个参数可以是重复的。这可以允许客户向函数传入可变长度参数 列表。想要标注一个重复参数,在参数的类型之后放一个星号

  def main(args: Array[String]): Unit = {
    def echo(args:String*) =
      for(s <- args) println(s)

    echo("hello","scala")
  }

打印结果:

hello

scala

函数内部,重复参数的类型是声明参数类型的数组。因此,echo 函数里被声明为类型“String*” 的 args 的类型实际上是 Array[String]。然而,如果你有一个合适类型的数组,并尝试把它当作 重复参数传入,你会得到一个编译器错误.要实现这个做法,你需要在数组参数后添加一个冒号和一个_*符号。

    val array = Array("hello","scala")
    echo(array:_*)

这个标注告诉编译器把 arr 的每个元素当作参数,而不是当作单一的参数传给 echo。

 

尾递归

def approximate(guess: Double): Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))

像 approximate 这样,在它们最后一个动作调用自己的 函数,被称为尾递归:tail recursive。Scala 编译器检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的跳转,从而避免了堆栈的变换

Scala 里尾递归的使用局限很大,因为 JVM 指令集使实现更加先进的尾递归形式变得很困难。 Scala 仅优化了直接递归调用使其返回同一个函数。如果递归是间接的,就像在下面的例子里两 个互相递归的函数,就没有优化的可能性了:

def isEven(x: Int): Boolean =
if (x == 0) true else isOdd(x - 1)
def isOdd(x: Int): Boolean =
if (x == 0) false else isEven(x - 1)

同样如果最后一个调用是一个函数值你也不获得尾调用优化:

val funValue = nestedFun _
def nestedFun(x: Int) {
if (x != 0) { println(x); funValue(x - 1) }
}

尾调用优化受限于方法或嵌套函数在最后一个操作调用本身,而没有转到某个函数值或什么其它的中间函数的情况。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!