Scala编程(第5版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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的元素。每进行一次迭代,一个新的名称为fileval都会被初始化成一个元素的值。编译器推断出文件的类型为File,这是因为filesHere是一个Array[File]。每进行一次迭代,for表达式的代码体——println(file),就被执行一次。由于FiletoString方法会返回文件或目录的名称,因此这段代码将会打印出当前目录的所有文件和子目录。

for表达式的语法可以用于任何种类的集合,而不仅仅是数组。[3]Range(区间)是一类特殊的用例,在表5.4(93页)中简略地提到过。可以用“1 to 5”这样的语法来创建Range,并用for表达式来遍历它。下面是一个简单的例子:

如果你不想在被遍历的值中包含区间的上界,则可以用until而不是to

在Scala中像这样遍历整数是常见的做法,不过与其他语言相比,要少一些。在其他语言中,你可能会通过遍历整数来遍历数组,就像这样:

这个for表达式引入了一个变量i,依次将0filesHere.length - 1之间的每个整数值赋值给它。每次对i赋值后,filesHere的第i个元素都会被提取出来做相应的处理。

在Scala中,这类遍历方式不那么常见的原因是可以直接遍历集合。这样做了以后,你的代码会更短,也避免了很多在遍历数组时会遇到的偏一位off-by-one)的错误。应该以0还是1开始?应该对最后一个下标后加上-1+1,还是什么都不加?这些疑问很容易回答,但也很容易答错。完全避免这些问题无疑是更安全的做法。

过滤

有时你并不想完整地遍历集合,但你想把它过滤成一个子集。这时你可以给for表达式添加过滤器(filter)。过滤器是for表达式的圆括号中的一个if子句。举例来说,示例7.6的代码仅列出当前目录中以“.scala”结尾的那些文件:

示例7.6 用带过滤器的for表达式查找.scala文件

也可以用如下代码达到同样的目的:

这段代码与前一段代码产生的输出没有区别,可能看上去对于有指令式编程背景的程序员来说更为熟悉。这种指令式编程的代码风格只是一种选项[4],因为这个特定的for表达式被用作打印的副作用,其结果是单元值()。稍后你将看到,for表达式之所以被称为“表达式”,是因为它能返回有意义的值,即一个类型可以由for表达式的<-子句决定的集合。

若想随意包含更多的过滤器,则直接添加if子句即可。例如,为了让代码具备额外的防御性,示例7.7的代码只输出文件名,不输出目录名。实现方式是添加一个检查文件的isFile方法的过滤器。

示例7.7 在for表达式中使用多个过滤器

嵌套迭代

如果你添加多个<-子句,将得到嵌套的“循环”。例如,示例7.8中的for表达式有两个嵌套迭代。外部循环遍历filesHere,内部循环遍历每个以.scala结尾的文件的fileLines(file)

示例7.8 在for表达式中使用多个生成器

中途(mid-stream)变量绑定

你大概已经注意到,示例7.8中line.trim被重复了两遍。这并不是一个很无谓的计算,因此你可能想最好只算一次。可以用等号(=)将表达式的结果绑定到新的变量上。被绑定的这个变量在引入和使用时都与val一样,只不过去掉了val关键字。示例7.9给出了一个例子。

在示例7.9中,for表达式在中途引入了名称为trimmed的变量。这个变量被初始化为line.trim的结果。for表达式余下的部分则两次用到了这个新的变量,一次在if表达式中,另一次在println中。

示例7.9 在for表达式中使用中途赋值

交出一个新的集合

虽然目前为止所有示例都是对遍历到的值进行操作然后忽略它,但是完全可以在每次迭代中生成一个可以被记住的值。具体做法就像我们在第3章的第12步介绍的,在for表达式的代码体之前加上关键字yield而不是do。例如,如下函数识别出.scala文件并将它保存在数组中:

for表达式的代码体每次被执行,都会交出一个值,本例中就是file。当for表达式执行完毕后,其结果将包含所有交出的值,且被包含在一个集合中。结果集合的类型基于迭代子句中处理的集合种类。在本例中,结果是Array[File],因为filesHere是一个数组,而交出的表达式类型为File

下面再看一个例子,示例7.10中的for表达式先将包含当前目录所有文件的名称为filesHereArray[File]转换成一个只包含.scala文件的数组。对于每一个文件,再使用fileLines方法(参见示例7.8)的结果生成一个Iterator[String]Iterator提供的nexthasNext方法,可以用来遍历集合中的元素。这个初始的迭代器又被转换成另一个Iterator[String],这一次只包含那些包含子串"for"的被去边的字符串。最后,对这些字符串再交出其长度的整数。这个for表达式的结果是包含这些长度整数的Array[Int]

示例7.10 用for表达式将Array[File]转换成Array[Int]

至此,你已经看到了Scala的for表达式的所有主要功能特性,不过我们讲得比较快。有关for表达式更完整的讲解请参考《Scala高级编程》。