Scala 2.8中新的数组

 

Scala 2.8重新设计了数组,解决了以前版本中一直存在的问题。
 

存在的问题

为了与Java互操作,Scala在设计数组类型时就需要和Java的数组类型保持一致。虽然这样可以获得不错的性能,但是Java中对数组类型的限制实在是太多了。

首先,在Java中,数组的类型表示实际上并不是一个,而是有九个不同的类型表示:一个用来表示数组类型的引用,其它八个分别表示不同的原始类型的数组:byte, char, short, int, long, float, double 和 boolean。这样除了java.lang.Object,就无法用一个共通的类型来引用这些不同的类型表示。虽然可以利用反射java.lang.reflect.Array来操作数组中的部分属性,但是方便程度还远远达不到我们的要求。

其次,我们无法在Java中创建泛型数组,只允许创建单一类型的数组。

第三,Java数组提供的操作只有:索引,更新,还有获得数组的长度。

Scala在设计数组的时候希望能够打破上面的这些限制,并且加入更多我们想要的特性,比如数组应该整合进集合的继承体系中,提供所有Seq类所包含的方法。当然,数组还必须是泛型的。
 

之前的版本

Scala小组的成员认为他们从一开始就把数组的设计方向搞错了。一直到2.7.x版本,Scala都使用一种类似于从原始类型到对象类型的自动拆箱装箱的功能来处理数组。当我们使用new Array[T](T是类型参数)来创建一个数组时,Scala编译器会将其自动转换为new BoxedAnyArray[T]。BoxedAnyArray是一个特殊的类型,它能够根据对应的Java数组类型来改变它的表示。这种方式在大部分情况下是没有问题的,但是当我们进行类型转换和类型测试的时候,这种方式就会出现问题。同样,自动装箱拆箱所带来的性能问题也不能忽略。
 

如何解决

有一种方法可以解决上面提到的这些问题,就是引入两种数组表示:一种接近于Java的数组类型,用来对应和Java数组的互操作;另一种作为Scala集合框架的一部分,用来提供方便的方法来操作数组。当然,它们之间的联系就靠隐式转换了。但是这么做问题也很明显,就是在实际使用的时候,我们该选择使用哪个数组类型。这种不一致带给我们的问题就是库和组件的不统一。MacIver 和 Koltsov希望能够在编译时引入一些魔法来处理这个问题,比如自动将数组作为参数的方法编译成两个重载的版本。显然这不是长久之计。

还有一种做法就是利用隐式转换将数组直接集成到集合框架中。这种做法好处就是可以保证在Scala语言中只存在一种数组类型。利用隐式转换,我们可以将数组在必要时转换成为集合类,类似于Scala中将String转换为RichString的做法。但是这种做法问题也是存在的。String/RichString的问题就很显而易见,下面这种情况就让人非常迷惑:

"abc".reverse.reverse == "abc"    //false
"abc" != "abc".reverse.reverse     //true

造成上面这个问题的就是因为隐式转换。reverse方法是定义在Seq类中的,而且它的返回值类型也是Seq类型。因为字符串不是序列,那么在String类上调用reverse方法返回的类型中最合适的就是RichString了。但是String中equals方法是继承于Java的,所以它认为String和RichString是不相等的。
 

2.8中的集合

Scala2.8中新的集合同时解决了数组和String的问题。Scala2.8在集合的trait上增加了对表示进行抽象的trait。比如,现在除了我们熟悉的Seq trait,还增加了一个叫做SeqLike的trait:

trait SeqLike[+Elem, +Repr] { ... }

这个trait将表示类型参数化为Repr。这样就不用再假设它的表示类型了,而且这个表示类型也不用一定是Seq的子类型了。SeqLike中的方法——比如reverse——将不必再返回Seq类型,而用表示类型Repr来代替。Seq trait继承于SeqLike,并且将参数类型Repr具体指定为Seq:

trait Seq[+Elem] extends ... with SeqLike[Elem, Seq[Elem]] { ... }

同样这种做法也应用于集合中其它的trait,包括Traversable,Iterable 和 Vector。
 

新的数组

Scala 2.8 使用两个隐式转换将数组集成进了集合框架。第一个转换会将数组Array[T]映射到一个称为ArrayOps的类型对象上,这个ArrayOps是VectorLike[T, Array[T]]的一个子类型。通过这个转换,我们就可以在数组上使用各种的序列操作了,而且这些方法会产生一个数组,而不是ArrayOps类型作为它们的返回值。另外,因为这些隐式转换的结果是短暂的,所以JVM可以使用逸出分析(escape analysis)来完完全全的忽略它。

第二个隐式转换可以将数组转换成为一个真正的Seq类型。这个被称做WrappedArray的类型是一个可变的Vector,并且它实现了所有Vector的方法。

WrappedArray和ArrayOps的区别表现在它们方法的类型上,比如reverse方法:调用WrappedArray的reverse方法会返回一个WrappedArray类型;而调用ArrayOps的reverse方法会返回一个数组类型。从数组类型到WrappedArray类型的转换是可逆的。WrappedArray和ArrayOps都继承实现了ArrayLike这个trait。
 

避免混淆

既然提供了两个隐式转换,那么Scala如何知道在什么时候该用哪个转换呢?在之前的版本中,Scala仅仅凭借参数的类型来做选择,而且还规定这个方法不能够定义在超类中。但是Scala2.8去掉了这些限制,转而引入了一种类似于评分的机制:当Scala在寻找重载或者隐式转换的方法时,每一个符合条件的方法都会得到一分,如果在两个方法分别出现在父类和子类中,那么子类中的方法会再得一分。之后,得分最高的那个方法会成为首选方法。这也就是说,如果多个方法拥有相同的方法签名,那么子类中的方法会最终被调用。

利用这个特性,我们在转换数组的时候就可以让转换为ArrayOps的方法级别优先于转换为WrappedArray的方法。我们将转换为ArrayOps的隐式方法定义在Predef中,将转换为WrappedArray的方法放在了LowPriorityImplicits中,然后让Predef继承于LowPriorityImplicits。这样我们就可以在想要调用序列方法时将String隐式转换为ArrayOps,而只有在我们真正需要转换成序列类时才将其转换为WrappedArray。
 

改进的String

以上同样的做法在Scala2.8中也应用到了String类的转换上。我们发现Scala2.8中取消了RichString类型,转而增加了StringOps和WrappedString类型。它们的转换和用法与之前提到的数组的转换和用法相似,这里就不多说了。
 

泛型数组的创建和Manifest

好吧,我承认不知道该如何翻译manifest,所以在这里直接引用了原文。

刚才我们讨论的都是关于在Scala中如何增强对数组的操作。剩下的也就只有泛型数组这个问题了。我们知道,Scala是允许我们使用new Array[T]来创建泛型数组的。然而,泛型数组在Java中是没有的,那么Scala是如何实现的呢?

实际上,Scala要做的仅仅是需要记录有关类型T的运行时信息。在Scala 2.8中引入了manifest来做这个事情。一个Manifest[T]的对象可以完整的记录关于类型T的运行时信息。Manifest的值是通过隐式参数传入的,编译器知道如何为静态的已知类型T来创建它们。另外,还存在一个稍弱一些的类型——ClassManifest,仅用来记录类方面的信息,而不涉及所有的参数类型。我们在创建泛型数组时使用的正是这个ClassManifest。

来举个例子。假设有一个方法,它能够利用传入的函数f来创建一个数组,而数组类型由函数f的返回值来确定。在2.8之前的版本中,我们的方法可以写成这样:

def tabulate[T](len: Int, f: Int=>T) = {
    val xs = new Array[T](len)
    for(i<-0 until len) xs(i) = f(i)
    xs
}

不幸的是,上面的代码在Scala 2.8中就不行了,因为Scala 2.8规定在创建泛型数组Array[T]是必须得有T的运行时信息。所以对于上面的方法,我们需要将一个ClassManifest[T]类型作为隐式参数传入方法中:

def tabulate[T](len: Int, f: Int=>T)(implicit m: ClassManifest[T]) = {
    val xs = new Array[T](len)
    for(i<-0 until len) xs(i) = f(i)
    xs
}

你可能觉得这么些太长了,我们可以利用上下文绑定来简写上面的方法:

def tabulate[T: ClassManifest](len: Int, f: Int=>T) = {
    val xs = new Array[T](len)
    for(i<-0 until len) xs(i) = f(i)
    xs
}

这样,当我们调用tabulate方法创建一个具体类型——比如Int或者String——的数组时,编译器就会为我们自动创建ClassManifest对象并通过隐式参数出入tabulate方法。当我们在另外一个带类型参数的方法中调用tabulate方法时,唯一需要做的就是利用上下文绑定的方式再次传入ClassManifest类型,比如:

def tabTen[T: ClassManifest](f: Int=>T) = tabulate(10, f)

Scala 2.8去掉了对数组自动装箱而引入manifest的做法,会对现有的代码造成一定的影响。还好这种影响实际上并不大,就想上面的代码那样,只要加入上下文绑定就可以了。
 

GnericeArray类型

有的时候虽然我们需要定义泛型数组,但是却无法提供manifest,Scala 2.8考虑到了这种情况,提供了一个新的类——GenericArray。这个类定义在scala.collection.mutable包中:

class GenericArray[T](length: Int) extends Vector[T] {
    val array: Array[AnyRef] = new Array[AnyRef](length)
    ...
    // all vector operations defined in terms of ‘array’
}

从上面的代码我们可以看到,GenericArray不同于普通的Array类型,它不需要manifest,因为它有着统一的表现形式:所有的元素存储在Array[AnyRef]中,就想Java中的Object数组一样。

不过,增加了GenericArray类型之后,还是会让人很困惑:到底在什么时候使用普通的Array,在什么时候使用GenericArray呢?答案很简单:当我们能够获得manifest的时候就使用普通的Array,毕竟Array类型性能更好,更紧凑,与Java的交互也更好;当我们无法获得或者不需要manifest的时候就可以使用GenericArray了。在当前Scala的集合框架中,只有一个地方使用了GenericArray,那就是Seq类的sortWith方法。当我们调用xs.sortWith(f)方法时,首先会将xs转换为GenericArray,然后将其传入java.util.Arrays的排序方法,最后再将排好序的结果转换为对应的Seq类型。因为转换成数组仅仅是因为排序时需要,所以没有必要记录数组的manifest,因此选择了
GenericArray。
 

结论

新的Scala集合框架解决了一直困扰Scalaer的Array和String的问题,去掉了大量的编译器魔法。这些主要依赖于Scala 2.8中三个新的特性:第一,泛化了重载和隐式转换,允许调整某个隐式转换的优先级高于其它的方法。第二,在泛型中类型擦除时,可以提供manifest来记录运行时信息。第三,上下文绑定为某些时候隐式参数提供了便捷。

以上这些大部分翻译自官方的PDF,有兴趣的人可以去看原文。

 

没有更多推荐了,返回首页