7.3 for表达式
Scala的for表达式是用于迭代的瑞士军刀,它让你以不同的方式组合一些简单的因子来表达各式各样的迭代。它可以帮助我们处理诸如遍历整数序列的常见任务,也可以通过更高级的表达式来遍历多个不同种类的集合,根据任意条件过滤元素,产出新的集合。
遍历集合
用for能做的最简单的事,是遍历某个集合的所有元素。例如,示例7.5展示了一组打印出当前目录所有文件的代码。I/O操作用到了Java API。首先,我们对当前目录(".")创建一个java.io.File对象,然后调用它的listFiles方法。这个方法返回一个包含File对象的数组,这些对象分别对应当前目录中的每个子目录或文件。我们将结果数组保存在filesHere变量中。
7.5 用for表达式列举目录中的文件清单
通过“file <- filesHere”这样的生成器(generator)语法,我们将遍历filesHere的元素。每一次迭代,一个新的名为file的val都会被初始化成一个元素的值。编译器推断出文件的类型为File,这是因为filesHere是个Array[File]。每做一次迭代,for表达式的代码体println(file)就被执行一次。由于File的toString方法会返回文件或目录的名称,这段代码将会打印出当前目录的所有文件和子目录。
for表达式的语法可以用于任何种类的集合,而不仅仅是数组。[4]Range(区间)是一类特殊的用例,在表5.4中(91页)简略地提到过。可以用“1 to 5”这样的语法来创建Range,并用for来遍历它们。以下是一个简单的例子:
如果你不想在被遍历的值中包含区间的上界,可以用until而不是to:
在Scala中像这样遍历整数是常见的做法,不过跟其他语言比起来,要少一些。在其他语言中,你可能会通过遍历整数来遍历数组,就像这样:
这个for表达式引入了一个变量i,依次将0到filesHere.length - 1之间的每个整数值赋值给它,每次对i赋完值以后,filesHere的第i个元素都被提取出来做相应的处理。
在Scala中这类遍历方式不那么常见的原因是可以直接遍历集合。这样做了以后,你的代码会更短,也避免了很多在遍历数组时会遇到的偏一位(off-by-one)的错误。应该以0还是以1开始?应该对最后一个下标后加上-1、+1还是什么都不加?这些疑问很容易回答,但同时也很容易答错。完全避免回答这些问题无疑是更安全的做法。
过滤
有时你并不想完整地遍历集合,你想把它过滤成一个子集。这时可以给for表达式添加过滤器(filter),过滤器是for表达式的圆括号中的一个if子句。举例来说,示例7.6的代码仅列出当前目录中以“.scala”结尾的那些文件:
示例7.6 用带过滤器的for表达式查找.scala文件
也可以用如下代码达到同样的目的:
这段代码跟前一段产生的输出没有区别,可能有指令式编程背景的程序员看上去更为熟悉。这种指令式的代码风格只是一种选项(不是默认和推荐的做法),因为这个特定的for表达式被用作打印的副作用,其结果是单元值()。稍后你将看到,for表达式之所以被称作“表达式”,是因为它能返回有意义的值,一个类型可以由for表达式的<-子句决定的集合。
若想随意包含更多的过滤器,直接添加if子句即可。例如,为了让我们的代码具备额外的防御性,示例7.7的代码只输出文件名,不输出目录名。实现方式是添加一个检查file的isFile方法的过滤器。
示例7.7 在for表达式中使用多个过滤器
嵌套迭代
如果你添加多个<-子句,将得到嵌套的“循环”。例如,示例7.8中的for表达式有两个嵌套迭代。外部循环遍历filesHere,内部循环遍历每个以.scala结尾的file的fileLines(file)。
示例7.8 在for表达式中使用多个生成器
如果你愿意,也可以使用花括号而不是圆括号来包括生成器和过滤器。这样做的一个好处是可以在需要时省去某些分号,因为Scala编译器在圆括号中并不会自动推断分号(参考4.2节)。
中途(mid-stream)变量绑定
你大概注意到前一例中line.trim重复了两遍。这并不是一个很无谓的计算,因此你可能想最好只算一次。可以用=来将表达式的结果绑定到新的变量上。被绑定的这个变量引入和使用起来都跟val一样,只不过去掉了val关键字。示例7.9给出了一个例子。
示例7.9 在for表达式中使用中途赋值
在示例7.9中,for表达式的中途,引入了名为trimmed的变量,这个变量被初始化为line.trim的结果。for表达式余下的部分则在两处用到了这个新的变量,一次在if中,另一次在println中。
产出一个新的集合
虽然到目前为止所有示例都是对遍历到的值进行操作然后忘掉它们,也完全可以在每次迭代中生成一个可以被记住的值。具体做法是在for表达式的代码体之前加上关键字yield。例如,如下函数识别出.scala文件并将它们保存在数组中:
for表达式的代码体每次被执行,都会产出一个值,本例中就是file。当for表达式执行完毕后,其结果将包含所有交出的值,包含在一个集合当中。结果集合的类型基于迭代子句中处理的集合种类。在本例中,结果是Array[File],因为filesHere是个数组,而交出的表达式类型为File。
要小心yield关键字的位置。for-yield表达式的语法如下:
for子句yield代码体
yield关键字必须出现在整个代码体之前。哪怕代码体是由花括号包起来的,也要将yield放在花括号之前,而不是在代码块最后一个表达式前面。避免像这样使用yield:
举例来说,示例7.10里的for表达式首先将包含当前目录所有文件的名为filesHere的Array[File]转换成一个只包含.scala文件的数组。对每一个文件,再用fileLines方法(参见示例7.8)的结果生成一个Array[String]。这个Array[string]里的每个元素都各自包含了当前被处理文件中的一行。这个初始的Array[String]又被转换成另一个Array[String],这一次只包含那些包含子串"for"的被去边的字符串。最后,对这些字符串再交出其长度的整数。这个for表达式的结果是包含这些长度整数的Array[Int]。
示例7.10 用for表达式将Array[File]转换成Array[Int]
至此,你已经看到了Scala的for表达式的所有主要功能特性,不过我们讲得比较快,后面会在第23章给出对for表达式更完整的讲解。