第2章 字符串

作为程序员,我们经常和字符串打交道,如名字、地址、电话号码等。Scala提供的字符串操作非常棒,因为除了拥有Java字符串的全部功能以外,还有一些新的扩展。本章将会在一些小节中介绍其字符串格式化和正则表达式相关的功能,而其他的小节则会展示Scala字符串的一些独有的功能。

Scala与Java语法一个最大的区别是字符串的声明方式。所有的Scala变量都是以val或var的形式声明的,因此一个字符串变量通常是这样创建的:

这个表达式与下面的Java代码等价:

在Scala中,一般来说总是将变量声明为val,除非有充分的理由使用var。(纯函数式编程要求则更严格,禁止使用var。)

也可以显式地声明成字符串类型:

这么做只会让代码变得冗长,所以并不推荐。因为Scala的类型推断非常强大,所以第一个例子中使用的隐式语法就是首选方式。在实际情况下,我其实只有在调用一个方法并且不确定其返回类型时,才会在创建变量时明确声明其类型,例如:

Scala字符串功能

Scala字符串的功能有:

·能使用==来比较字符串是否相等。

·多行字符串。

·字符串插值操作,可以写出类似println(s"Name: $name")的代码。

·数十种额外的函数方法可以将字符串当作字符序列来处理。

本章将展示所有这些功能。

字符串是字符序列

上面提到的比较新颖的一个要点是,Scala的字符串可以被当作字符序列来看待,也就是当作Seq[Char]。因此,给出以下字符串:

下面是一些常用的“序列”方法,可以在该字符串上进行调用:

这些方法都是Seq的标准方法,将在第11章中深入介绍。

链式方法调用

Seq上所有的方法都是“函数式”的,这意味着它们不会改变现有的序列,而是在调用时返回一个新的序列,但foreach方法是一个例外,它返回Unit。由于这种函数式特点,可以在字符串上使用链式调用:

如果你之前没有见过这种写法,这里我先简单介绍一下这个例子的工作原理:drop是集合的一个方法,它从集合的开头丢弃指定数量的元素,保留剩余的元素。当这里调用drop(2)时,它从字符串(scala)中丢掉前两个字符(sc),并返回剩余的元素:

然后,take(2)方法保留了字符串"ala"中的前2个字符,并丢弃了其他字符:

最后,调用capitalize得到最终结果:

如果不熟悉这样的链式调用,它也被称为流式编程风格。请参阅8.8节以了解更多的信息。在函数式编程中这种代码非常常见,每个函数都是纯函数并返回一个值。这种风格同样在RxJava和RxScala等Rx技术中很流行,同时也被大量用于Apache Spark中。

这些方法从哪来

熟悉Java的人都知道Java的String类并没有capitalize方法,所以Scala上有这个方法可能会让人惊讶。Scala字符串在Java字符串的基础上扩充了几十个额外的方法,而这些方法可以通过Eclipse或者IntelliJ IDEA等IDE的“代码辅助”功能看到。

当你通过这种方式看到这些可用的方法并且得知Scala没有字符串类时,可能会觉得很惊讶。如果没有String类,字符串是如何拥有这些方法的?

其工作原理是,Scala通过隐式转换和扩展方法“继承”了Java的String类,从而完成了向其添加方法的功能。隐式转换是Scala 2中向封闭类添加方法的方式,而扩展方法则是Scala 3中使用的方式。关于如何创建扩展方法的细节,请参阅8.9节。

虽然这可能会随着时间的推移而改变,但在Scala 3.0中,Scala字符串的许多额外方法定义在StringOps类中,这些方法会随着scala.Predef自动导入代码中,在Scala 2.13的Predef对象(它也被Scala 3所使用)中可以找到这个隐式转换以及相关文档:

augmentString将一个字符串转换成StringOps类型。这样做会将StringOps中的方法添加到所有的Scala字符串实例中,其中包括drop、take和filter这些能把字符串当作字符序列处理的方法。

阅读Predef源代码

我强烈建议Scala的初学者阅读Scala 2.13的scala.Predef对象(https://oreil.ly/kkdJM)的源码,可以在Scaladoc页面上找到源代码链接,它提供了许多与Scala编程特性相关的例子。可以看到它是如何引入其他像StringOps和WrappedString这样的类型的。