您的位置:首页 > 其它

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

2016-09-14 18:24 597 查看

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.scalaSwitchDemo.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?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  读书笔记 结构 scala