Scala Cookbook读书笔记 Chapter 3.Control Structures 第一部分

3.0 总体介绍

  • Scala中的if/then/else结构和Java中很像,但是它还可以用来返回一个值。比如Java中用的三目运算,在Scala中只需要使用正常的if语句即可。

    val x = if (a) y else z
    
  • try/catch/finilly结构和Java中很像,但是Scala中的catch部分使用的是模式匹配。
  • Scala中可以使用2个for循环读取文件的每行,然后在每行上操作每个字符:

    for (line <- source.getLines) {
        for {
            char <- line
            if char.isLetter
        } // char algorithm here ...
    }
    
  • 在Scala中,还可以更加简单点:

    for {
        line <- source.getLines
        char <- line
        if char.isLetter
    } // char algorithm here ...
    
  • Scala很容易对一个集合进行操作并产生一个新的集合:

    scala> val nieces = List("emily", "hannah", "mercedes", "porsche")
    nieces: List[String] = List(emily, hannah, mercedes, porsche)
    
    scala> for (n <- nieces) yield n.capitalize
    res0: List[String] = List(Emily, Hannah, Mercedes, Porsche)
    
  • 类似的,基本用法中Scala的匹配表达式和Java的Switch语句很像,但是匹配模式可以匹配任何对象,从匹配的对象中提取信息,添加case分支,返回结果而且更多。匹配表达式是Scala语言的一大特色。

3.1 使用for和foreach循环

  • 问题:想要遍历集合中的元素,或者操作集合中的每个元素,或者根据已有集合创建一个新集合。

    3.1.1 解决方案

  • 有很多循环Scala集合的方法,包括for循环,while循环,foreach、map、flatMap等集合方法。该方案主要针对for循环和foreach方法。

    val a = Array("apple", "banana", "orange")
    
    //for循环
    scala> for (e <- a) println(e)
    apple
    banana
    orange
    
  • 多行数据处理,使用for循环,并且类代码块里执行

    scala> for (e <- a) {
        | // imagine this requires multiple lines
        | val s = e.toUpperCase
        | println(s)
        | }
    APPLE
    BANANA
    ORANGE
    

    3.1.2 for循环返回值

  • 使用for/yield组合根据输入集合创建一个新的集合:

    scala> val newArray = for (e <- a) yield e.toUpperCase
    newArray: Array[java.lang.String] = Array(APPLE, BANANA, ORANGE)
    
  • 注意输入类型是Array,输出也是Array,而不是其他比如Vector
  • 在yield关键词后使用代码块处理多行代码:

    scala> val newArray = for (e <- a) yield {
        | // imagine this requires multiple lines
        | val s = e.toUpperCase
        | s
        | }
    newArray: Array[java.lang.String] = Array(APPLE, BANANA, ORANGE)
    

    3.1.3 for循环计数器

  • 使用计数器获取数组元素

    for (i <- 0 until a.length) {
        println(s"$i is ${a(i)}")
    }
    
    //输出
    0 is apple
    1 is banana
    2 is orange
    
  • 使用zipWithIndex方法创建一个循环计数器

    scala> for ((e, count) <- a.zipWithIndex) {
        | println(s"$count is $e")
        | }
    0 is apple
    1 is banana
    2 is orange
    
  • 更多zipWithIndex看10.11章

    3.1.4 生成

  • 使用Range执行3次for循环

    scala> for (i <- 1 to 3) println(i)
    1 
    2 
    3
    
  • 使用1 to 3创建一个Range

    scala> 1 to 3
    res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)
    
  • 章节3.3演示如何使用guards,这里简单预览下:

    scala> for (i <- 1 to 10 if i < 4) println(i)
    1 
    2 
    3
    

    3.1.5 Map的循环

  • 当循环Map里的键和值时,下面循环方法简明可读:

    val names = Map("fname" -> "Robert",
                    "lname" -> "Goren")
    for ((k,v) <- names) println(s"key: $k, value: $v")
    
  • 更多查看11.17章

3.1.6 讨论

  • for/yield组合是创建并返回一个新集合。但是没有yield的for循环只是操作集合中的每个元素,并没有创建一个新集合。for/yield组合基本使用就像map方法一样。更多看3.4章
  • for循环并不一定是最好的解决问题的方法。foreach,map,flatMap,collect,reduce等等方法经常被用来解决问题,而不要求使用for循环。
  • 可以通过foreach循环每个元素

    scala> a.foreach(println)
    apple
    banana
    orange
    
  • 需要在集合每个元素上进行数据处理,使用匿名函数语法:

    scala> a.foreach(e => println(e.toUpperCase))
    APPLE
    BANANA
    ORANGE
    
  • 需要多行数据处理,使用代码块

    scala> a.foreach { e =>
        | val s = e.toUpperCase
        | println(s)
        | }
    APPLE
    BANANA
    ORANGE
    

3.1.7 for循环如何被翻译

  • Scala Language Specification 提供了在不同环境for循环被翻译的精确细节。可以总结为以下四点:

    • 简单for循环被翻译成集合上的foreach方法调用
    • 使用guard(3.3章)的for循环被翻译成集合上使用withFilter方法的序列,后面跟着foreach调用
    • for/yield组合表达式被翻译成集合上的map方法调用
    • for/yield/guard组合被翻译成集合上使用withFilter方法,后面跟着map调用
  • 使用下面的scalac命令行提供Scala编译器翻译for循环为其他代码的初始化输出:

    $ scalac -Xprint:parse Main.scala
    
  • 在一个命名为Main.scala的文件中写以下代码

    class Main {
        for (i <- 1 to 10) println(i)
    }
    
  • 使用scalac命令行后输出如下

    $ scalac -Xprint:parse Main.scala
    
    [[syntax trees at end of parser]] // Main.scala
    package <empty> {
        class Main extends scala.AnyRef {
            def <init>() = {
                super.<init>();
                ()
            };
            1.to(10).foreach(((i) => println(i)))
        }
    }
    
  • 如果使用 -Xprint:all 选项代替 -Xprint:parse 编译文件,会发现代码进一步被被翻译成下面的代码,包括太多编译时的细节:

    $ scalac -Xprint:all Main.scala
    
    scala.this.Predef.intWrapper(1).to(10).foreach[Unit]
    (((i: Int) => scala.this.Predef.println(i)))
    
  • 上面使用的是Range,但是在其他集合编译器表现一样。下例使用list代替Range:

    // original List code
    val nums = List(1,2,3)
    for (i <- nums) println(i)
    
    // translation performed by the compiler
    val nums = List(1,2,3)
    nums.foreach(((i) => println(i)))
    
  • 下面演示各种for循环如何被翻译:

    • 第一个代码

      // #1 - input (my code)
      for (i <- 1 to 10) println(i)
      
      // #1 - compiler output
      1.to(10).foreach(((i) => println(i)))
      
    • 添加guard(if语句)

      // #2 - input code
      for {
          i <- 1 to 10
          if i % 2 == 0
      } println(i)
      
      // #2 - translated output
      1.to(10).withFilter(((i) => i.$percent(2).$eq$eq(0))).foreach(((i) =>
          println(i)))
      
    • 两个guard被翻译成两个withFilter调用

      // #3 - input code
      for {
          i <- 1 to 10
          if i != 1
          if i % 2 == 0
      } println(i)
      
      // #3 - translated output
      1.to(10).withFilter(((i) => i.$bang$eq(1)))
              .withFilter(((i)
          => i.$percent(2).$eq$eq(0))).foreach(((i) => println(i)))
      
    • for/yield组合

      // #4 - input code
      for { i <- 1 to 10 } yield i
      
      // #4 - output
      1.to(10).map(((i) => i))
      
    • for/yield/guard组合

      // #5 - input code (for loop, guard, and yield)
      for {
          i <- 1 to 10
          if i % 2 == 0
      } yield i
      
      // #5 - translated code
      1.to(10).withFilter(((i) => i.$percent(2).$eq$eq(0))).map(((i) => i))    
      

3.2 使用多个计数器的for循环

  • 问题:当你循环多维数组时,需要创建一个带有多个计数器的循环。

    3.2.1 解决方案

  • 如下方法创建:

    scala> for (i <- 1 to 2; j <- 1 to 2) println(s"i = $i, j = $j")
    i = 1, j = 1
    i = 1, j = 2
    i = 2, j = 1
    i = 2, j = 2
    
  • 对于多行for循环首选的格式是花括号:

    for {
        i <- 1 to 2
        j <- 1 to 2
    } println(s"i = $i, j = $j")
    
  • 三个计数器:

    for {
        i <- 1 to 3
        j <- 1 to 5
        k <- 1 to 10
    } println(s"i = $i, j = $j, k = $k")
    
  • 当循环多维数组时非常有用,假设创建一个二维数组:

    val array = Array.ofDim[Int](2,2)
    array(0)(0) = 0
    array(0)(1) = 1
    array(1)(0) = 2
    array(1)(1) = 3
    
  • 通过以下方式打印数组的每个元素:

    scala> for {
        | i <- 0 to 1
        | j <- 0 to 1
        | } println(s"($i)($j) = ${array(i)(j)}")
    (0)(0) = 0
    (0)(1) = 1
    (1)(0) = 2
    (1)(1) = 3
    

    3.2.2 讨论

  • 在for循环里使用符号<-创建的Range指的是生成器,可以在一个循环里使用多个生成器。
  • 较长for循环里推荐格式是使用大括号:
    for {
        i <- 1 to 2
        j <- 2 to 3
    } println(s"i = $i, j = $j")
    
  • 这种格式比其他格式可扩展性高,这里指的是在表达式里添加更多生成器和guards后依旧可读性高
  • 查看更多

    Scala Style Guide page on formatting control


3.3 使用加入if语句的for循环

  • 问题:想要在for循环添加一个或者更多条件语句用来过滤掉集合中的某些元素。

    3.3.1 解决方案

  • 在生成器后添加if语句

    // print all even numbers
    scala> for (i <- 1 to 10 if i % 2 == 0) println(i)
    2 
    4 
    6 
    8
    10
    
  • 或者使用大括号格式:

    for {
        i <- 1 to 10
        if i % 2 == 0
    } println(i)
    
  • 使用多个if语句

    for {
        i <- 1 to 10
        if i > 3
        if i < 6
        if i % 2 == 0
    } println(i)
    

    3.3.2 讨论

  • 在for循环里使用if语句简明可读性高,当然也可以使用传统方式

    for (file <- files) {
        if (hasSoundFileExtension(file) && !soundFileIsLong(file)) {
            soundFiles += file
        }
    }
    
  • 一旦习惯Scala的for循环语句,下面的方式更加可读,因为它从业务逻辑中把循环和过滤分开:

    for {
        file <- files
        if passesFilter1(file)
        if passesFilter2(file)
    } doSomething(file)
    

3.4 创建一个for/yield组合

  • 问题:在已有集合上每个元素进行运算处理创建一个新的集合

    3.4.1 解决方案

  • 使用yield

    scala> val names = Array("chris", "ed", "maurice")
    names: Array[String] = Array(chris, ed, maurice)
    
    scala> val capNames = for (e <- names) yield e.capitalize
    capNames: Array[String] = Array(Chris, Ed, Maurice)
    
  • 多行运算处理,在yield关键词之后使用代码块执行工作

    scala> val lengths = for (e <- names) yield {
        | // imagine that this required multiple lines of code
        | e.length
        | }
    lengths: Array[Int] = Array(5, 2, 7)
    
  • 除了特殊情况,for循环集合返回类型和开始类型是一样的。举例如果循环一个ArrayBuffer,那么返回也是ArrayBuffer

    //原集合
    var fruits = scala.collection.mutable.ArrayBuffer[String]()
    fruits += "apple"
    fruits += "banana"
    fruits += "orange"
    
    //新集合
    scala> val out = for (e <- fruits) yield e.toUpperCase
    out: scala.collection.mutable.ArrayBuffer[java.lang.String] =
        ArrayBuffer(APPLE, BANANA, ORANGE)
    
  • 输入时List集合,那么for/yield循环之后返回也是List集合

    scala> val fruits = "apple" :: "banana" :: "orange" :: Nil
    fruits: List[java.lang.String] = List(apple, banana, orange)
    
    scala> val out = for (e <- fruits) yield e.toUpperCase
    out: List[java.lang.String] = List(APPLE, BANANA, ORANGE)
    

3.4.2 讨论

  • for/yield工作原理
    • 开始运行时,for/yield循环立刻创建一个和输入集合一样的新的空的集合。比如输入类型是Vector,那么输出就是Vector,可以认为新集合是一个桶。
    • 每次循环,从输入集合的当前元素创建一个新的输出元素,当输出元素创建,就被放置在桶中。
    • 循环结束时,返回这个桶的所有内容。
  • 没有guard的for/yield组合和调用map方法一样:

    scala> val out = for (e <- fruits) yield e.toUpperCase
    out: List[String] = List(APPLE, BANANA, ORANGE)
    
    scala> val out = fruits.map(_.toUpperCase)
    out: List[String] = List(APPLE, BANANA, ORANGE)
    

3.5 实现break和continue

  • 问题:有一个需要使用break和continue结构的情况,但是Scala没有break和continue关键词

    3.5.1 解决方案

  • Scala没有break和continue关键词,但在scala.util.control.Breaks提供了相同的功能
package com.alvinalexander.breakandcontinue

import util.control.Breaks._

object BreakAndContinueDemo extends App {

    println("\n=== BREAK EXAMPLE ===")
    breakable {
        for (i <- 1 to 10) {
            println(i)
            if (i > 4) break // break out of the for loop
        }
    }

    println("\n=== CONTINUE EXAMPLE ===")
    val searchMe = "peter piper picked a peck of pickled peppers"
    var numPs = 0
    for (i <- 0 until searchMe.length) {
        breakable {
            if (searchMe.charAt(i) != 'p') {
                break // break out of the 'breakable', continue the outside loop
            } else {
                numPs += 1
            }
        }
    }
    println("Found " + numPs + " p's in the string.")
}

//输出
=== BREAK EXAMPLE ===
1 
2 
3 
4 
5

=== CONTINUE EXAMPLE ===
Found 9 p's in the string.

3.5.2 break例子

  • 当i大于4时,运行break“关键词”,此时会抛出一个异常,并且for循环退出。breakable“关键词”捕获这个异常然后运行breakable语句块之后的代码

    breakable {
        for (i <- 1 to 10) {
            println(i)
            if (i > 4) break // break out of the for loop
        }
    }
    
  • 注意break和breakable并不是真的关键词,他们是scala.util.control.Breaks里的方法。在Scala2.10,break方法被调用时会抛出一个BreakControl异常实例

    private val breakException = new BreakControl
    def break(): Nothing = { throw breakException }
    
  • breakable方法定义成捕获一个BreakControl异常

    def breakable(op: => Unit) {
        try {
            op
        } catch {
            case ex: BreakControl =>
                if (ex ne breakException) throw ex
        }
    }
    
  • 更多看3.18章,如何与Breaks库类似的方式实现自己的控制结构

    3.5.3 continue例子

  • 代码循环字符串中的每个字符,如果当前字符不是字母p,那么跳出if/then语句,然后继续执行for循环。
  • 执行到break方法后,抛出一个异常,然后被breakable捕获。这个异常跳出if/then语句,然后捕获以允许for循环继续执行后面的元素。

    val searchMe = "peter piper picked a peck of pickled peppers"
    var numPs = 0
    
    for (i <- 0 until searchMe.length) {
        breakable {
            if (searchMe.charAt(i) != 'p') {
                break // break out of the 'breakable', continue the outside loop
            } else {
                numPs += 1
            }
        }
    }
    
    println("Found " + numPs + " p's in the string.")
    

3.5.4 一般语法

  • 实现break和continue功能的一般的语法如下,部分以伪代码方式写,然后与java进行比较。

    //break Scala
    breakable {
        for (x <- xs) {
            if (cond)
                break
        }
    }
    
    //break Java
    for (X x : xs) {
        if (cond) break;
    }
    
    //continue Scala
    for (x <- xs) {
        breakable {
            if (cond)
                break
        }
    }
    
    //continue Java
    for (X x : xs) {
        if (cond) continue;
    }
    

3.5.5 关于continue例子

  • 在Scala中有更好的方法解决continue例子的问题,比如直接的方法是以简单匿名函数使用count方法,此时count依旧是9:

    val count = searchMe.count(_ == 'p')
    

3.5.6 嵌套循环和标记break

  • 在任何情况下,可以创建标记break:

    object LabeledBreakDemo extends App {
    
        import scala.util.control._
    
        val Inner = new Breaks
        val Outer = new Breaks
    
        Outer.breakable {
            for (i <- 1 to 5) {
                Inner.breakable {
                    for (j <- 'a' to 'e') {
                        if (i == 1 && j == 'c') Inner.break else println(s"i: $i, j: $j")
                        if (i == 2 && j == 'b') Outer.break
                    }
                }
            }
        }
    }
    
  • 这个例子中,第一个if条件满足,会抛出一个异常然后被Inner.breakable捕获,外面的for循环继续。不过如果第二个if条件触发,控制流发送到Outer.breakable,然后两个循环都退出。运行结果如下:

    i: 1, j: a
    i: 1, j: b
    i: 2, j: a
    
  • 如果偏爱标记break使用相同的方法,下面演示只用一个break方法时的标记break使用

    import scala.util.control._
    
    val Exit = new Breaks
    Exit.breakable {
        for (j <- 'a' to 'e') {
            if (j == 'c') Exit.break else println(s"j: $j")
        }
    }
    

3.5.7 讨论

  • 如果不喜欢使用break和continue,还有其他方法可以解决这个问题。
  • 举例,需要添加猴子到桶中,直到桶装满了。可以利用一个简单的布尔测试来退出for循环

    var barrelIsFull = false
    for (monkey <- monkeyCollection if !barrelIsFull) {
        addMonkeyToBarrel(monkey)
        barrelIsFull = checkIfBarrelIsFull
    }
    
  • 另一个方法是在函数里进行计算,当达到所需条件时从函数里返回,下面的例子如果sum比limit大,sumToMax函数提前返回

    // calculate a sum of numbers, but limit it to a 'max' value
    def sumToMax(arr: Array[Int], limit: Int): Int = {
        var sum = 0
        for (i <- arr) {
            sum += i
            if (sum > limit) return limit
        }
        sum
    }
    val a = Array.range(0,10)
    println(sumToMax(a, 10))
    
  • 在函数式编程里通用方法是使用递归算法,下面演示阶乘函数:

    def factorial(n: Int): Int = {
        if (n == 1) 1
        else n * factorial(n - 1)
    }
    
  • 需要注意这个例子没有使用尾递归,所以不是最优方法,尤其起始值n特别大时。下面演示利用尾递归的更优方法。

    import scala.annotation.tailrec
    
    def factorial(n: Int): Int = {
        @tailrec def factorialAcc(acc: Int, n: Int): Int = {
            if (n <= 1) acc
            else factorialAcc(n * acc, n - 1)
        }
        factorialAcc(1, n)
    }
    
  • 当确认算法是尾递归这种情况时可以使用@tailrec注解。如果你使用了这个注解但是你的算法不是尾递归,编译器会报错,比如在第一个factorial方法使用注解,会得到如下错误:

    Could not optimize @tailrec annotated method factorial: it contains a recursive call not in tail position
    
  • 查看更多

    Branching Statements
    Scala factorial on large numbers sometimes crashes and sometimes doesn’t


3.6 像三目运算符一样使用if结构

  • 问题: 使用Scala的if表达式如三目运算符一样更加简洁有效的解决问题

3.6.1 解决方案

  • 不像Java中,在Scala中有点小问题,因为Scala里没有特殊的三目运算符,只能使用if/else表达式:

    val absValue = if (a < 0) -a else a
    
    println(if (i == 0) "a" else "b")
    
    hash = hash * prime + (if (name == null) 0 else name.hashCode)
    

3.6.2 讨论

  • 更多例子如下,返回一个结果并且Scala语法使得代码更加简洁

    def abs(x: Int) = if (x >= 0) x else -x
    
    def max(a: Int, b: Int) = if (a > b) a else b
    
    val c = if (a > b) a else b
    
  • 查看更多

    Equality, Relational, and Conditional Operators


3.7 像switch语句一样使用match表达式

  • 问题:需要创建一个基于整数的Java Switch语句,比如匹配一周中的每一天,一年中的每一月,等等其他整数map一个结果的情况。

    3.7.1 解决方案

  • 使用Scala的match表达式,像Java Switch语句一样:

    // i is an integer
    i match {
        case 1 => println("January")
        case 2 => println("February")
        case 3 => println("March")
        case 4 => println("April")
        case 5 => println("May")
        case 6 => println("June")
        case 7 => println("July")
        case 8 => println("August")
        case 9 => println("September")
        case 10 => println("October")
        case 11 => println("November")
        case 12 => println("December")
        // catch the default with a variable so you can print it
        case whoa => println("Unexpected case: " + whoa.toString)
    }
    
  • 从match表达式返回值的函数方法:

    val month = i match {
        case 1 => "January"
        case 2 => "February"
        case 3 => "March"
        case 4 => "April"
        case 5 => "May"
        case 6 => "June"
        case 7 => "July"
        case 8 => "August"
        case 9 => "September"
        case 10 => "October"
        case 11 => "November"
        case 12 => "December"
        case _ => "Invalid month" // the default, catch-all
    }
    

    3.7.2 @switch注解

  • 如果使用简单match表达式,推荐使用@switch注解。在编译时如果switch不能编译成tableswitch或者lookupswitch时会提供一个警告。
  • 编译成tableswitch或者lookupswitch有更好的性能,因为它的结果是一个分支表而不是决策树。当给表达式一个给定值时,可以直接跳到结果处而不是执行完决策树。
  • 官方文档解释:如果在一个match表达式使用一个注解,编译器会确认这个match是否已经编译成tableswitch或者lookupswitch,如果编译成一系列条件表达式则会发出一个错误。

    // Version 1 - compiles to a tableswitch
    import scala.annotation.switch
    
    class SwitchDemo {
    
        val i = 1
        val x = (i: @switch) match {
            case 1 => "One"
            case 2 => "Two"
            case _ => "Other"
        }
    
    }
    
  • 编译代码:

    $ scalac SwitchDemo.scala
    
  • 反汇编代码

    $ javap -c SwitchDemo
    
    //输出
    16: tableswitch{ //1 to 2
    1: 50;
    2: 45;
    default: 40 }
    
  • 输出是一个tableswitch,表明Scala可以优化match表达式成tableswitch
  • 然后进行小改动,使用变量代替整数2:

    import scala.annotation.switch
    
    // Version 2 - leads to a compiler warning
    class SwitchDemo {
    
        val i = 1
        val Two = 2 // added
        val x = (i: @switch) match {
            case 1 => "One"
            case Two => "Two" // replaced the '2'
            case _ => "Other"
        }
    }
    
    //编译
    $ scalac SwitchDemo.scala
    SwitchDemo.scala:7: warning: could not emit switch for @switch annotated match
        val x = (i: @switch) match {
                     ^
    one warning found
    
  • 这个警告是说这个匹配表达式既不能生成tableswitch也不能生成lookupswitch.
  • 在Scala In Depth指出必须满足下面几点才可以应用tableswitch优化
    • 匹配的值必须是已知整数
    • 匹配表达式必须简单,不能包含任何的类型检查,if语句,或者提取器
    • 编译时表达式必须有值
    • 有超过2个的case语句

3.7.3 讨论

  • 不止可以匹配整数:

    def getClassAsString(x: Any): String = x match {
        case s: String => s + " is a String"
        case i: Int => "Int"
        case f: Float => "Float"
        case l: List[_] => "List"
        case p: Person => "Person"
        case _ => "Unknown"
    }
    

    3.7.4 处理默认的case

  • 如果不关注默认match的值,可以使用_

    case _ => println("Got a default match")
    
  • 相反,如果对默认match的值感兴趣,分配一个变量,然后可以在表达式右边使用变量:

    case default => println(default)
    
  • 使用default名称往往是最有意义的并导致代码可读。但是也可以使用任何合法的变量名称:

    case oops => println(oops)
    
  • 如果不处理默认的case会生成一个MatchError,比如下面:

    i match {
        case 0 => println("0 received")
        case 1 => println("1 is good, too")
    }
    
    //如果i的值超过0或1时,会抛出以下异常
    scala.MatchError: 42 (of class java.lang.Integer)
        at .<init>(<console>:9)
        at .<clinit>(<console>)
            much more error output here ...
    

3.7.5 是否真的需要switch语句