元组和集合
在 C++ 中,我们将之称为结构体。在 Java 编程中,我们称之为数据传输对象或参数对象。在 Scala 中,我们称为元组。实质上,它们是一些将其他数据类型收集到单个实例的类,并且不使用封装或抽象 — 实际上,不 使用任何抽象常常更有用。
在 Scala 创建一个元组类型非常的简单,这只是主体的一部分:如果首先将元素公开给外部,那么在类型内部创建描述这些元素的名称就毫无价值。考虑清单 3:
清单 3. tuples.scala// JUnit test suite
//
classTupleTest
{
importorg.junit._, Assert._
importjava.util.Date
@Testdef simpleTuples() =
{
val tedsStartingDateWithScala = Date.parse("3/7/2006")
val tuple = ("Ted","Scala", tedsStartingDateWithScala)
assertEquals(tuple._1,"Ted")
assertEquals(tuple._2,"Scala")
assertEquals(tuple._3, tedsStartingDateWithScala)
}
}
创建元组非常简单,将值放入一组圆括号内,就好象调用一个方法调用一样。提取这些值只需要调用 “_n” 方法,其中 n 表示相关的元组元素的位置参数:_1 表示第一位,_2 表示第二位,依此类推。传统的 Java java.util.Map 实质上是一个分两部分的元组集合。
元组可以轻松地实现使用单个实体移动多个值,这意味着元组可以提供在 Java 编程中非常重量级的操作:多个返回值。例如,某个方法可以计算 String 中字符的数量,并返回该 String 中出现次数最多的字符,但是如果程序员希望同时 返回最常出现的字符和 它出现的次数,那么程序设计就有点复杂了:或是创建一个包含字符及其出现次数的显式类,或将值作为字段保存到对象中并在需要时返回字段值。无论使用哪种方法,与使用 Scala 相比,都需要编写大量代码;通过简单地返回包含字符及其出现次数的元组,Scala 不仅可以轻松地使用 “_1”、“_2” 等访问元组的各个值,还可以轻松地返回多个返回值。
如下节所示,Scala 频繁地将 Option 和元组保存到集合(例如 Array[T] 或列表)中,从而通过一个比较简单的结构提供了极大的灵活性和威力。
数组带您走出阴霾
让我们重新审视一个老朋友 — 数组 — 在 Scala 中是 Array[T]。和 Java 代码中的数组一样,Scala 的 Array[T] 是一组有序的元素序列,使用表示数组位置的数值进行索引,并且该值不可以超过数组的总大小,如清单 4 所示:
清单 4. array.scalaobject ArrayExample1
{
def main(args : Array[String]) : Unit =
{
for(i 0to args.length-1)
{
System.out.println(args(i))
}
}
}
尽管等同于 Java 代码中的数组(毕竟后者是最终的编译结果),Scala 中的数组使用了截然不同的定义。对于新手,Scala 中的数组实际上就是泛型类,没有增加 “内置” 状态(至少,不会比 Scala 库附带的其他类多)。例如,在 Scala 中,数组一般定义为 Array[T] 的实例,这个类定义了一些额外的有趣方法,包括常见的 “length” 方法,它将返回数组的长度。因此,在 Scala 中,可以按照传统意义使用 Array,例如使用 Int 在 0 到 args.length - 1 间进行迭代,并获取数组的第 i 个元素(使用圆括号而不是方括号来指定返回哪个元素,这是另一种名称比较有趣的方法)。
扩展数组
事实证明 Array 拥有大量方法,这些方法继承自一个非常庞大的 parent 层次结构:Array 扩展 Array0,后者扩展 ArrayLike[A],ArrayLike[A] 扩展 Mutable[A],Mutable[A] 又扩展 RandomAccessSeq[A],RandomAccessSeq[A] 扩展了 Seq[A],等等。实际上,这种层次结构意味着 Array 可以执行很多操作,因此与 Java 编程相比,在 Scala 中可以更轻松地使用数组。
例如,如清单 4 所示,使用 foreach 方法遍历数组更加简单并且更贴近函数的方式,这些都继承自 Iterable 特性:
清单 5. ArrayExample2object
{
def main(args : Array[String]) : Unit =
{
args.foreach( (arg) => System.out.println(arg) )
}
}
看上去您没有节省多少工作,但是,将一个函数(匿名或其他)传入到另一个类中以便获得在特定语义下(在本例中指遍历数组)执行的能力,是函数编程的常见主题。以这种方式使用更高阶函数并不局限于迭代;事实上,还得经常对数组内容执行一些过滤 操作去掉无用的内容,然后再处理结果。例如,在 Scala 中,可以轻松地使用 filter 方法进行过滤,然后获取结果列表并使用 map 和另一个函数(类型为 (T) => U,其中 T 和 U 都是泛型类型),或 foreach 来处理每个元素。我在清单 6 中采取了后一种方法(注意 filter 使用了一个 (T) : Boolean 方法,意味着使用数组持有的任意类型的参数,并返回一个 Boolean)。
清单 6. 查找所有 Scala 程序员classArrayTest
{
importorg.junit._, Assert._
@Testdef testFilter =
{
val programmers = Array(
newPerson("Ted","Neward",37,50000,
Array("C++","Java","Scala","Groovy","C#","F#","Ruby")),
newPerson("Amanda","Laucher",27,45000,
Array("C#","F#","Java","Scala")),
newPerson("Luke","Hoban",32,45000,
Array("C#","Visual Basic","F#")),
newPerson("Scott","Davis",40,50000,
Array("Java","Groovy"))
)
// 查找所有Scala程序员 ...
val scalaProgs =
programmers.filter((p) => p.skills.contains("Scala") )
// 应该只有2
assertEquals(2, scalaProgs.length)
// ... now perform an operation on each programmer in the resulting
// array of Scala programmers (give them a raise, of course!)
//
scalaProgs.foreach((p) => p.salary +=5000)
// Should each be increased by 5000 ...
assertEquals(programmers(0).salary,50000+5000)
assertEquals(programmers(1).salary,45000+5000)
// ... except for our programmers who don't know Scala
assertEquals(programmers(2).salary,45000)
assertEquals(programmers(3).salary,50000)
}
}
创建一个新的 Array 时将用到 map 函数,保持原始的数组内容不变,实际上大多数函数性程序员都喜欢这种方式:
清单 7. Filter 和 map@Testdef testFilterAndMap =
{
val programmers = Array(
newPerson("Ted","Neward",37,50000,
Array("C++","Java","Scala","C#","F#","Ruby")),
newPerson("Amanda","Laucher",27,45000,
Array("C#","F#","Java","Scala")),
newPerson("Luke","Hoban",32,45000,
Array("C#","Visual Basic","F#"))
newPerson("Scott","Davis",40,50000,
Array("Java","Groovy"))
)
// Find all the Scala programmers ...
val scalaProgs =
programmers.filter((p) => p.skills.contains("Scala") )
// Should only be 2
assertEquals(2, scalaProgs.length)
// ... now perform an operation on each programmer in the resulting
// array of Scala programmers (give them a raise, of course!)
//
def raiseTheScalaProgrammer(p : Person) =
{
newPerson(p.firstName, p.lastName, p.age,
p.salary +5000, p.skills)
}
val raisedScalaProgs =
scalaProgs.map(raiseTheScalaProgrammer)
assertEquals(2, raisedScalaProgs.length)
assertEquals(50000+5000, raisedScalaProgs(0).salary)
assertEquals(45000+5000, raisedScalaProgs(1).salary)
}
注意,在清单 7 中,Person 的 salary 成员可以标记为 “val”,表示不可修改,而不是像上文一样为了修改不同程序员的薪资而标记为 “var”。
Scala 的 Array 提供了很多方法,在这里无法一一列出并演示。总的来说,在使用数组时,应该充分地利用 Array 提供的方法,而不是使用传统的 for ... 模式遍历数组并查找或执行需要的操作。最简单的实现方法通常是编写一个函数(如果有必要的话可以使用嵌套,如清单 7 中的 testFilterAndMap 示例所示),这个函数可以执行所需的操作,然后根据期望的结果将该函数传递给 Array 中的 map、filter、foreach 或其他方法之一。