第8步 使用列表
函数式编程的重要理念之一是方法不能有副作用。一个方法唯一要做的是计算并返回一个值。这样做的好处是方法不再互相纠缠在一起,因此变得更可靠、更易复用。另一个好处(作为静态类型的编程语言)是类型检查器会检查方法的入参和出参,因此逻辑错误通常都是以类型错误的形式出现的。将这个函数式的哲学应用到对象的世界意味着让对象不可变。
正如你看到的,Scala数组是一个拥有相同类型的对象的可变序列。例如,一个Array[String]只能包含字符串。虽然无法在数组实例化以后改变其长度,但是可以改变它的元素值。因此,数组是可变的对象。
对于需要拥有相同类型的对象的不可变序列的场景,可以使用Scala的List类。与数组类似,一个List[String]只能包含字符串。Scala的List类(即scala.List)与Java的List类(即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类的某个方法,而这个方法的名称看上去像是会改变列表的时候,它实际上是创建并返回一个带有新值的新列表。例如,List类有个方法叫“:::”,用于列表拼接。用法如下:
执行这段脚本,oneTwoThreeFour将会指向List(1, 2, 3, 4),而oneTwo仍指向List(1, 2),threeFour仍指向List(3, 4)。参与计算的两个列表都没有被拼接操作符:::改变,而是返回了值为List(1, 2, 3, 4)的新列表。
也许在列表上用得最多的操作符是“::”,读作“cons”。它在一个已有列表的最前面添加一个新的元素,并返回这个新的列表。例如,如果执行下面这段代码:
oneTwoThree的值将会是List(1, 2, 3)。
注意
在表达式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所示。我们将在第14章揭示列表的全面功能。
为什么不在列表末尾追加元素
List类的确提供了“追加”(append)操作,写作:+(在第24章有详细介绍),但这个操作很少被使用,因为向列表(末尾)追加元素的操作所需要的时间随着列表的大小线性增加,而使用::在列表的前面添加元素只需要常量时间(constant time)。如果想通过追加元素的方式高效地构建列表,则可以依次在头部添加完成后,调用reverse方法。也可以用ListBuffer,这是一个可变的列表,它支持追加操作,完成后调用toList方法即可。ListBuffer在15.1节有详细介绍。
表3.1 List类的一些方法和用途
续表
续表