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后依旧可读性高
- 查看更多
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
- 查看更多
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语句
如果有一个map的数据结构,并不需要一个switch语句:
val monthNumberToName = Map( 1 -> "January", 2 -> "February", 3 -> "March", 4 -> "April", 5 -> "May", 6 -> "June", 7 -> "July", 8 -> "August", 9 -> "September", 10 -> "October", 11 -> "November", 12 -> "December" ) val monthName = monthNumberToName(4) println(monthName) // prints "April"
查看更多
The @switch annotation documentation
Compiling Switches:讨论tableswitch和lookupswitch
Difference between JVM’s LookupSwitch and TableSwitch?