第8步 使用列表
函数式编程的重要理念之一是方法不能有副作用。一个方法唯一要做的是计算并返回一个值。这样做的好处是方法不再互相纠缠在一起,因此变得更可靠、更易复用。另一个好处(作为静态类型的编程语言)是类型检查器会检查方法的入参和出参,因此逻辑错误通常都以类型错误的形式出现。将这个函数式的哲学应用到对象的世界意味着让对象不可变。
正如你看到的,Scala数组是一个拥有相同类型的对象的可变序列。例如一个Array[String]只能包含字符串。虽然无法在数组实例化以后改变其长度,却可以改变它的元素值。因此,数组是可变的对象。
对于需要拥有相同类型的对象的不可变序列的场景,可以使用Scala的List类。跟数组类似,一个List[String]只能包含字符串。Scala的List(即scala.List)跟Java的java.util.List的不同在于Scala的List是不可变的,而Java的List是可变的。更笼统地说,Scala的List被设计为允许函数式风格的编程。创建列表的方法很简单,如示例3.3:
示例3.3 创建并初始化一个列表
示例3.3中的代码建立了一个新的名为oneTwoThree的val,并将其初始化成一个新的拥有整型元素1、2、3的List[Int]。[3]由于List是不可变的,它们的行为有点类似于Java的字符串:当你调用列表的某个方法,而这个方法的名字看上去像是会改变列表的时候,它实际上是创建并返回一个带有新值的新列表。例如,List有个方法叫“:::”,用于列表拼接。用法如下:
执行这段脚本,你将看到:
也许列表上用得最多的操作是“::”,读作“cons”。它在一个已有列表的最前面添加一个新的元素,并返回这个新的列表。例如,如果执行下面这段脚本:
将会看到:
注意
在表达式“1 :: twoThree”中,::是它右操作元(right operand,即twoThree这个列表)的方法。你可能会觉得::方法的结合性(associativity)有些奇怪,实际上其背后的规则很简单:如果一个方法被用在操作符表示法(operator notation)当中时,比如a * b,方法调用默认都发生在左操作元(left operand),除非方法名以冒号(:)结尾。如果方法名的最后一个字符是冒号,该方法的调用会发生在它的右操作元上。因此,在1 :: twoThree中,::方法调用发生在twoThree上,传入的参数是1,就像这样:twoThree.::(1)。关于操作符结合性的更多细节将在5.9节详细介绍。
表示空列表的快捷方式是Nil,初始化一个新的列表的另一种方式是用::将元素串接起来,并将Nil作为最后一个元素。[4]例如,如下脚本会产生跟前一个示例相同的输出,即“List(1, 2, 3)”:
Scala的List定义了大量有用的方法,大部分都列在表3.1中。我们将在第16章揭示列表的完整威力。
为什么不在列表末尾追加元素?
List类的确提供“追加”(append)操作,写作: +(在第24章有详细介绍),但这个操作很少被使用,因为往列表(末尾)追加元素的操作所需要的时间随着列表的大小线性增加,而使用::在列表的前面添加元素只需要固定的时间(constant time)。如果想通过追加元素的方式高效地构建列表,可以依次在头部添加完成后,再调用reverse。也可以用ListBuffer,这是个可变的列表,它支持追加操作,完成后调用toList即可。ListBuffer在22.2节有详细介绍。
表3.1 List的一些方法和用途
续表
续表