首页 > 代码库 > 控制抽象

控制抽象

所有的函数都可以被分成通用部分,以及非通用部分,这将导致代码存在大量的冗余。

代码1-1

object FileMatcher {
  private def filesHere = new java.io.File(".").listFiles()
  def fileEnding(query: String) =
    for (file <- filesHere; if file.getName.endsWith(query)) yield file
  def fileContains(query: String) =
    for (file <- filesHere; if file.getName.contains(query)) yield file
}

 代码1-1中,fileEnding和fileContains两个方法,一个是查询以query结尾的,一个是查询包含query的,虽然功能不同,但代码大部分却相同,随着功能的增加,冗余的代码将会越来越多,维护也会越来越困难。为了解决这一问题,可以使用高阶函数,可以将函数当做对象传入另外一个函数,如代码1-2。

object FileMatcher {
  private def filesHere = new java.io.File(".").listFiles()
  def filesMatching(query: String, matcher: (String, String) => Boolean) =
    for (file <- filesHere; if matcher(file.getName, query)) yield file
  def filedEnding(query: String) = filesMatching(query, _.endsWith(_))
  def filedContaining(query: String) = filesMatching(query, _.contains(_))
}

 filesMatching方法除了接收一个String对象的参数,还接收一个函数值对象,这个函数值对象接收两个String对象,经过一系列计算,最后得出的结果为Boolean类型,将filedEnding和filedContaining两个方法分别将_.endsWith(_)_.contains(_)作为对象传入filesMatching方法中,下划线是占位符,如果有不懂的同学可以去看上一篇函数与闭包,红色下划线代表第一个参数,蓝色下划线代表第二个参数,经过改造,可以发现代码的冗余少了很多。

实际上,我们的代码还可以更精简一点,如代码1-3。

代码1-3

object FileMatcher {
  private def filesHere = new java.io.File(".").listFiles()
  def filesMatching(query: String, matcher: (String) => Boolean) =
    for (file <- filesHere; if matcher(file.getName)) yield file
  def filedEnding(query: String) = filesMatching(query, _.endsWith(query))
  def filedContaining(query: String) = filesMatching(query, _.contains(query))
}

 柯里化

  柯里化的函数被应用于多个参数列表,而不是仅仅一个,代码1-4展示的是未被柯里化的函数,它实现了对两个Int类型参数的加法

代码1-4

scala> def add(x: Int, y: Int) = x + y
add: (x: Int, y: Int)Int

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

 相对的,代码1-5展示了柯里化后的同一个函数

代码1-5

scala> def add(x: Int)(y: Int) = x + y
add: (x: Int)(y: Int)Int

scala> add(1)(2)
res1: Int = 3

 当add方法被调用时,实际上接连调用两个传统函数。第一个函数调用带单个的名为x的Int参数,并返回第二个函数的函数值。第二个函数带Int参数y,如代码1-6

代码1-6

scala> def first(x: Int) = (y: Int) => x + y
first: (x: Int)Int => Int

scala> val second = first(1)
second: Int => Int = <function1>

scala> second(2)
res2: Int = 3

 first和second函数只是柯里化过程的一个演示,它们并不直接连在add函数上,尽管如此,仍然有一个方式获得实际指向add第二个函数的参考,可以用部分应用表达式方式,把占位符标注用在add里,如代码1-7

代码1-7

scala> def add(x: Int)(y: Int) = x + y
add: (x: Int)(y: Int)Int

scala> add(1)(2)
res1: Int = 3

scala> val onePlus = add(1)_
onePlus: Int => Int = <function1>

scala> onePlus(2)
res3: Int = 3

scala> val twoPlus = add(2)_
twoPlus: Int => Int = <function1>

scala> twoPlus(3)
res4: Int = 5

 add(1)_里的下划线是第二个参数列表的占位符,结果指向一个函数的参考,这个函数被调用的时候,对它唯一的Int参数加1并返回结果

 

双倍控制结构,能够重复一个操作两次,见代码1-8

代码1-8

scala> def twice(op: Double => Double, x: Double) = op(op(x))
twice: (op: Double => Double, x: Double)Double

scala> twice(_ + 1, 6)
res6: Double = 8.0

 

IO流的借贷模式,见代码1-9

代码1-9

def withPrintWriter(file: File, op: PrintWriter => Unit) = {
      val writer = new PrintWriter(file)
      try {
        op(writer)
      } finally {
        writer.close()
      }
    }
    withPrintWriter(new File("date.txt"), writer => writer.println(new Date))

 使用这个方法的好处是,由withPrintWriter而并非用户代码,确认文件在结尾被关闭。因此忘记关闭文件是不可能的,之所以被称为借贷模式,因此控制抽象函数,如withPrintWriter,打开了资源并“贷出”给函数

传名参数

  传值参数在函数调用之前表达式会被求值,例如Int、Long等数值参数类型,而传名参数在函数调用前表达式不会求值,只会当做一个匿名参数传递下去,如代码1-10和代码1-11

代码1-10

scala> def strToIntByValue(s: String) = {
    println("call strToIntByValue")
    s.toInt
  }
strToIntByValue: (s: String)Int

scala> strToIntByValue({ println("hello world"); "10" })
hello world
call strToIntByValue
res0: Int = 10

 代码1-10中,先打印了hello world再打印call strToIntByValue,方法strToIntByValue时已经对传入参数的表达式求值

 代码1-11

scala> def strToIntByName(s: => String) = {
    println("call strToIntByName")
    s.toInt
  }
strToIntByName: (s: => String)Int

scala> strToIntByName({ println("hello world"); "10" })
call strToIntByName
hello world
res1: Int = 10

 代码1-11,先打印了strToIntByName再打印hello world,在调用strToIntByName时并未对传入参数的表达式求值,一直到需要用到传入参数表达式的时候

 

控制抽象