快学Scala 2
控制结构和函数
1.在Scala中,几乎所有构造出来的语法结构都有值。这个特性是为了使得程序更加精简,也更易读。
(1)if表达式有值
(2)块也有值——是它最后一个表达式的值
(3)Scala的for循环就像是“增强版”的Java for循环
(4)分号(在绝大多数情况下)不是必须的
(5)void类型是Unit
(6)避免在函数定义中使用return
(7)注意别在函数式定义中漏掉了=
(8)异常的工作方式和Java中基本一样,不同的是catch语句中使用“模式匹配”
(9)Scala没有受检异常
2.条件表达式
在Scala中if/else表达式有值,这个值就是跟在if或else之后的表达式的值。例如:
if(x > 0) 1 else -1
可以将if/else表达式的值赋值给变量:
val s = if(x > 0) 1 else -1
这与如下语句的效果一样:
if(x > 0) s = 1 else s = -1
不过第一种写法给号,因为它可以用来初始化一个val;而第二种写法中,s必须是var。
Scala中每个表达式都有一个类型。例如,表达式 if(x > 0) 1 else -1的类型是Int,因为两个分支的类型都是Int。混合类型表达式,比如if(x > 0) "positive" else -1,上述表达式的类型是另个分支类型的公共超类型。java.lang.String和Int的公共超类型叫做Any。
如果else部分缺失了,比如:if(x > 0) 1,那么有可能if语句没有输出值。但是在Scala中,每个表达式都应该有某种值。解决方案是引入一个Unit类,写作()。不带else的if语句等同于:if(x > 0) 1 else ()
把()当做表示“无有用值”的占位符,将Unit当做Java中的void。
Scala没有switch语句,不过它有一个强大得多的模式匹配机制。现阶段,用一系列的if语句就好。
3.REPL比起编译器来更加“近视”——它在同一时间只能看到一行代码。可用花括号:
if(x > 0) {1
} else if(x == 0) 0 else -1
只有在REPL中才会有这个顾虑。在REPL中粘贴成块代码,可用使用粘贴模式。键入: :paste 把代码块粘贴进去,然后按下Ctrl+D。这样REPL就会把代码块当做一个整体来分析。
4.语句终止
在Scala中——与JavaScript和其他脚本语言类似——行尾的位置不需要分号。不过,如果你想要在单行中写下多个语句,就需要将它们以分号隔开。例如:
if(n > 0) { r = r * n; n-=1}
如果写较长的语句,需要分两行来写,就要确保第一行以一个不能用来做语句结尾的符号结尾。通常来说一个比较好的选择是操作符:
s = s0 + (v- v0) * t + //+告诉解析器这里不是语句的末尾
0.5 * (a - a0) * t * t
如果倾向于使用分号,用就是了——他们没啥坏处。
5.块表达式和赋值
在Java中,语句块是一个包含于{}中的语句序列。在Scala中,{}快包含一系列表达式,其结果也是一个表达式。块中最后一个表达式的值就是块的值。
这个特性对于val的初始化需要很多步完成的情况很有用。例如:
val distance = { val dx = x - x0; val dy = y - y0;sqrt(dx * dx, dy * dy) }
在Scala中,赋值动作本身是没有值的——或者,更严格地说,它们的值是Unit类型的。
一个以赋值语句结束的块,比如{r = r * n; n-=1 }的值是Unit类型的。当我们定义函数时需要意识到这一点。
6.输入和输出
如果要打印一个值,用print或者println函数。另外,还有一个带有C风格格式化字符串的printf函数:
printf("Hello", %s! You are %d old. \n", "Fred", 42) // Hello, Fred! You are 42 old.
用readLine函数从控制台读取一行输入。也可以用readInt、readDouble、readByte、readShort、readLong、readFloat、readBoolean、readChar。不过,readLine带一个参数作为提示字符串:
val name = readLine(”Your name:”)
print(“Your age: ”)
val age = readInt()
printf("Hello, %s! Next Year, you will be %d .\n, name, age + 1)
7.循环
Scala拥有与Java和C++相同的while和do循环。例如:
while(n > 0) {
r = r * n;
n -=1;
}
Scala没有与for(初始化变量;检查变量是否满足条件;更新变量)循环直接对应的结构。如果需要这样的循环,有两种选择:一是使用while循环,二是使用如下for语句:
for(i <- 表达式) 让变量i遍历 <- 右边的表达式的所有值。至于这个遍历具体如何执行,则取决于表达式的类型。对应Range而言,这个循环会让i一次取得区间中的每个值。
例如:
for(i <- 1 to n)
r = r * i
遍历数组和字符串时,需要0到n-1的区间,可以使用until方法而不是to方法。util方法返回一个并不包含上限的区间。
val s = "Hello"
var sum = 0
for(i <- 0 util s.length) //[0,s.length-1]
Scala中并没有提供break或continue语句来退出循环。如果需要break语句:
(1)使用Boolean型的控制变量
(2)使用嵌套函数——从函数当中return
(3)使用Breaks对象中的break方法:
import scala.util.control.Breaks._
breakable {
for(…) {
if(...) break;
...
}
}
这里,控制权的转移是通过抛出和捕获异常完成的,因此,如果时间很重要的话,尽量避免使用这套机制。
8. 高级for循环和for推导式
(1)可以以 变量 <- 表达式 的形式提供多个生成器,用分号将它们隔开。
for(i <- 1 to 3; j <- 1 to 3) print ((10 * i + j) + " ") //打印11 12 13 21 22 23 31 32 33
每个生成器都可以带一个守卫,以 if 开头的Boolean表达式:
for(i <- 1 to 3; j <- 1 to 3 if i != j) print((10 * i + j) + " ") //打印12 13 21 23 31 32
注意if之前并没有分号。
可以使用任意多的定义,引入可以在循环中使用的变量:
for(i <- 1 to 3; from = 4 - i; j <- from to 3) print((10 * i + j) + " ") //打印 13 22 23 31 32 33
如果for循环的循环体以yield开始,则该循环会构造一个集合,每次迭代生成集合中的一个值:
for(1 <- 1 to 10) yield i % 3
//生成 Vector(1, 2, 0, 1, 2, 0, 1, 2, 0, 1)
这类循环叫for推导式。
for推到式生成的集合与它的第一个生成器是类型兼容的。
for(c <- "Hello"; i <- 0 to 1) yield (c + i).toChar //将生成"HIeflmlmop"
for(i <- 0 to 1; c <- "Hello") yield (c + i).toChar //将生成Vector('H', 'e', 'l', 'l', 'o', 'I', 'f', 'm', 'm', 'p')
也可以将生成器、守卫和定义包含在花括号中,并可以一换行的方式而不是分号来分隔它们:
for{ i <- 1 to 3
from = 4 - i
j <- from to 3 }
9. 函数
Scala除了方法还支持函数。方法针对对象进行操作,函数不是。不过在Java中我们只能使用静态方法来模拟。
定义函数,给出函数的名称,参数和函数体,例如:
def abs(x : Double) = if (x >= 0) x else -x
必须给出所有参数的类型。只要函数不是递归的,就不用指定返回值类型。Scala编译器可以通过=符号右侧的表达式推断出返回值类型。
如果函数体需要多个表达式完成,可以用代码块。块中最后一个表达式的值就是函数的返回值。例如:
def fac(n : Int) = {
var r = 1
for(i <- 1 to n) r = r * i
r //返回r的值
}
对于递归函数,必须制定返回值类型。例如:
def fac(n : Int) : Int = if(n<=0) 1 else n * fac(n - 1)
如果没有返回类型,Scala编译器无法校验n * fac(n - 1)的类型是Int。
10.默认参数和带名参数
在调用某些函数时并不显式地给出所有的参数值,对于这些函数可以使用默认参数。例如,
def decorate(str : String, left: String = "[", right : String = "]") =
left + str + right
这个函数的两个参数left和right带有默认值 "[" 和 "]"。调用 decorate("hello")得到"[hello]"。
如果想对参数的数量,你给出的值不够,默认参数会从后往前逐个应用进来。例如,decorate("hello",">>[")会使用right参数的默认值。
可以在提供参数值的时候指定参数名,例如:decorate(left=“《《”,str = “hello”,right=“》》”),结果是"《《hello》》"。注意,带名参数并不需要跟参数列表的顺序完全一致。带名参数使得函数更加可读。
可以混用未命名参数和带名参数,只要那些未命名的参数是排在前面的即可。
decorate("hello", right = "}>>" //将调用decorate("hello", "[", "}>>")
11.变长参数
实现一个可以接受可变长度参数列表的函数会更方便。
def sum(args: Int*) = {
var result = 0
for(arg <- args) result += arg
result
}
可以使用任意多的参数来调用该函数: val s = sum(1, 4, 9 ,16, 25)
如果已有一个值的序列,则不能将它传入上述函数。例如:val s = sum(1 to 5) //错误
如果sum函数被调用传入的是单个参数,那么该参数必须是单个整数,而不是一个整数区间。解决方案:告诉编译器你希望这个参数被当做参数序列处理。追加 :_*
val s = sum(1 to 5 : _*) //将1 to 5 当做参数序列处理