2.6 字符串的逐个字符处理

问题

你想知道如何遍历一个字符串中的每个字符,并在遍历字符串时对每个字符执行操作。

解决方案

如果需要转换字符串中的字符以得到一个新的结果(而不是副作用),应该使用for表达式或者高阶函数(Higher-Order Function,HOF)(如map和filter)。如果想做一些有副作用的的操作,如打印输出,那么应该使用简单的for循环或者使用像foreach这样的方法。如果需要把字符串当作一个字节序列,可以使用getBytes方法。

转换

以下是一个for表达式的例子,它是一个带有yield的for循环,对字符串中每一个字符进行转换:

下面是与之等价的map方法:

这段代码还可以使用Scala的下划线字符来变得更短:

使用高阶函数和纯转换函数的好处是,可以将它们串联起来,得到想要的结果。下面是一个先调用filter然后调用map的例子:

副作用

当需要执行一个副作用时,如将一个字符串中的每个字符输出到STDOUT,就可以使用一个简单for循环:

也可以使用foreach方法:

处理字符串字节

如果需要将一个字符串作为一个字节序列来处理,也可以使用getBytes方法。getBytes返回一个字符串的字节序列集合:

在getBytes之后添加foreach展示了对每个字节进行操作的一种方式:

写一个能传入map的方法

要写一个可以传入map的方法来对字符串中的字符进行操作,需要定义一个Char作为输入,然后在方法中对该Char进行逻辑运算。当运算完成后,返回你的算法所需的数据类型。虽然下面的算法很短,但它演示了如何创建一个自定义方法并将该方法传入map:

请参阅10.2节中对Eta Expansion的讨论,以了解更多关于如何将一个方法传递给另一个以函数作为参数的方法的细节。

讨论

因为Scala将字符串视为一个字符序列Seq[Char],所以下面的所有例子都能正常工作。

for+yield

如果你是从命令式语言(Java、C、C#等)转到Scala,一开始使用map方法可能会不太适应。在这种情况下,你可能更愿意写一个类似于这样的for表达式:

在for循环中加入yield,本质上是将每次循环迭代的结果放入一个临时的保留区域。当循环完成时,保留区域中的所有元素将作为一个单一的集合返回,也可以说它们是由for循环产生的。

虽然我(强烈)建议你熟悉map方法的工作原理,但如果想在刚开始就使用for表达式,那么需要了解下面使用filter和map方法的表达式:

与下面的for表达式等价:

几乎不用写自定义的for循环

正如我在Scala官方网站上的第一版Scala Book(https://oreil.ly/2mkjT)中所写的,Scala集合类的一大优势是它们带有几十个预置的方法。这样做的一大好处是,每次需要处理一个集合时,你不再需要编写自定义的for循环。如果这听起来还不够吸引人的话,它还意味着你不再需要读别人写的各种for循环。

更重要的是,研究表明,开发人员花在阅读代码上的时间远远多于编写代码的时间,阅读时间/编写时间的比例估计最多20:1,最少也有10:1,因为我们花了这么多时间阅读代码,所以代码既要保持简洁又要具备可读性是很重要的,这也就是Scala开发者所说的表现力。

转换方法

一旦适应了“Scala风格”即利用Scala内置的转换函数,这样就不用写自定义的for循环了,而是去使用map方法。这两个map表达式产生的结果都与for表达式相同:

像map这样的转换方法既可以接受一个如上所示的单行匿名函数,也可以接受一个更大的代码块算法。下面是一个使用多行代码块的map的例子:

注意,这个算法是用大括号括起来的,当创建一个像这样的多行代码块时,都需要使用大括号。

你可能会从这些例子中推测出map有一个内置的循环,在这个循环中,它一次将一个Char传递给它的参数函数。

在继续之前,这里还有几个字符串转换方法的例子:

副作用方法

map或for/yield方法用于将一个集合转换为另一个集合,而foreach方法用来对每个元素进行操作而不返回结果,这完全可以从它的方法签名的返回值是Unit推断出来:

所以,foreach能很好地处理副作用,比如输出:

一个完整的例子

下面的例子演示了如何在一个字符串上调用getBytes,然后将一个代码块传入foreach,用来计算一个字符串的Adler-32校验值(https://oreil.ly/xbTRd):

@main方法中的第二个println语句输出十六进制值11e60398,这与Adler-32算法页面上的0x11E60398相匹配。

请注意,我在这个例子中使用了foreach而不是map,目的是在字符串的每个字节上循环,然后对每个字节做一些操作,而不从循环中返回任何东西。与map不同的是,这个算法会更新可变变量a和b。

另见

·Scala编译器会将for循环翻译成foreach方法调用。如果循环中有一个或多个if语句(守卫)或yield表达式,情况则会变得复杂的多。这在我的Functional Programming,Simplifiedhttps://oreil.ly/wKfgQ)(CreateSpace出版社)一书中有详细讨论。

·Adler代码基于维基百科中对Adler-32校验值算法的讨论(https://oreil.ly/xbTRd)。