Scala的for表达式是为枚举准备的“瑞士军刀”。它可以让你用不同的方式把若干简单的成分组合来表达各种各样的枚举。简单的用法完成如把整数序列枚举一遍那样通常的任务。更高级的表达式可以列举不同类型的多个集合,可以用任意条件过滤元素,还可以制造新的集合。
枚举集合类
你能用for做的最简单的事情就是把一个集合类的所有元素都枚举一遍。如,代码7.5展示了打印当前目录所有文件名的例子。I/O操作使用了Java的API。首先,我们创建指向当前目录,".",的文件,然后调用它的listFiles方法。方法返回File对象数组,每个都代表当前目录包含的目录或文件。我们把结果数组保存在filesHere变量。
val filesHere = (new java.io.File(".")).listFiles for (file < - filesHere) println(file)
代码 7.5 用for循环列表目录中的文件
通过使用被称为发生器:generator的语法“file < - filesHere”,我们遍历了filesHere的元素。每一次枚举,名为file的新的val就被元素值初始化。编译器推断file的类型是File,因为filesHere是Array[File]。对于每一次枚举,for表达式的函数体,println(file),将被执行一次。由于File的toString方法产生文件或目录的名称,因此当前目录的所有文件和目录的名称都会被打印出来。
for表达式语法对任何种类的集合类都有效,而不只是数组。更精确地说,在<-符号右侧的表达式必须支持名为foreach的方法。第80页的表格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
如果你不想包括被枚举的Range的上边界,可以用until替代to:
scala> for (i < - 1 until 4) println("Iteration " + i) Iteration 1 Iteration 2 Iteration 3
像这样枚举整数在Scala里是很平常的,但在其他语言中就不是这么回事。其它语言中,你或许要采用如下方式遍历数组:
// Scala中不常见…… for (i < - 0 to filesHere.length - 1) println(filesHere(i))
// Scala中不常见…… for (i < - 0 to filesHere.length - 1) println(filesHere(i))
这个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)
代码 7.6 用带过滤器的for发现.scala文件
或者你也可以这么写:
for (file < - filesHere) if (file.getName.endsWith(".scala")) println(file)
这段代码可以产生与前一段代码同样的输出,而且对于指令式背景的程序员来说看上去更熟悉一些。然而指令式格式只是一个可选项,因为这个for表达式的运用执行的目的是为了它的打印这个副作用并产生unit值()。正如在本节后面将展示的,for表达式之所以被称为“表达式”是因为它能产生令人感兴趣的值,一个其类型取决于for表达式< -子句的集合。
如果愿意的话,你可以包含更多的过滤器。只要不断加到子句里即可。例如,为了加强防卫,代码7.7中的代码仅仅打印文件而不是目录。通过增加过滤器检查file的isFile方法做到:
for ( file < - filesHere if file.isFile; if file.getName.endsWith(".scala") ) println(file)
代码 7.7 在for表达式中使用多个过滤器
注意
如果在发生器中加入超过一个过滤器,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.*")
代码 7.8 在for表达式中使用多个发生器
如果愿意的话,你可以使用大括号代替小括号环绕发生器和过滤器。使用大括号的一个好处是你可以省略一些使用小括号必须加的分号。
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.*")
代码 7.9 在for表达式里的流间赋值
代码中,名为trimmed的变量被从半当中引入for表达式,并被初始化为line.trim的结果值。之后的for表达式就可以在两个地方使用这个新变量,一次在if中,一次在println中。
制造新集合
到现在为止所有的例子都只是对枚举值进行操作然后就放过,除此之外,你还可以创建一个值去记住每一次的迭代。只要在for表达式之前加上关键字yield。比如,下面的函数鉴别出.scala文件并保存在数组里:
def scalaFiles = for { file < - filesHere if file.getName.endsWith(".scala") } yield file
for表达式在每次执行的时候都会制造一个值,本例中是file。当for表达式完成的时候,结果将是一个包含了所有产生的值的集合。结果集合的类型基于枚举子句处理的集合类型。本例中结果为Array[File],因为filesHere是数组并且产生的表达式类型是File。
另外,请注意放置yield关键字的地方。对于for-yield表达式的语法是这样的:
- for {子句} yield {循环体}
yield在整个循环体之前。即使循环体是一个被大括号包围的代码块,也一定把yield放在左括号之前,而不是代码块的最后一个表达式之前。请抵挡住写成如下方式的诱惑:
for (file < -filesHere if file.getName.endsWith(".scala")) { yield file // 语法错误! }
例如,代码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
代码 7.10 用for把Array[File]转换为Array[Int]