本文由 Yison 發表在 ScalaCool 團隊博客。html
Scala 中的 collection 庫是符合 DRY 設計原則的典範,它包含了大量通用的集合操做 API,由此咱們能夠基於標準庫,輕鬆構建出一個強大的新集合類型。git
本文將介紹「如何實現一個新集合類」,在開始以前,咱們先來了解下 Scala 2.8 版本後的集合結構設計。github
看過 Scala 中的集合(一) 的朋友已經知道,Scala 的集合類系統地區分了可變的和不可變的集合,它們存在於如下三個包中:app
然而,以上全部的集合都繼承了兩個相同的特質 — Traversable
和 Iterable
(後者繼承了前者)。ide
Traversable
是集合類最高級的特性,它具備一個抽象方法:性能
def foreach[U](f: Elem => U)複製代碼
顧名思義,foreach
方法用於遍歷集合類的全部元素,而後進行指定的操做。Iterable
繼承了 Traversable
,也實現了 foreach
方法,繼而全部繼承了 Iterable
的集合類同時也得到了一個 foreach
的基礎版本。測試
不少集合操做都是基於 foreach
實現,所以它的性能很是關鍵。一些 Iterable
子類覆寫了這個方法的實現,從而得到了符合不一樣集合特性的優化。優化
那麼,常見的集合類型(如 Seq
) 是如何實現通用操做的呢(如 map
)?ui
原來,Traversable
除了惟一的抽象方法之外,還包含了大量通用的集合操做方法。this
Scala 文檔對這些操做方法進行了歸類,以下所示:
分類 | 方法 |
---|---|
抽象方法 | foreach |
相加 | ++ |
Map | map / flatMap / collect |
集合轉換 | toArray / toList / toIterable / toSeq / toIndexedSeq / toStream / toSet / toMap |
拷貝 | copyToBuffer / copyToArray |
size 信息 | isEmpty / nonEmpty / size / hasDefiniteSize |
元素檢索 | head / last / headOption / lastOption / find |
子集合檢索 | tail / init / slice / take / drop / takeWhilte / dropWhile / filter / filteNot / withFilter |
拆分 | splitAt / span / partition / groupBy |
元素測試 | exists / forall / count |
摺疊 | foldLeft / foldRight / /: / :\ / reduceLeft / reduceRight |
特殊摺疊 | sum / product / min / max |
字符串轉化 | mkString / addString / stringPrefix |
視圖生成 | view |
由此,一個集合僅需定義 foreach
方法,以上全部其它方法均可以從 Traversable
繼承。
Scala 當前版本的 Iterable
設計略顯尷尬,它實現了 Traversable
,也同時被其它全部集合實現。然而事實上這並非一個好的設計,緣由以下:
Traversable
具備隱式的行爲假設,它在公開的簽名中是不可見的,容易致使 API 出錯Traversable
比 Iterable
性能要差Traversable
的數據類型,無不接受 Iterator
的實現,前者顯得多餘詳情參見 @Alexelcu 的文章 — Why scala.collection.Traversable Is Bad Design
所以,正在進行的 Scala collection redesign 項目也已經拋棄了 Traversable
。
然而,這並不妨礙咱們研究 Iterable
中的通用方法,它們也在 collection-strawman 中被保留,以下所示:
分類 | 方法 |
---|---|
抽象方法 | iterator |
其餘迭代器 | grouped / sliding |
子集合 | takeRight / dropRight |
拉鍊操做 | zip / zipAll |
比對 | sameElements |
幾乎全部的集合操做都由「遍歷器」和「構建器」完成,在瞭解以上內容以後,咱們再來了解下如何構建一個集合類型。在當前的 Scala 中,是利用一個 Builder
類實現的。
package scala.collection.mutable
class Builder[-Elem, +To] {
def +=(elem: Elem): this.type
def result(): To
def clear(): Unit
def mapResult[NewTo](f: To => NewTo): Builder[Elem, NewTo] = ...
}複製代碼
注意類型參數,Elem
表示元素的類型(如 Int
),To
表示集合的類型(如 Array[Int]
)。
此外:
+=
能夠增長元素result
返回一個集合clear
把集合重置爲空狀態mapResult
返回一個 Builder
,擁有新的集合類型咱們來看下Builder
如何結合 foreach
方法,實現常見的 filter
操做:
def filter(p: Elem => Boolean): Repr = {
val b = newBuilder
foreach { elem => if (p(elem)) b += elem }
b.result
}複製代碼
So easy!沒什麼挑戰。
咱們再來考慮下 map
,它與 filter
的差別之一,在於前者能夠返回一個「元素類型不一樣」的集合。如:
scala > List(1, 2, 3).map(_.toString)
res0: List[String] = List(1, 2, 3)複製代碼
這下有難度了,僅憑 Builder
和 foreach
組合,彷佛完成不了這個任務。
因而,咱們決定看下 TraversableLike
中 map
的 Scala 源碼實現:
def map[B, That](f: Elem => B)
(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(this)
this.foreach(x => b += f(x))
b.result
}複製代碼
當前 Scala 集合中,???Like 命名的特質是 ??? 特質的實現。
一個大發現 — 當前版本的 Scala 原來是利用 CanBuildFrom
類型來解決如何集合「類型轉換」的問題。
package scala.collection.generic
trait CanBuildFrom[-From, -Elem, +To] {
// 建立一個新的構造器(builder)
def apply(from: From): Builder[Elem, To]
}複製代碼
這種利用 TypeClass 技術 — 採用隱式轉換來得到擴展的方式,顯得強大且靈活,但在新手看來會比較怵。
經過字面的理解,咱們知曉 — From
表明當前的集合類型,Elem
表明元素類型,To
表明目標集合的類型。
因此咱們能夠如此解讀 CanBuildFrom
:「有這麼一個方法,由給定的 From 類型的集合,使用 Elem 類型,創建 To 類型的集合」。
經過以上的介紹,你們對 Scala 的集合結構設計有了總體的認識,如今開始來實現一個新的集合類。
如下例子來自 Scala 文檔,細節有調整,精簡。
假設咱們須要設計一套新的「密文編碼序列」,由最基本的 A、B、C、D 四個字母組成。定義類型以下:
abstract class Base
case object A extends Base
case object B extends Base
case object C extends Base
case object D extends Base
object Base {
val fromInt: Int => Base = Array(A, B, C, D)
val toInt: Base => Int = Map(A -> 0, B -> 1, C -> 2, D -> 3)
}複製代碼
顯然,咱們可使用 Seq[Base]
來表示一個密文序列,但因爲這個密文可能很長,而且 Base 類型只有 4 種可能,咱們能夠經過「位計算」的方式來開發一種壓縮過的集合,它是 Seq[Base]
的子類。
如下將採用伴生對象的方式來建立
Message
實例,可參考 Builder 建立者模式
import collection.IndexedSeqLike
final class Message private ( val groups: Array[Int], val length: Int) extends IndexedSeq[Base] {
import Message._
def apply(idx: Int): Base = {
if (idx < 0 || length <= idx)
throw new IndexOutOfBoundsException
Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
}
}
object Message {
private val S = 2 // 表示一組所須要的位數
private val N = 32 / S // 一個Int可以放入的組數
private val M = (1 << S) - 1 // 分離組的位掩碼(bitmask)
def fromSeq(buf: Seq[Base]): Message = {
val groups = new Array[Int]((buf.length + N - 1) / N)
for (i <- 0 until buf.length)
groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
new Message(groups, buf.length)
}
def apply(bases: Base*) = fromSeq(bases)
}複製代碼
測試:
val message = Message(A, B, B ,D)
println(message.length) // 4
println(message.last) // D
println(message.take(3)) // Vector(A, B, B)複製代碼
IndexedSeq
的通用集合方法,如 length
、last
take
方法並無得到預期的 Message(A, B, B)
,而是 Vector(A, B, B)
改進一下:
def take(count: Int): Message = Message.fromSeq(super.take(count))複製代碼
take
返回動態類型的問題,可獲得 Message(A, B, B)
的結果take
外還有大量通用方法,覆寫每一個方法的策略不可取 import collection.mutable.{Builder, ArrayBuffer}
import collection.generic.CanBuildFrom複製代碼
在伴生類中從新實現 newBuilder
:
final class Message private (val groups: Array[Int], val length: Int)
extends IndexedSeq[Base] with IndexedSeqLike[Base, Message] {
import Message._
// 在IndexedSeq中必須從新實現newBuilder
override protected[this] def newBuilder: Builder[Base, Message] =
Message.newBuilder
def apply(idx: Int): Base = {
……
}
}複製代碼
改寫伴生對象:
object Message {
……
def fromSeq(buf: Seq[Base]): Message = {
……
}
def apply(bases: Base*) = fromSeq(bases)
def newBuilder: Builder[Base, Message] =
new ArrayBuffer mapResult fromSeq
implicit def canBuildFrom: CanBuildFrom[Message, Base, Message] =
new CanBuildFrom[Message, Base, Message] {
def apply(): Builder[Base, Message] = newBuilder
def apply(from: Message): Builder[Base, Message] = newBuilder
}
}複製代碼
此外,如前文提到,咱們還能夠從新實現 foreach
方法來提升該集合類的效率:
final class Message private (val groups: Array[Int], val length: Int)
extends IndexedSeq[Base] with IndexedSeqLike[Base, Message] {
……
override def foreach[U](f: Base => U): Unit = {
var i = 0
var b = 0
while (i < length) {
b = if (i % N == 0) groups(i / N) else b >>> S
f(Base.fromInt(b & M))
i += 1
}
}
}複製代碼
以上,咱們便構建了一個新的集合類型 Message
,經過極少的代碼,擁有了強大的通用集合特性。
咱們將在下一篇文章中進一步介紹 CanBuildFrom
,幾乎肯定地說,它也會在將來的 Scala 版本中被新的方案替代。