内建控制结构

时间:2022-01-15 05:49:39

Scala里没有多少内建控制结构。仅有的包括if,while,for,try,match和函数调用。如此之少的理由是,从一开始Scala就包括了函数文本。代之以在基本语法之上一个接一个添加高层级控制结构,Scala把它们汇集在库里.

有件你会注意到的事情是,几乎所有的Scala的控制结构都会产生某个值。这是函数式语言所采用的方式,程序被看成是计算值的活动,因此程序的控件也应当这么做。你也可以把这种方式看做早已存在于指令式语言中的一种趋势(函数调用返回值,被调用函数更新被当作参数传入的输出变量也归于此类)的逻辑推演另外,指令式语言经常具有三元操作符(如C,C++和Java的?:操作符),表现得就像if,却产生值。Scala采用了这种三元操作符模型,但是把它称为if。换句话说,Scala的if可以产生值。于是Scala持续了这种趋势让for,try和match也产生值.程序员能够利用这些结果值简化他们的代码,就好象用函数的返回值那样。如果没有这种机制,程序员就必须创建临时变量来保存控制结构中计算的结果。去掉这些临时变量能让代码更简洁并避免许多你在一个分支里设置了变量却在另外一个分支里忘了设置的bug。

7.1 if表达式

Scala的if如同许多其它语言中的一样工作。它测试一个状态并据其是否为真,执行两个分支中的一个。下面是一个常见的例子,以指令式风格编写:

 1 var filename = "default.txt" 2 if (!args.isEmpty) 3 filename = args(0) 

这段代码声明了一个变量,filename,并初始化为缺省值。然后使用if表达式检查是否提供给程序了任何参数。如果是,就把变量改成定义在参数列表中的值。如果没有参数,就任由变量设定为缺省值。

这段代码可以写得更好一点,因为就像第2章第三步提到过的,Scala的if是能返回值的表达式。代码7.1展示了如何不使用任何var而实现前面一个例子同样的效果:

内建控制结构内建控制结构
1 val filename = if (!args.isEmpty) args(0) else "default.txt"
View Code

这一次,if有了两个分支。如果args不为空,那么初始化元素,args(0),被选中。否则,缺省值被选中。这个if表达式产生了被选中的值,然后filename变量被初始化为这个值。这段代码更短一点儿,不过它的实际优点在于使用了val而不是var。使用val是函数式的风格,并能以差不多与Java的final变量同样的方式帮到你。它让代码的读者确信这个变量将永不改变,节省了他们扫描变量字段的所有代码以检查它是否改变的工作。使用val而不是var的第二点好处是他能更好地支持等效推论:equational reasoning。在表达式没有副作用的前提下,引入的变量等效于计算它的表达式。因此,无论何时都可以用表达式替代变量名。如,要替代println(filename),你可以这么写:

内建控制结构内建控制结构
println(if (!args.isEmpty) args(0) else "default.txt")
View Code

尽可能寻找使用val的机会。它们能让你的代码既容易阅读又容易重构。

7.2 while循环

Scala的while循环表现的和在其它语言中一样。包括一个状态和循环体,只要状态为真,循环体就一遍遍被执行。代码7.2展示了一个例子:

Scala也有do-while循环。除了把状态测试从前面移到后面之外,与while循环没有区别。代码7.3展示了使用do-while反馈从标准输入读入的行记录直到读入空行为止的Scala脚本

内建控制结构内建控制结构
var line = "" do { line = readLine() println("Read: " + line) }
while (line != null)
View Code

while和do-while结构被称为“循环”,不是表达式,因为它们不产生有意义的结果,结果的类型是Unit。说明产生的值(并且实际上是唯一的值)的类型为Unit。被称为unit value,写做()。()的存在是Scala的Unit不同于Java的void的地方。请在解释器里尝试下列代码:

内建控制结构内建控制结构
1 scala> def greet() { println("hi") }
2 greet: ()Unit
3 scala> greet() == ()
4 hi
5 res0: Boolean = true
View Code

由于方法体之前没有等号,greet被定义为结果类型为Unit的过程。因此,greet返回unit值,()。这被下一行确证:比较greet的结果和unit值,(),的相等性,产生true。

另一个产生unit值的与此相关的架构,是对var的再赋值。比如,假设尝试用下面的从Java(或者C或C++)里的while循环成例在Scala里读取一行记录,你就遇到麻烦了:

内建控制结构内建控制结构
var line = "" while ((line = readLine()) != "") // 不起作用 println("Read: "+ line)
View Code

编译这段代码时,Scala会警告你使用!=比较类型为Unit和String的值将永远产生true。而在Java里,赋值语句可以返回被赋予的那个值,同样情况下标准输入返回的一条记录在Scala的赋值语句中永远产生unit值,()。因此,赋值语句“line = readLine()”的值将永远是()而不是""。结果,这个while循环的状态将永远不会是假,于是循环将因此永远不会结束。

由于while循环不产生值,它它经常被纯函数式语言所舍弃。这种语言只有表达式,没有循环。虽然如此,Scala仍然包含了while循环,因为有些时候指令式的解决方案更可读,尤其是对那些以指令式背景为主导的程序员来说。例如,如果你想做一段重复某进程直到某些状态改变的算法代码,while循环可以直接地表达而函数式的替代者,大概要用递归实现,或许对某些代码的读者来说就不是那么显而易见的了。

如,代码7.4展示了计算两个数的最大公约数的替代方式。1
代码7.4 使用递归计算最大公约数通常意义上,我们建议你如质疑var那样质疑你代码中的while循环。实际上,while循环和var经常是结对出现的。因为while循环不产生值,为了让你的程序有任何改变,while循环通常不是更新var就是执行I/O。可以在之前的gcdLoop例子里看到。在while循环工作的时候,更新了a和b两个var。因此,我们建议你在代码中对while循环抱有更怀疑的态度。如果没有对特定的while或do循环较好的决断,请尝试找到不用它们也能做同样事情的方式。 给定同样的值x和y,代码7.4展示的gcd函数将返回与代码7.2中gcdLoop函数同样的结果。这两种方式的不同在于gcdLoop写成了指令式风格,使用了var和while循环,而gcd更函数式风格,采用了递归(gcd调用自身)并且不需要var:

内建控制结构内建控制结构
def gcd(x: Long, y: Long): Long = if (y == 0) x else gcd(y, x % y)
View Code

7.3 for表达式

Scala的for表达式是为枚举准备的“瑞士军刀”。它可以让你用不同的方式把若干简单的成分组合来表达各种各样的枚举。简单的用法完成如把整数序列枚举一遍那样通常的任务。更高级的表

枚举集合类

你能用for做的最简单的事情就是把一个集合类的所有元素都枚举一遍。如,代码7.5展示了打印当前目录所有文件名的例子。I/O操作使用了Java的API。首先,我们创建指向当前目录,".",的文件,然后调用它的listFiles方法。方法返回File对象数组,每个都代表当前目录包含的目录或文件。我们把结果数组保存在filesHere变量。

内建控制结构内建控制结构
val filesHere = (new java.io.File(".")).listFiles for (file <- filesHere) println(file)
View Code

通过使用被称为发生器:generator的语法“file <- filesHere”,我们遍历了filesHere的元素。每一次枚举,名为file的新的val就被元素值初始化。编译器推断file的类型是File,因为filesHere是Array[File]。对于每一次枚举,for表达式的函数体,println(file),将被执行一次。由于File的toString方法产生文件或目录的名称,因此当前目录的所有文件和目录的名称都会被打印出来。for表达式语法对任何种类的集合类都有效,而不只是数组。282第页的表格5-4中看到的Range类型是其中一个方便的特例,你可以使用类似于“1 to 5”这样的语法创建一个Range,然后用for枚举。以下是一个简单的例子:

内建控制结构内建控制结构
scala> for (i <- 1 to 4) println("Iteration " + i) Iteration 1 Iteration 2 Iteration 3 Iteration 4
View Code

如果你不想包括被枚举的Range的上边界,可以用until替代to:

内建控制结构内建控制结构
scala> for (i <- 1 until 4) println("Iteration " + i) Iteration 1 Iteration 2 Iteration 3
View Code

像这样枚举整数在Scala里是很平常的,但在其他语言中就不是这么回事。其它语言中,你或许要采用如下方式遍历数组:

内建控制结构内建控制结构
// Scala中不常见…… for (i <- 0 to filesHere.length - 1) println(filesHere(i))
View Code

这个for表达式引入了变量i,依次把它设成从0到filesHere.length - 1的整数值,然后对i的每个设置执行一次for表达式的循环体。对应于每一个i的值,filesHere的第i个元素被取出并处理。

这种类型的枚举在Scala里不常见的原因是直接枚举集合类也做得同样好。这样做,你的代码变得更短并规避了许多枚举数组时频繁出现的超位溢出:off-by-one error。该从0开始还是从1开始?应该加-1,+1,还是什么都不用直到最后一个索引?这些问题很容易回答,但也很容易答错。还是避免碰到为佳。

过滤

有些时候你不想枚举一个集合类的全部元素。而是想过滤出一个子集。你可以通过把过滤器:filter:一个if子句加到for的括号里做到。如代码7.6的代码仅对当前目录中以“.scala”结尾的文件名做列表:

内建控制结构内建控制结构
val filesHere = (new java.io.File(".")).listFiles for (file <- filesHere if file.getName.endsWith(".scala")) println(file)
View Code

或者你也可以这么写:

内建控制结构内建控制结构
for (file <- filesHere) if (file.getName.endsWith(".scala")) println(file)
View Code

这段代码可以产生与前一段代码同样的输出,而且对于指令式背景的程序员来说看上去更熟悉一些。然而指令式格式只是一个可选项,因为这个for表达式的运用执行的目的是为了它的打印这个副作用并产生unit值()。正如在本节后面将展示的,for表达式之所以被称为“表达式”是因为它能产生令人感兴趣的值,一个其类型取决于for表达式<-子句的集合。

如果愿意的话,你可以包含更多的过滤器。只要不断加到子句里即可。例如,为了加强防卫,代码7.7中的代码仅仅打印文件而不是目录。通过增加过滤器检查file的isFile方法做到:

内建控制结构内建控制结构
for ( file <- filesHere if file.isFile; if file.getName.endsWith(".scala") ) println(file)
View Code

注意 如果在发生器中加入超过一个过滤器,if子句必须用分号分隔。这是代码7.7中的“if file.isFile”过滤器之后带着分号的原因。

嵌套枚举

如果加入多个<-子句,你就得到了嵌套的“循环”。比如,代码7.8展示的for表达式有两个嵌套循环。外层的循环枚举filesHere,内层的枚举所有以.scala结尾文件的fileLines(file)。

内建控制结构内建控制结构
def fileLines(file: java.io.File) = scala.io.Source.fromFile(file).getLines.toList def grep(pattern: String) = for { file <- filesHere if file.getName.endsWith(".scala") line <- fileLines(file) if line.trim.matches(pattern) } println(file + ": " + line.trim) grep(".*gcd.*")
View Code

如果愿意的话,你可以使用大括号代替小括号环绕发生器和过滤器。使用大括号的一个好处是你可以省略一些使用小括号必须加的分号。

mid-stream(流间)变量绑定

请注意前面的代码段中重复出现的表达式line.trim。这不是个可忽略的计算,因此你或许想每次只算一遍。通过用等号(=)把结果绑定到新变量可以做到这点。绑定的变量被当作val引入和使用,不过不用带关键字val。代码7.9展示了一个例子。

mid-stream(流间)变量绑定

请注意前面的代码段中重复出现的表达式line.trim。这不是个可忽略的计算,因此你或许想每次只算一遍。通过用等号(=)把结果绑定到新变量可以做到这点。绑定的变量被当作val引入和使用,不过不用带关键字val。代码7.9展示了一个例子。

内建控制结构内建控制结构
def grep(pattern: String) = for { file <- filesHere if file.getName.endsWith(".scala") line <- fileLines(file) trimmed = line.trim if trimmed.matches(pattern) } println(file + ": " + trimmed) grep(".*gcd.*")
View Code

代码中,名为trimmed的变量被从半当中引入for表达式,并被初始化为line.trim的结果值。之后的for表达式就可以在两个地方使用这个新变量,一次在if中,一次在println中。

制造新集合

到现在为止所有的例子都只是对枚举值进行操作然后就放过,除此之外,你还可以创建一个值去记住每一次的迭代。只要在for表达式之前加上关键字yield。比如,下面的函数鉴别出.scala文件并保存在数组里:

内建控制结构内建控制结构
def scalaFiles = for { file <- filesHere if file.getName.endsWith(".scala") } yield file
View Code

for表达式在每次执行的时候都会制造一个值,本例中是file。当for表达式完成的时候,结果将是一个包含了所有产生的值的集合。结果集合的类型基于枚举子句处理的集合类型。本例中结果为Array[File],因为filesHere是数组并且产生的表达式类型是File

另外,请注意放置yield关键字的地方。对于for-yield表达式的语法是这样的:

for {子句} yield {循环体}

yield在整个循环体之前。即使循环体是一个被大括号包围的代码块,也一定把yield放在左括号之前,而不是代码块的最后一个表达式之前。请抵挡住写成如下方式的诱惑:

内建控制结构内建控制结构
for (file <-filesHere if file.getName.endsWith(".scala")) { yield file // 语法错误! }
View Code

例如,代码7.10展示的for表达式首先把包含了所有当前目录的文件的名为filesHere的Array[File],转换成一个仅包含.scala文件的数组。对于每一个对象,产生一个Iterator[String](fileLines方法的结果,定义展示在代码7.8中),提供方法next和hasNext让你枚举集合的每个元素。这个原始的枚举器又被转换为另一个Iterator[String]仅包含含有子字串"for"的修剪过的行。最终,对每一行产生整数长度。这个for表达式的结果就是一个包含了这些长度的Array[Int]数组

内建控制结构内建控制结构
val forLineLengths = for { file <- filesHere if file.getName.endsWith(".scala") line <- fileLines(file) trimmed = line.trim if trimmed.matches(".*for.*") } yield trimmed.length
View Code

7.4 使用try表达式处理异常

Scala的异常和许多其它语言的一样。代之用普通方式那样返回一个值,方法可以通过抛出一个异常中止。方法的调用者要么可以捕获并处理这个异常,或者也可以简单地中止掉,并把异常升级到调用者的调用者。异常可以就这么升级,一层层释放调用堆栈,直到某个方法处理了它或没有剩下其它的方法。

抛出异常

异常的抛出看上去与Java的一模一样。首先创建一个异常对象然后用throw关键字抛出: throw new IllegalArgumentException

尽管可能感觉有些出乎意料,Scala里, throw也是有结果类型的表达式。下面举一个有关结果类型的例子:

内建控制结构内建控制结构
val half = if (n % 2 == 0) n / 2 else throw new RuntimeException("n must be even")
View Code

这里发生的事情是,如果n是偶数,half将被初始化为n的一半。如果n不是偶数,那么在half能被初始化为任何值之前异常将被抛出。因此,无论怎么说,把抛出的异常当作任何类型的值都是安全的。任何使用从throw返回值的尝试都不会起作用,因此这样做无害从技术角度上来说,抛出异常的类型是Nothing。尽管throw不实际得出任何值,你还是可以把它当作表达式。这种小技巧或许看上去很怪异,但像在上面这样的例子里却常常很有用。if的一个分支计算值,另一个抛出异常并得出Nothing。整个if表达式的类型就是那个实际计算值的分支的类型。Nothing类型将在以后的11.3节中讨论

捕获异常

用来捕获异常的语法展示在代码7.11中。选择catch子句这样的语法的原因是为了与Scala很重要的部分:模式匹配:pattern matching保持一致。模式匹配是一种很强大的特征,将在本章概述并在第十五章详述。

内建控制结构内建控制结构
import java.io.FileReader import java.io.FileNotFoundException import java.io.IOException try { val f = new FileReader("input.txt") // Use and close file } catch { case ex: FileNotFoundException => // Handle missing file case ex: IOException => // Handle other I/O error }
View Code

这个try-catch表达式的行为与其它语言中的异常处理一致。程序体被执行,如果抛出异常,每个catch子句依次被尝试。本例中,如果异常是FileNotFoundException,那么第一个子句将被执行。如果是IOException类型,第二个子句将被执行。如果都不是,那么try-catch将终结并把异常上升出去。你将很快发现与Java的一个差别是Scala里不需要你捕获检查异常:checked exception,或把它们声明在throws子句中。如果你愿意,可以用ATthrows标注声明一个throws子句,但这不是必需的

finally子句

如果想让某些代码无论方法如何中止都要执行的话,可以把表达式放在finally子句里。如,你或许想让打开的文件即使是方法抛出异常退出也要确保被关闭。代码7.12展示了这个例子。

import java.io.FileReader val file = openFile() try { // 使用文件 } finally { file.close() // 确保关闭文件 }

内建控制结构内建控制结构
import java.io.FileReader val file = openFile() try { // 使用文件 } finally { file.close() // 确保关闭文件 }
View Code

注意 代码7.12展示了确保非内存资源,如文件,套接字,或数据库链接被关闭的惯例方式。首先你获得了资源。然后你开始一个try代码块使用资源。最后,你在finally代码块中关闭资源。这种Scala里的惯例与在Java里的一样,然而,Scala里你还使用另一种被称为贷出模式:loan pattern的技巧更简洁地达到同样的目的。出借模式将在9.4节描述。

生成值

和其它大多数Scala控制结构一样,try-catch-finally也产生值。如,代码7.13展示了如何尝试拆分URL,但如果URL格式错误就使用缺省值。结果是,如果没有异常抛出,则对应于try子句;如果抛出异常并被捕获,则对应于相应的catch子句。如果异常被抛出但没被捕获,表达式就没有返回值。由finally子句计算得到的值,如果有的话,被抛弃。通常finally子句做一些清理类型的工作如关闭文件;他们不应该改变在主函数体或try的catch子句中计算的值。

内建控制结构内建控制结构
import java.net.URL import java.net.MalformedURLException def urlFor(path: String) = try { new URL(path) } catch { case e: MalformedURLException => new URL("http://www.scalalang.org") }
View Code

如果熟悉Java,不说你也知道,Scala的行为与Java的差别仅源于Java的try-finally不产生值。Java里,如果finally子句包含一个显式返回语句,或抛出一个异常,这个返回值或异常将“凌驾”于任何之前源于try代码块或某个它的catch子句产生的值或异常之上。如:

def f(): Int = try { return 1 } finally { return 2 } 调用f()产生结果值2。相反:
def g(): Int = try { 1 } finally { 2 }

调用g()产生1。这两个例子展示了有可能另大多数程序员感到惊奇的行为,因此通常最好还是避免从finally子句中返回值。最好是把finally子句当作确保某些副作用,如关闭打开的文件,发生的途径

Scala的匹配表达式允许你在许多可选项:alternative中做选择,就好象其它语言中的switch语句。通常说来match表达式可以让你使用任意的模式:pattern做选择,第十五章会介绍。通用的模式可以稍等再说。目前,只要考虑使用match在若干可选项中做选择。 作为例子,代码7.14里的脚本从参数列表读入食物名然后打印食物配料。match表达式检查参数列表的第一个参数firstArg。如果是字串"salt",就打印"pepper",如果是"chips",就打印"salsa",如此递推。缺省情况用下划线(_)说明,这是常用在Scala里作为占位符表示完全不清楚的值的通配符。

内建控制结构内建控制结构
val firstArg = if (args.length > 0) args(0) else "" firstArg match { case "salt" => println("pepper") case "chips" => println("salsa") case "eggs" => println("bacon") case _ => println("huh?") }
View Code

与Java的switch语句比,匹配表达式还有一些重要的差别。其中之一是任何种类的常量,或其他什么东西,都能用作Scala里的case,而不只是Java的case语句里面的整数类型和枚举常量。在这个例子里,可选项是字串。另一个区别是在每个可选项的最后并没有break。取而代之,break是隐含的,不会有从一个可选项转到另一个里面去的情况。这通常把代码变短了,并且避免了一些错误的根源,因为程序员不再因为疏忽在选项里转来转去。 然而,与Java的switch相比最显著的差别,或许是match表达式也能产生值。在前一个例子里,match表达式的每个可选项打印输出一个值。只生成值而不是打印也可以一样做到,展示在代码7.15中。match表达式产生的值储存在friend变量里。这除了能让代码变得更短之外(至少减少了几个指令),还解开了两个不相干的关注点:首先选择食物名,其次打印它。

内建控制结构内建控制结构
val firstArg = if (!args.isEmpty) args(0) else "" val friend = firstArg match { case "salt" => "pepper" case "chips" => "salsa" case "eggs" => "bacon" case _ => "huh?" } println(friend)
View Code

7.6 离开break和continue

你可能注意到了这里没有提到过break和continue。Scala去掉了这些命令因为他们与函数式文本,下一章会谈到这个特征,啮合得不好。continue在while循环中的意思很清楚,但是在函数式文本中表示什么呢?虽然Scala既支持指令式风格也支持函数式风格,但在这点上它略微倾向于函数式编程从而换得在语言上的简洁性。尽管如此,请不要着急。有许多不用break和continue的编程方式,如果你能有效利用函数式文本,就能比原来的代码写得更短。

最简单的方式是用if替换每个every和用布尔变量替换每个break。布尔变量指代是否包含它的while循环应该继续。比如说,假设你正搜索一个参数列表去查找以“.scala”结尾但不以连号开头的字串。Java里你可以——如果你很喜欢while循环,break和continue——如此写

内建控制结构内建控制结构
int i = 0; // 在Java中…… boolean foundIt = false; while (i < args.length) { if (args[i].startsWith("-")) { i = i + 1; continue; } if (args[i].endsWith(".scala")) { foundIt = true; break; } i = i + 1; }
View Code
如果要字面直译成Scala的代码,代之以执行一个if然后continue,你可以写一个if环绕while余下的全部内容。要去掉break,你可以增加一个布尔变量提示是否继续做下去,不过在这里你可以复用foundIt。使用这两个技巧,代码就可以像代码7.16这样完成了: 内建控制结构内建控制结构
var i = 0 var foundIt = false while (i < args.length && !foundIt) { if (!args(i).startsWith("")) { if (args(i).endsWith(".scala")) foundIt = true } i = i + 1 }
View Code

如果要去掉代码7.16里面的var,你可以尝试的一种方式是用递归函数重写循环。比方说,你可以定义带一个整数值做输入的searchFrom函数,向前搜索,并返回想要的参数的索引。采用这种技巧的代码看上去会像展示在代码7.17中这样的:

7.7 变量范围

现在你已经看过了Scala的内建控制结构,我们将在本节中使用它们来解释Scala里的范围是如何起作用的。

Java程序员的快速通道 如果你是Java程序员,你会发现Scala的范围规则几乎是Java的翻版。然而,两者之间仍然有一个差别,Scala允许你在嵌套范围内定义同名变量。因此如果你是Java程序员,或许至少还是快速浏览一下。

Scala程序里的变量定义有一个能够使用的范围:scope。范围设定的最普通不过的例子就是,大括号通常引入了一个新的范围,所以任何定义在打括号里的东西在括号之后就脱离了范围

内建控制结构内建控制结构
def printMultiTable() { var i = 1 // 这里只有i在范围内 while (i <= 10) { var j = 1 // 这里i和j在范围内 while (j <= 10) { val prod = (i * j).toString // 这里i,j和prod在范围内 var k = prod.length// 这里i,j,prod和k在范围内 while (k < 4) { print(" ") k += 1 } print(prod) j += 1 } // i和j仍在范围内;prod和k脱离范围 println() i += 1 } // i仍在范围内;j,prod和k脱离范围 }
View Code

你可以在这使用i因为它仍在范围内。在while循环的第一个语句里,你引入了另一个变量,叫做j,并再次初始化为1。因为变量j定义在while循环的大括号内,所以只能用在while循环里。如果你想尝试在while循环的大括号之后,在那个说j,prod和k已经出了范围的注释后面,再用j做点儿什么事,你的程序就编译不过

Scala和Java间要注意的一个不同是,与Scala不同,Java不允许你在内部范围内创建与外部范围变量同名的变量。在Scala程序里,内部变量被说成是遮蔽:shadow了同名的外部变量,因为在内部范围内外部变量变得不可见了

或许你已经注意到了一些在解释器里看上去像是遮蔽的东西

解释器里,你可以对你的核心内容重用变量名。撇开别的不说,这样能允许你当发现你在解释器里第一次定义变量时犯了错误的时候改变主意。你能这么做的理由是因为,在理论上,解释器在每次你输入新的语句时都创建了一个新的嵌套范围。因此,你可以把之前解释的代码虚拟化认为是:

内建控制结构内建控制结构
1 val a = 1; { var a = 2; { println(a) } }
View Code

这段代码可以像Scala脚本那样编译和执行,而且像输入到解释器里的代码那样,打印输出2。请记住这样的代码对读者来说是很混乱的,因为在嵌套范围中变量名称拥有了新的涵义。通常更好的办法是选择一个新的有意义的变量名而不是遮蔽外部变量。

7.8 重构指令式风格的代码

为了帮助你在函数式风格上获得更多的领悟,本节我们将重构代码7.18中以指令式风格打印乘法表的方式。我们的函数式替代品展示在代码7.19中。

代码7.18中的代码在两个方面显示出了指令式风格。首先,调用printMultiTable有副作用:在标准输出上打印乘法表。代码7.19中,我们重构了函数,让它把乘法表作为字串返回。由于函数不再执行打印,我们把它重命名为multiTable。正如前面提到过的,没有副作用的函数的一个优点是它们很容易进行单元测试。要测试printMultiTable,你需要重定义print和println从而能够检查输出的正确性。测试multiTable就简单多了,只要检查结果即可。

内建控制结构内建控制结构
// 以序列形式返回一行乘法表 def makeRowSeq(row: Int) = for (col <- 1 to 10) yield { val prod = (row * col).toString val padding = " " * (4 - prod.length) padding + prod } // 以字串形式返回一行乘法表 def makeRow(row: Int) = makeRowSeq(row).mkString // 以字串形式返回乘法表,每行记录占一行字串 def multiTable() = { val tableSeq = // 行记录字串的序列 for (row <- 1 to 10) yield makeRow(row) tableSeq.mkString("\n") }
View Code

printMultiTable里另一个揭露其指令式风格的信号来自于它的while循环和var。与之相对,multiTable函数使用了val,for表达式,帮助函数:helper function,并调用了mkString。 我们提炼出两个帮助函数,makeRow和makeRowSeq,使代码容易阅读。函数makeRowSeq使用for表达式从1到10枚举列数。这个for函数体计算行和列的乘积,决定乘积前占位的空格,并生成由占位空格,乘积字串叠加成的结果。for表达式的结果是一个包含了这些生成字串作为元素的序列(scala.Seq的某个子类)。另一个帮助函数,makeRow,仅仅调用了makeRowSeq返回结果的mkString函数。叠加序列中的字串把它们作为一个字串返回。 multiTable方法首先使用一个for表达式的结果初始化tableSeq,这个for表达式从1到10枚举行数,对每行调用makeRow获得该行的字串。因为字串前缀yield关键字,所以表达式的结果就是行字串的序列。现在仅剩下的工作就是把字串序列转变为单一字串。mkString的调用完成这个工作,并且由于我们传递进去"\n",因此每个字串结尾插入了换行符。如果把multiTable返回的字串传递给println,你将看到与调用printMultiTable所生成的同样的输出结果