Scala练习二控制结构和函数

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

控制结构和函数

摘要

本篇主要学习在Scala中使用条件表达式循环函数,你会看到Scala和其他编程语言之间一个根本性的差异。在Java或C++中,我们把表达式(比如3+4)和语句(比如if语句)看做两样不同的东西。表达式有值,而语句执行动作。在Scala中,几乎所有构造出来的语法结构都有值。这个特性使得程序更加精简,也更易读。本篇的要点包括:

1. 表达式有值

2. 块也有值,是它最后一个表达式的值

3. Scala的for循环就像是"增强版"的Java for循环

4. 分号(在绝大多数情况下)不是必需的

5. void类型是Unit

6. 避免在函数定义中使用return

7. 注意别在函数式定义中漏掉了=

8. 异常的工作方式和Java或C++中基本一样,不同的是你在catch语句中使用"模式匹配"

9. Scala没有受检异常

条件表达式

表达式的值

Scala的if/else语法结构,和Java或C++-样。不过,在Scala中if/else表达式有值,这个值就是跟在if或else之后表达式的值。例如:

if (x > 0) 1 else-1

上述表达式的值是1或-1,具体是哪一个取决于x的值。你可以将if/else表达式的值赋值给变量

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

这与如下语句的效果一样:

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

不过,第一种写法更好,因为它可以用来初始化一个val。而在第二种写法当中,S必须是var

Java和C++有一个 ? : 操作符用于同样目的。如下表达式

x > 0 ? 1: -1 // Java或c++

等同于Scala表达式if(x>0) 1 else -1。不过,你不能在 ? : 表达式中插入语句。Scala的if/else将在Java和C++中分开的两个语法结构if/else? :结合在了一起

表达式的类型

在Scala中,每个表达式都有一个类型。举例来说,表达式

if(x > 0) 1 else-1

上述表达式的类型是lnt,因为两个分支的类型都是Int。混合类型表达式,比如:

if (x > 0) "positive" else -1

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

if (x > 0) 1

那么有可能该语句没有输出值。但是在Scala中,每个表达式都应该有某种值。这个问题的解决方案是引入一个Unit类,写做()。不带else的这个if语句等同于:

if (x > 0) 1 else ()

你可以把()当做是表示"无有用值"的占位符,将Unit当做Java或C++中的void。从技术上讲,void没有值但是Unit有一个表示"无值"的值。如果你一定要深究的话,这就好比空的钱包和里面有一张写着"没钱"的无面值钞票的钱包之间的区别

块表达式和赋值

块表达式

在java或C++中,块语句是一个包含于{}中的语句序列。每当你需要在逻辑分支或循环中放置多个动作时,你都可以使用块语句。在 Scala中,{}块包含一系列表达式,其结果也是一个表达式。块中最后一个表达式的值就是块的值

这个特性对于那种对某个val的初始化需要分多步完成的情况很有用。例如:

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

{}块的值取其最后一个表达式,在此处以红色字体标出。变量dx和dy仅作为计算所需要的中间值,很干净地对程序其他部分而言不可见了。

赋值

在Scala中,赋值动作本身是没有值的或者更严格地说,它们的值是Unit类型的。你应该还记得,Unit类型等同于Java和C++中的void,而这个类型只有一个值,写做()。一个以赋值语句结束的块,比如

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

上述块表达式的值是Unit类型。这没有问题,只是当我们定义函数时需要意识到这一点。由于赋值语句的值是Unit类型的,别把它们串接在一起:

x=y=1 //别这样做

y=1的值是(),你几乎不太可能想把一个Unit类型的值赋值给x。而在Java和C++中,赋值语句的值是被赋的那个值。在这些语言中,将赋值语句串接在一起是有意义的。

输入输出

输出

如果要打印一个值,我们用print或println函数。后者在打印完内容后会追加一个换行符。举例来说

print ("Answer : ")

println (42)

与下面的代码输出的内容相同:

println("Answer: "+42)

另外,还有一个带有C风格格式化字符串的printf函数:

printf("Hello, %s! You are%d years old.\n", "Fred",42)

输入

你可以用readLine函数从控制台读取一行输入。如果要读取数字Boolean或者是字符,可以用readlnt、readDouble、 readByte、readShort、readLong、readFloat、readBoolean或者readChar。与其他方法不同,readLine带一个参数作为提示字符串:

val name=readLine ("Your name: ")

print("Your age:")

val age=readlnt()

printf("Hello, %s! Next year, your will be %d.\n", name, age+1)

循环

While循环

Scala拥有与Java和C++相同的while和do循环。例如:

while (n>0) {

r=r*n

n-=1

}

for循环

Scala没有与for ( 初始化变量;检查变量是否满足某条件;更新变量 ) 循环直接对应的结构。如果你需要这样的循环,有两个选择:一是使用while循环,二是使用如下for句:

for (i <- 1 to n)

r=r*i

通过Richlnt类的这个to方法,1 to n这个调用返回数字1到数字n(含)的Range(区间)。下面的这个语法结构

for (i <- 表达式)

让变量i遍历<- 右边的表达式的所有值。至于这个遍历具体如何执行,则取决于表达式的类型。对于Scala集合比如Range而言,这个循环会让i依次取得区间中的每个值。

遍历字符串数组时,你通常需要使用从0到n-1的区间。这个时候你可以用util方法而不是to方法。util方法返回一个并不包含上限的区间

val s="Hello"

var sum=0

for (i <- 0 util s.length)//i的最后一个取值是s.length -1

sum+=s(i)

在本例中,事实上我们并不需要使用下标。你可以直接遍历对应的字符序列:

var sum=0

for (ch <- "Hello" )

sum+=ch

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

高级for循环和for推导式

在Scala中,for循环比起Java和C++的功能要丰富得多,下面将介绍其高级特性

生成器

你可以以变量 <- 表达式的形式提供多个生成器,用分号将它们隔开。例如:

for(i <- 1 to 3;j <- 1 to 3){

print(10*i+j+"\t") //将打印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+"\t") //将打印12 13 21 23 31 32

}

注意在if之前并没有分号。

引入定义

除此之外,你可以使用任意多的定义,引入可以在循环中使用的变量:

for(i <- 1 to 3;form=4-i;j <- form to 3 ){

print(10*i+j+"\t") //将打印13 22 23 31 32 33

}

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

for(i <- 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)

函数

普通函数

Scala除了方法外还支持函数。方法对对象进行操作,函数不是。C++也有函数,不过在Java中我们只能用静态方法来模拟。要定义函数,你需要给出函数的名称参数函数体,就像这样:

def abs(x:Double) = if (x>0) x else -x

必须给出所有参数的类型。不过,只要函数不是递归的,你就不需要指定返回类型。Scala编译器可以通过=符号右侧的表达式的类型推断出返回类型。如果函数体需要多个表达式完成,可以用代码块。块中最后一个表达式的值就是函数的返回值。举例来说,下面的这个函数返回位于for循环之后的r的值。

def fac(n:Int) = {

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

}

在本例中我们并不需要用到return。我们也可以像Java或C++那样使用retum,来立即从某个函数中退出,不过在Scala中这种做法并不常见。

递归函数

对于递归函数,我们必须指定返回类型。例如:

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

如果没有返回类型,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","<<<",">>>")

如果相对参数的数量,你给出的值不够,默认参数会从后往前逐个应用进来。举例来说:

decorate("Hello",">>>[")

则会使用right自带的默认参数,得到:"<<<[Hello] "。

指定参数名

你也可以在提供参数值的时候指定参数名。例如:

decorate (left="<<<", str ="Hello", right=">>>")

结果是"<Hello>>>",由上可知带名参数并不需要跟参数列表的顺序完全一致。带名参数可以让函数更加可读。它们对于那些有很多默认参数的函数来说也很有用。

混用参数名

当然,也可以混用未命名参数带名参数,只耍那些未命名的参数排在前面的即可:

decorate ("Hello",right="]<<<")

上面代码,将调用decorate ("Hello","[" ,"]<<<")

变长参数

有时候,实现一个可以接受可变长度参数列表的函数会更方便。以下示例显示了它的语法:

def sum (args : Int*)={

var result=0

for (arg <- args)

result +=arg

result

}

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

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

函数得到的是一个类型为Seq参数,可以使用for循环来访问每一个元素。

如果你已经有一个值的序列,则不能直接将它传入上述函数。举例来说,如下的写法是不对的:

val s =sum(1 to 5)

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

理。追加: _*,就像这样:

val s=sum(l to 5._*) //将1 to 5当做参数序列处理

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

def recursiveSum (args: Int*): Int:{

if (args.length==0)

0

else

args.head+recursiveSum( args.tail : _* )

}

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

过程

Scala对于不返回值的函数有特殊的表示法。如果函数体包含在花括号当中但没有前面的=号,那么返回类型就是Unit。这样的函数被称做过程(procedure),过程不返回值,我们调用它仅仅是为了它的副作用。举例来说,如下过程把一个字符串打印在一个框中,就像这样:

---------

|Hello|

---------

由于过程不返回任何值,所以我们可以略去;号。

def box(s:String) { //注意前面没有=

var border="-" * s.length+"--\n"

println(border+"|"+s+"|\n"+border)

}

有人不喜欢用这种简明的写法来定义过程,并建议大家总是显式声明Unit返回类型:

def box (s: String): Unit=(

}

懒值

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

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

这个调用将从一个文件读取所有字符并拼接成一个字符串, 如果程序从不访问words,那么文件也不会被打开。为了验证这个行为,我们可以在REPL中试验,但故意拼错文件名。在初始化语句被执行的时候并不会报错。不过,一旦你访问words,就将会得到一个错误提示:文件未找到。

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

val words =scala.io.Source.fromFile ("/usr/share/dict/words") .mkString // 在words被定义时即被取值

lazy val words=scala.jo.Source.fromFile("/usr/share/dict/words").mkString // 在words被首次使用时取值

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

需要注意的是:懒值并不是没有额外开销。我们每次访问懒值,都会有一个方法被调用,而这个方法将会以线程安全的方式检查该值是否已被初始化。

异常

Scala异常机制

Scala异常的工作机制和Java或C++ 样,当你抛出异常时,比如:

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

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

和Java样,抛出的对象必须是java.lang.Throwable的子类。不过,与Java不同的是,Scala没有"受检"异常,即你不需要声明说函数或方法可能会抛出某种异常。在Java中, "受检"异常编译期被检查。如果你的方法可能会抛出IOException,你必须做出声明。这就要求程序员必须去想那些异常应该在哪里被处理掉,这是个值得称道的目标。不幸的是,它同时也催生出怪兽般的方法签名,比如void doSometing() throws IOException,InterruptedException,ClassNotFoundException。许多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异常捕捉

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

try{

process (new URL( "http: //horstmann.com/fred-tiny. gif"))

}catch{

case _: MalformedURLException=>println("Bad URL: "+url)

case ex: IOException=>ex.printStacKTrace()

}

和Java或C++样,更通用的异常应该排在更具体的异常之后。而且,如果你不需要使用捕获的异常对象,可以使用_来替代变量名。

try/finally释放资源

try/finally语句让你可以释放资源,不论有没有异常发生。例如:

var in=new URL("http://horstmann.com/fred.gif") .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 { …}