《快学Scala》 第2章 控制结构和函数

时间:2022-09-08 17:56:23

一、条件表达式

Scala 的 if/else 语法结构和 Java 或 C++一样。不过在 Scala 中的 if/else表达式有值,这个值就是跟在 if 或 else 之后的表达式的值,且该表达式的类型为 Int ,例如

val s = if (x > 0) 1 else -1

Scala 的 if/else 将在 Java 和 C++ 中分开的两个语法结构 if/else 和 ?: 结合一起了

在 Scala 中,每个表达式都有一个类型,在下面的混合类型表达式中

if (x > 0) "postitive" else 1

上述表达式的类型是两个分支类型的公共超类型,其中一个分支是 java.lang.String,而另外一个分支是 Int。它们的公共超类型叫做 Any。若表达式中的 else 部分缺失了,比如

if (x > 0) 1    <=>   if (x > 0) 1 else ()

那它缺失分支的类型是?解决方案是引入一个 Unit 类,写作 ()。等价的表达式如上所示,这里可以把 () 当做 “无有用值” 的占位符,将 Unit 当做 Java 或 C++ 中的 void 。从技术上讲, void 没有值但是 Unit 有一个表示“无值”的值,好比 “空的钱包”和“里面有一张写着‘没钱’的无面值钞票的钱包”的区别

说明Scala 没有 switch 语句,不过它有一个强大得多的模式匹配机制

注意 REPL 比起编译器来更加“近视”——它在同一时间只能看到一行代码,例如

if (x > 0) 1
else if (x == 0) 0 else -1

REPL 会执行 if (x > 0) 1 ,然后显示结果。之后它看到接下来的 else 关键字就会不知所措。如果想在 else 前换行的话,用花括号 {}

if (x > 0) { 1
}
else if (x == 0) 0 else -1

只有在 REPL 中才会有这个顾虑的,在被编译的程序中,解释器会找到下一行的 else

提示如果想在 REPL 中粘贴成块的代码,而不像担心 REPL 的近视问题,可以使用粘贴模式。键入

:paste

把代码块粘贴进入,然后按下 Ctrl+D 。遮掩 REPL 就会把代码块当作一个整体来分析


二、语句终止

在 Scala 中,行尾的位置不需要分号,只要能够从上下文明确地判断出这里是语句的终止即可。当书写较长代码的语句时,需要分两行写,就要确保第一行以一个不能用做语句结尾的符号结尾。通常来说一个比较好的选择是操作符:

s = s0 + ( v - v0) * t +    // + 告诉解析器这里不是语句的末尾
0.5 * (a - a0) * t * t

当然 Scala 程序员更倾向于 花括号 来清除的表示后面还有更多内容


三、块表达式和赋值

在 Scala 中, {} 块包含一系列表达式其结果也是一个表达式块中最后一个表达式的值就是块的值。这个特性对那种对某个 val 的初始化 需要分多步完成的情况很有用,例如

val distance = {val dx = x - x0; val dy = y - y0; sqrt(dx*dx + dy*dy)}

{} 块的值取其最后一个表达式,即 sqrt(dx*dx + dy*dy) 。变量 dxdy 仅作为计算所需要的中间值,很干净地对程序其他部分而言不可见了

在 Scala 中,赋值本身是没有值的—或者,更严格地说,它们的值是 Unit 类型的,该类型只有一个 () 的值,比如下面的表达式就是 Unit 类型的

{r = r * n; n -= 1}

但因为赋值语句是 Unit 类型的,别把它们串接在一起

x = y = 1 // 错误的写法

y = 1 的值是 (), 不能把一个 Unit 类型的值赋值给 x


四、输入和输出

  • 可以使用 printprintln 函数

    println("Hello World " + 886 + true)
  • 可以使用带 C 风格格式化字符串的 printf 函数

    printf("Hello, %s! You are %d years old \n", "world", 25)
  • 可以用 readLine 函数从控制台读取一行输入,可以用 readLinereadBytereadShortreadLongreadFloatreadBooleanreadChar 。与其他方法不同,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)

《快学Scala》 第2章 控制结构和函数


五、循环

1. Scala 拥有与 Java 和 C++ 相同的 while 和 do 循环

while (n > 0){
r = r * n
n -= 1
}

2. Scala 没有与 for(初始化变量;检查变量是否满足某变量;更新变量),只有 for (i <- 表达式)

for ( i <- 1 to n)
r = r * i

1 to n 这个调用返回数字 1 到数字 n (含)的 Range (区间),让变量 i 遍历 <- 右边表达式的所有值,至于这个遍历具体如何执行,则取决于表达式的类型。比如 Range ,这个循环会让 i 依次取得区间中的每个值。当需要从 0 到 n-1 的区间时,可以使用 until ,until 方法返回一个兵不包含上限的区间

val s = "Hello"
var sum = 0
for (i <- 0 until s.length)
sum += s(i)

或者可以直接遍历对应的字符序列

var sum = 0
for(ch <- "Hello") sum += ch

说明1:在 Scala 中,对循环的使用并不如其他语言那么频繁,通常可以通过对序列中的所有值应用某个函数的方式来处理它们,从而完成这项工作只需要一次方法调用即可

说明2: Scala 并没有提供 breakcontinue 语句,当需要 break 时,可以采用以下三种方式

  • 使用 Boolean 型的控制变量
  • 使用嵌套函数——可以从函数当中 return
  • 使用 Breaks 对象中的 break 方法

    import scala.util.control.Breaks._
    breakable {
    for (...){
    if (...) break; // 退出 `breakable ` 块
    ...
    }
    }

    在这里,控制权的转移是通过抛出和捕获异常完成的,因此如果时间很重要的话,你应该避免使用这套机制(言下之意就是这个很浪费时间?)


六、高级 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

2. 每个生成器都可以带一个守卫 ,以 if 开头的 Boolean 表达式(在 if 之前并没有分号)

《快学Scala》 第2章 控制结构和函数

3. 可以使用任意多的定义,引入可以在循环中使用的变量

for (i <- 1 to 3; from = 4 - j; j = from to 3) print ((10 * i+ j) + " ")    // 打印出 13 22 3 31 32 33

4. 如果 for 循环的循环体以 yield 开始,则该循环会构造出一个集合,每次迭代生成集合中的一个值

《快学Scala》 第2章 控制结构和函数

这类循环叫做for 推导式,for 推导式生成的集合与它的第一个生成器是类型兼容的

for (c <- "Hello"; i <- 0 to 1) yield (c + i).toChar

for (i <- 0 to 1; c <- "Hello" ) yield (c + i).toChar

《快学Scala》 第2章 控制结构和函数

说明:可以将生成器、守卫和定义包含在花括号中并可以以换行的方式而不是分号来隔开它们

for {i <- 1 to 3
from = 4 - j
j = from to 3}

七、函数

Scala 除了方法外还支持函数,方法对对象进行操作,函数不是。 C++ 也有函数,不过在 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
}

上面的这个函数返回位于 for 循环之后的 r 的值
提示:在 Scala 中并不常见 return ,用了貌似也无妨。我们会大量使用 匿名函数 ,这些函数中 return 并不返回值给调用者,它跳出到包含它的带名函数中。可以把 return 当做是函数版的 break 语句,仅在需要时使用

def fac(n: Int): Int = if (n <= 0) 1 else n * fac(n - 1)

:Int 就是我们要强调的返回类型,例如

如果没有返回函数类型,Scala 编译器无法校验 n * fac(n - 1) 的类型是 Int

说明:某些编程语言(如 ML 和 Haskell)能够推断出递归函数的类型,用的是 Hindley-Milner 算法。不过,在面向对象的语言中这样做并不总是行得通。如何扩展 Hindley-Milner 算法让它能够处理子类型仍然是个科研命题


八、默认参数和带名参数

  • 我们在调用某些函数时并不显式地给出所有参数值,对于这些函数我们可以使用默认参数,例如:
def decorate(str: String, left: String = "[", right: String = "]") = left + str + right
  • 这个函数有两个参数,left 和 right ,带有默认值 “[”和 “]” 。如果你调用 decorate(“Hello”),你会得到 “[Hello]”。如果不喜欢默认的值,可以给出自己版本:
decorate("Hello","<<<",">>>") => "<<<Hello>>>"
  • 如果相对参数的数量,给出的值不够,你给出的值不够,默认参数会从后往前逐个应用进来
decorate("Hello", ">>>[") =>  >>>[Hello]
  • 可以在提供参数值的时候指定参数名,例如
decorate(left="<<<", str="Hello", right=">>>") => <<<Hello>>>

带名参数并不需要与参数列表的顺序完全一致,带名参数可以让函数更加可读

  • 可以混用未命名参数和带名参数,只要那些未命名的参数是排在前面的即可:
decorate("Hello", right= "]<<<" ) => [Hello]<<<

九、变长参数

实现一个可以接受可变长度参数列表的函数会更方便

def sum(args: Int*)={
var result = 0
for (arg <- args) result += arg
result

可以使用任意多的参数来调用该函数

val s = sum(1,4,9,16,25)

函数得到的是一个类型为 Seq 的参数。如果已经有一个值的序列,则不能直接将它传入到上述函数

val s = sum (1 to 5)    // 错误

如果 sum 函数被调用时传入的是单个参数,那么该参数必须是单个整数,而不是一个整数区间。解决这个办法的是告诉编译器你希望这个参数被当做参数序列处理。追加 _*

val s = sum (1 to 5:_*)

《快学Scala》 第2章 控制结构和函数

在递归定义当中,我们会用到上述语法:

def recursiveSum(args: Int*):Int = {
if(args.length == 0) 0
else args.head + recursiveSum(args.tail:_*)
}

序列的 head 是它的首个元素,而 tail 是所有其他元素的序列,这又是一个 Seq,我们用 :_* 来将它转换成参数序列

注意:当调用变长参数且参数类型为 Object 的 Java 方法,如 PrintStream.printf 或 MessageFormat.format 时,这时就需要手工对基本类型进行转换

val str = MessageFormat.format("The answer to {0} is {1}", "everything", 42.asInstanceOf[AnyRef])

对于任何 Object 类型的参数都是这样,因为类似的参数在变长参数方法中使用得最多


十、过程

Scala 对于不返回值的函数有特殊的表示法,如果函数体包含在花括号当中,但没有前面的 = 号 ,那么返回的类型就是 Unit 。这样的函数就称为 过程 。过程不返回值,调用它仅仅是为了它的副作用。由于过程不返回任何值,所以我们可以省略 =

def box(s: String) { // 仔细看:没有 = 号
var border = "-" * s.length + "--\n"
println(border + "|" + s + "|\n" + border)

也可以显示声明 Unit 返回类型:

def box(s: String): Unit = {
...
}

注意:简明版本的过程定义语法对于 Java 和 C++ 程序员来说可能带来意想不到的后果,比如不小心在函数定义中略去了 = 号,于是在函数被调用的地方得到一个错误提示, Unit 在那里不被接受


十一、懒值

当 val 被声明为 lazy 时,它的初始化将被推迟,直到我们首次对它取值

lazy val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString

该函数这个调用将从一个文件读取所有字符并拼接成一个字符串,如果程序不访问 words ,那么文件也不会被打开

懒值对于开销较大的初始化语句而言十分有用,它们可以应对其他初始化问题,比如循环依赖,更重要的是,它们是开发懒数据结构的基础。可以把懒值当做是介于 val 和 def 的中间状态,对比如下定义

val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString  // 在 words 被定义时即被取值
lazy val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString // 在 words 被首次使用时取值
def words = scala.io.Source.fromFile("/usr/share/dict/words").mkString // 在每一次 words 被使用时取值

十二、异常

Scala 异常的工作机制和 Java 或 C++ 一样,当抛出异常时

throw new IllegalArgumentException("x should not be negative")

当前的运算被中止,运行时系统查找可以接受 IllegalArgumentException 的异常处理器。控制权将在离抛出点最近的处理器中恢复。如果没有找到符合要求的异常处理器,则程序退出

和 Java 一样,抛出的对象必须是 java.lang.Throwable 的子类。不过,与 Java 不同的是,Scala 没有“受检”异常——不需要声明说函数或方法可能会抛出某种异常

说明:在 Java 中,“受检”异常在编译期被检查,如果程序的方法可能会抛出 IOException,你必须做出声明。这就要求程序员必须去想那些异常应该在哪里被处理掉,这是个值得称道的目标。许多 Java 程序员很反感这个特性,最终过早捕获这些异常,或者使用超通用的异常类。Scala 的设计者们决定不支持“受检”异常,因为他们意识到彻底的编译期检查并不总是好的

throw 表达式有特殊的类型 Nothing。这在 if/else 表达式中很有用,如果一个分支的类型是 Nothing,那么 if/else 表达式的类型就是另一个分支的类型

if (x > 0) { sqrt(x)} else throw new IllegalArgumentException("x should not be negative")

第一个分支类型是 Double,第二个分支类型是 Nothing。因此 if/else 表达式的类型是 Double

《快学Scala》 第2章 控制结构和函数

捕获异常的语法采用的是模式匹配的语法

try {
process(new URL("htpp://www.ibigdta.wang"))
} catch {
case _:MalformedURLException => println("Bad URL: " + url)
case ex: IOException => ex.printStackTrace()
}

和 Java 或 C++ 一样,更通用的异常应该排在更具体的异常之后

注意:如果不需要使用捕获的异常对象,可以使用 _ 来代替变量名,且 try/finally 语句让你可以释放资源,无论有没有异常发生

var in = new URL("htpp://www.ibigdta.wang").openStream()
try {
process(in)
} finally {
in.close()
}

finally 语句无论 process 函数是否抛出异常都会执行,reader 总会被关闭。这段代码有些微妙,也提出三个问题

  • 如果 URL 构造器或 openStream 方法抛出异常怎么办?
    这样一来 try 代码块和 finally 语句都不会被执行。但这没什么不好—— in 从未被初始化,因此调用 close方法没有意义

  • 为什么 val in = new URL(…).openStream() 不放在 try 代码块里?
    因为这样做的话, in 的作用域不会延展到 finally 语句中

  • 如果 in.close() 抛出异常怎么办?
    这样一来异常跳出当前语句,废弃并替代掉所有先前抛出的异常。(这跟 Java 一模一样,并不是很完美。理想情况是老的异常应该与新的异常一起保留)

注意:try/catch 和 try/finally 的目的是互补的。 try/catch 语句处理异常,而 try/finally 语句在异常没有被处理时执行某种动作(通常是清理工作),我们可以把它们结合在一起成为单个 try/catch/finally 语句:

try {...} catch{...} finally {...}

这和下面的语句一样:

try {try {...} catch{...}} finally {...}

不过这样的组合在一起的写法并没有什么卵用….


本章知识总结

  • if 表达式有值
  • 块也有值 —— 是它最后一个表达式的值
  • Scala 的 for 循环就像是“增强版”的 Java for 循环
  • 分号(在绝大多数情况下)不是必需的
  • void 类型是 Unit
  • 避免在函数定义中使用 return
  • 注意别在函数式定义中漏掉 =
  • 异常的工作方式和 Java 或 C++ 中基本一样,不同的是在 catch 语句中使用 “模式匹配”
  • Scala 没有受检异常

《快学Scala》 第二章 控制结构和函数 课后练习请参阅:http://blog.csdn.net/u011414200/article/details/48471985