diff --git a/cn/overviews/Thanks.md b/cn/overviews/Thanks.md new file mode 100644 index 0000000000..5d44915df1 --- /dev/null +++ b/cn/overviews/Thanks.md @@ -0,0 +1,47 @@ +--- +layout: guides-Thanks +language: cn +title: 致谢名单 +--- + +2013年10月份起,CSDN CODE开始组织志愿者翻译Scala官方文档。计划翻译的文档主要为Scala官网上overview部分的内容,包含以下部分: + +- The Scala Actors Migration Guide +- Value Classes and Universal Traits +- String Interpolation NEW IN 2.10 +- Implicit Classes AVAILABLE +- Futures and Promises NEW IN 2.10 +- Scala’s Parallel Collections Library +- The Architecture of Scala Collections +- The Scala Actors API +- Scala’s Collections Library + +经过公开征集、筛选,我们最终组织了二十多位志愿者来进行此项翻译工作。我们并邀请到了国内Scala知名社区“Scala研学社”的两位老师**连城**、**尹绪森**来担任顾问和翻译校对的工作。在此向Scala研学社表示衷心的感谢! + +更要特别感谢的是在此次翻译工作中付出辛勤劳动的、广大的翻译志愿者朋友们,他们是: +(以下按姓氏拼音排序) +姓名 CSDN ID +陈骏 [jacty0219](https://code.csdn.net/jacty0219) +陈幸 Meteor2520 +董泉 dqsweet +何乃梧 [yuyi20112011](https://code.csdn.net/yuyi20112011) +黄越勇 aptweasel +赖正兴 laizx +李奕飞 fancylee +林君 a455642158 +刘国锋 [iceongrass](https://code.csdn.net/iceongrass) +吕浩志 lvhaozhi +聂雪珲 blueforgetmenot +潘栋华 +潘义文 Caidaoqq +王金岩 i9901028 +王雨施 +熊杰 [xiaoxiong345064855](https://code.csdn.net/xiaoxiong345064855) +杨志斌 qwewegfd +张冰 usen521 +张明明 [a775901421](https://code.csdn.net/a775901421) +张欣 kevenking@gmail.com +周逸灵 pastgift + +感谢大家的辛勤劳动! +我们已将经过最终校审的Scala文档中文版上传在此文档项目中,欢迎各位阅读、指正。如果您发现翻译稿件中有什么错误或问题,可以在此项目中给我们留言,或者直接派生、修改后提交合并请求给我们。谢谢! diff --git a/cn/overviews/collections/Arrays.md b/cn/overviews/collections/Arrays.md new file mode 100644 index 0000000000..8fa7388933 --- /dev/null +++ b/cn/overviews/collections/Arrays.md @@ -0,0 +1,118 @@ +--- +layout: overview-large +title: 数组 + +disqus: true + +partof: collections +num: 10 +languages: [cn] +--- + +在Scala中,[数组](http://www.scala-lang.org/api/2.10.0/scala/Array.html)是一种特殊的collection。一方面,Scala数组与Java数组是一一对应的。即Scala数组Array[Int]可看作Java的Int[],Array[Double]可看作Java的double[],以及Array[String]可看作Java的String[]。但Scala数组比Java数组提供了更多内容。首先,Scala数组是一种泛型。即可以定义一个Array[T],T可以是一种类型参数或抽象类型。其次,Scala数组与Scala序列是兼容的 - 在需要Seq[T]的地方可由Array[T]代替。最后,Scala数组支持所有的序列操作。这里有个实际的例子: + + scala> val a1 = Array(1, 2, 3) + a1: Array[Int] = Array(1, 2, 3) + scala> val a2 = a1 map (_ * 3) + a2: Array[Int] = Array(3, 6, 9) + scala> val a3 = a2 filter (_ % 2 != 0) + a3: Array[Int] = Array(3, 9) + scala> a3.reverse + res1: Array[Int] = Array(9, 3) + +既然Scala数组表现的如同Java的数组,那么Scala数组这些额外的特性是如何运作的呢?实际上,Scala 2.8与早期版本在这个问题的处理上有所不同。早期版本中执行打包/解包过程时,Scala编译器做了一些“神奇”的包装/解包的操作,进行数组与序列对象之间互转。其中涉及到的细节相当复杂,尤其是创建一个新的泛型类型数组Array[T]时。一些让人迷惑的罕见实例以及数组操作的性能都是不可预测的。 + +Scala 2.8设计要简单得多,其数组实现系统地使用隐式转换,从而基本去除了编译器的特殊处理。Scala 2.8中数组不再看作序列,因为本地数组的类型不是Seq的子类型。而是在数组和 `scala.collection.mutable.WrappedArray`这个类的实例之间隐式转换,后者则是Seq的子类。这里有个例子: + + scala> val seq: Seq[Int] = a1 + seq: Seq[Int] = WrappedArray(1, 2, 3) + scala> val a4: Array[Int] = s.toArray + a4: Array[Int] = Array(1, 2, 3) + scala> a1 eq a4 + res2: Boolean = true + +上面的例子说明数组与序列是兼容的,因为数组可以隐式转换为WrappedArray。反之可以使用Traversable提供的toArray方法将WrappedArray转换为数组。REPL最后一行表明,隐式转换与toArray方法作用相互抵消。 + +数组还有另外一种隐式转换,不需要将数组转换成序列,而是简单地把所有序列的方法“添加”给数组。“添加”其实是将数组封装到一个ArrayOps类型的对象中,后者支持所有序列的方法。ArrayOps对象的生命周期通常很短暂,不调用序列方法的时候基本不会用到,其内存也可以回收。现代虚拟机一般不会创建这个对象。 + +在接下来REPL中展示数组的这两种隐式转换的区别: + + scala> val seq: Seq[Int] = a1 + seq: Seq[Int] = WrappedArray(1, 2, 3) + scala> seq.reverse + res2: Seq[Int] = WrappedArray(3, 2, 1) + scala> val ops: collection.mutable.ArrayOps[Int] = a1 + ops: scala.collection.mutable.ArrayOps[Int] = [I(1, 2, 3) + scala> ops.reverse + res3: Array[Int] = Array(3, 2, 1) + +注意seq是一个WrappedArray,seq调用reverse方法也会得到一个WrappedArray。这是没问题的,因为封装的数组就是Seq,在任意Seq上调用reverse方法都会得到Seq。反之,变量ops属于ArrayOps这个类,对其调用reverse方法得到一个数组,而不是Seq。 + +上例直接使用ArrayOps仅为了展示其与WrappedArray的区别,这种用法非常不自然。一般情况下永远不要实例化一个ArrayOps,而是在数组上调用Seq的方法: + + scala> a1.reverse + res4: Array[Int] = Array(3, 2, 1) + +ArrayOps的对象会通过隐式转换自动的插入,因此上述的代码等价于 + + scala> intArrayOps(a1).reverse + res5: Array[Int] = Array(3, 2, 1) + +这里的intArrayOps就是之前例子中插入的隐式转换。这里引出一个疑问,上面代码中,编译器为何选择了intArrayOps而不是WrappedArray做隐式转换?毕竟,两种转换都是将数组映射到支持reverse方法的类型,并且指定输入。答案是两种转换是有优先级次序的,ArrayOps转换比WrappedArray有更高的优先级。前者定义在Predef对象中,而后者定义在继承自Predef的`scala.LowPritoryImplicits`类中。子类、子对象中隐式转换的优先级低于基类。所以如果两种转换都可用,Predef中的会优先选取。字符串的情况也是如此。 + +数组与序列兼容,并支持所有序列操作的方法,你现在应该已经了然于胸。那泛型呢?在Java中你不可以定义一个以T为类型参数的`T[]`。那么Scala的`Array[T]`是如何做的呢?事实上一个像`Array[T] `的泛型数组在运行时态可任意为Java的八个原始数组类型像`byte[]`, `short[]`, `char[]`, `int[]`, `long[]`, `float[]`, `double[]`, `boolean[]`,甚至它可以是一个对象数组。最常见的运行时态类型是AnyRef ,它包括了所有的这些类型(相当于java.lang.Object),因此这样的类型可以通过Scala编译器映射到`Array[T]`.在运行时,当`Array[T]`类型的数组元素被访问或更新时,就会有一个序列的类型测试用于确定真正的数组类型,随后就是java中的正确的数组操作。这些类型测试会影响数组操作的效率。这意味着如果你需要更大的性能,你应该更喜欢具体而明确的泛型数组。代表通用的泛型数组是不够的,因此,也必然有一种方式去创造泛型数组。这是一个更难的问题,需要一点点的帮助你。为了说明这个问题,考虑下面用一个通用的方法去创造数组的尝试。 + + //这是错的! + def evenElems[T](xs: Vector[T]): Array[T] = { + val arr = new Array[T]((xs.length + 1) / 2) + for (i <- 0 until xs.length by 2) + arr(i / 2) = xs(i) + arr + } + +evenElems方法返回一个新数组,该数组包含了参数向量xs的所有元素甚至在向量中的位置。evenElems 主体的第一行构建了结果数组,将相同元素类型作为参数。所以根据T的实际类型参数,这可能是一个`Array[Int]`,或者是一个`Array[Boolean]`,或者是一个在java中有一些其他基本类型的数组,或者是一个有引用类型的数组。但是这些类型有不同的运行时表达,那么Scala如何在运行时选择正确的呢?事实上,它不是基于信息传递做的,因为与类型参数T相对应的实际类型在运行时已被抹去。这就是为什么你在编译上面的代码时会出现如下的错误信息: + + error: cannot find class manifest for element type T + val arr = new Array[T]((arr.length + 1) / 2) + ^ +这里需要你做的就是通过提供一些运行时的实际元素类型参数的线索来帮助编译器处理。这个运行时的提示采取的形式是一个`scala.reflect.ClassManifest`类型的类声明。一个类声明就是一个类型描述对象,给对象描述了一个类型的顶层类。另外,类声明也有`scala.reflect.Manifest`类型的所有声明,它描述了类型的各个方面。但对于数组创建而言,只需要提供类声明。 + +如果你指示编译器那么做它就会自动的构建类声明。“指示”意味着你决定一个类声明作为隐式参数,像这样: + + def evenElems[T](xs: Vector[T])(implicit m: ClassManifest[T]): Array[T] = ... + +使用一个替换和较短的语法。通过用一个上下文绑定你也可以要求类型与一个类声明一起。这种方式是跟在一个冒号类型和类名为ClassManifest的后面,想这样: + + // this works + def evenElems[T: ClassManifest](xs: Vector[T]): Array[T] = { + val arr = new Array[T]((xs.length + 1) / 2) + for (i <- 0 until xs.length by 2) + arr(i / 2) = xs(i) + arr + } + +这两个evenElems的修订版本意思是完全相同的。当Array[T] 构造时,在任何情况下会发生的是,编译器会寻找类型参数T的一个类声明,这就是说,它会寻找ClassManifest[T]一个隐式类型的值。如果如此的一个值被发现,声明会用来构造正确的数组类型。否则,你就会看到一个错误信息像上面一样。 + +下面是一些使用evenElems 方法的REPL 交互。 + + scala> evenElems(Vector(1, 2, 3, 4, 5)) + res6: Array[Int] = Array(1, 3, 5) + scala> evenElems(Vector("this", "is", "a", "test", "run")) + res7: Array[java.lang.String] = Array(this, a, run) + +在这两种情况下,Scala编译器自动的为元素类型构建一个类声明(首先,Int,然后String)并且通过它传递evenElems 方法的隐式参数。编译器可以对所有的具体类型构造,但如果论点本身是另一个没有类声明的类型参数就不可以。例如,下面的错误: + + scala> def wrap[U](xs: Array[U]) = evenElems(xs) + :6: error: could not find implicit value for + 证明类型ClassManifest[U]的参数 + def wrap[U](xs: Array[U]) = evenElems(xs) + ^ +这里所发生的是,evenElems 需要一个类型参数U的类声明,但是没有发现。这种情况下的解决方案是,当然,是为了U的另一个隐式类声明。所以下面起作用了: + + scala> def wrap[U: ClassManifest](xs: Array[U]) = evenElems(xs) + wrap: [U](xs: Array[U])(implicit evidence$1: ClassManifest[U])Array[U] + +这个实例还显示在定义U的上下文绑定里这仅是一个简短的隐式参数命名为`ClassManifest[U]`类型的`evidence$1`。 + +总结,泛型数组创建需要类声明。所以每当创建一个类型参数T的数组,你还需要提供一个T的隐式类声明。最简单的方法是声明类型参数与ClassManifest的上下文绑定,如 `[T: ClassManifest]`。 + diff --git a/cn/overviews/collections/Concrete_Immutable_Collection_Classes.md b/cn/overviews/collections/Concrete_Immutable_Collection_Classes.md new file mode 100644 index 0000000000..036a7fed5c --- /dev/null +++ b/cn/overviews/collections/Concrete_Immutable_Collection_Classes.md @@ -0,0 +1,188 @@ +--- +layout: overview-large +title: 不可变集实体类 + +disqus: true + +partof: collections +num: 8 +languages: [cn] +--- + + +Scala中提供了多种具体的不可变集类供你选择,这些类(maps, sets, sequences)实现的接口(traits)不同,比如是否能够是无限(infinite)的,各种操作的速度也不一样。下面的篇幅介绍几种Scala中最常用的不可变集类型。 + +## List(列表) + +列表[List](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/List.html)是一种有限的不可变序列式。提供了常数时间的访问列表头元素和列表尾的操作,并且提供了常熟时间的构造新链表的操作,该操作将一个新的元素插入到列表的头部。其他许多操作则和列表的长度成线性关系。 + +List通常被认为是Scala中最重要的数据结构,所以我们在此不必过于赘述。版本2.8中主要的变化是,List类和其子类::以及其子对象Nil都被定义在了其逻辑上所属的scala.collection.immutable包里。scala包中仍然保留了List,Nil和::的别名,所以对于用户来说可以像原来一样访问List。 + +另一个主要的变化是,List现在更加紧密的融入了Collections Framework中,而不是像过去那样更像一个特例。比如说,大量原本存在于与List相关的对象的方法基本上全部都过时(deprecated)了,取而代之的是被每种Collection所继承的统一的构造方法。 + +## Stream(流) + +流[Stream](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/Stream.html)与List很相似,只不过其中的每一个元素都经过了一些简单的计算处理。也正是因为如此,stream结构可以无限长。只有那些被要求的元素才会经过计算处理,除此以外stream结构的性能特性与List基本相同。 + +鉴于List通常使用 `:: `运算符来进行构造,stream使用外观上很相像的`#::`。这里用一个包含整数1,2和3的stream来做一个简单的例子: + + scala> val str = 1 #:: 2 #:: 3 #:: Stream.empty + str: scala.collection.immutable.Stream[Int] = Stream(1, ?) + +该stream的头结点是1,尾是2和3.尾部并没有被打印出来,因为还没有被计算。stream被特别定义为懒惰计算,并且stream的toString方法很谨慎的设计为不去做任何额外的计算。 + +下面给出一个稍复杂些的例子。这里讲一个以两个给定的数字为起始的斐波那契数列转换成stream。斐波那契数列的定义是,序列中的每个元素等于序列中在它之前的两个元素之和。 + + scala> def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b) + fibFrom: (a: Int,b: Int)Stream[Int] + +这个函数看起来比较简单。序列中的第一个元素显然是a,其余部分是以b和位于其后的a+b为开始斐波那契数列。这段程序最大的亮点是在对序列进行计算的时候避免了无限递归。如果函数中使用`::`来替换`#::`,那么之后的每次调用都会产生另一次新的调用,从而导致无限递归。在此例中,由于使用了`#::`,等式右值中的调用在需要求值之前都不会被展开。这里尝试着打印出以1,1开头的斐波那契数列的前几个元素: + + scala> val fibs = fibFrom(1, 1).take(7) + fibs: scala.collection.immutable.Stream[Int] = Stream(1, ?) + scala> fibs.toList + res9: List[Int] = List(1, 1, 2, 3, 5, 8, 13) + Vector(向量) + +对于只需要处理数据结构头结点的算法来说,List非常高效。可是相对于访问、添加和删除List头结点只需要固定时间,访问和修改头结点之后元素所需要的时间则是与List深度线性相关的。 + +向量[Vector](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/Vector.html)是用来解决列表(list)不能高效的随机访问的一种结构。Vector结构能够在“更高效”的固定时间内访问到列表中的任意元素。虽然这个时间会比访问头结点或者访问某数组元素所需的时间长一些,但至少这个时间也是个常量。因此,使用Vector的算法不必仅是小心的处理数据结构的头结点。由于可以快速修改和访问任意位置的元素,所以对Vector结构做写操作很方便。 + +Vector类型的构建和修改与其他的序列结构基本一样。 + + scala> val vec = scala.collection.immutable.Vector.empty + vec: scala.collection.immutable.Vector[Nothing] = Vector() + scala> val vec2 = vec :+ 1 :+ 2 + vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2) + scala> val vec3 = 100 +: vec2 + vec3: scala.collection.immutable.Vector[Int] = Vector(100, 1, 2) + scala> vec3(0) + res1: Int = 100 + +Vector结构通常被表示成具有高分支因子的树(树或者图的分支因子是指数据结构中每个节点的子节点数目)。每一个树节点包含最多32个vector元素或者至多32个子树节点。包含最多32个元素的vector可以表示为一个单一节点,而一个间接引用则可以用来表示一个包含至多`32*32=1024`个元素的vector。从树的根节点经过两跳到达叶节点足够存下有2的15次方个元素的vector结构,经过3跳可以存2的20次方个,4跳2的25次方个,5跳2的30次方个。所以对于一般大小的vector数据结构,一般经过至多5次数组访问就可以访问到指定的元素。这也就是我们之前所提及的随机数据访问时“运行时间的相对高效”。 + +由于Vectors结构是不可变的,所以您不能通过修改vector中元素的方法来返回一个新的vector。尽管如此,您仍可以通过update方法从一个单独的元素中创建出区别于给定数据结构的新vector结构: + + scala> val vec = Vector(1, 2, 3) + vec: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3) + scala> vec updated (2, 4) + res0: scala.collection.immutable.Vector[Int] = Vector(1, 2, 4) + scala> vec + res1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3) + +从上面例子的最后一行我们可以看出,update方法的调用并不会改变vec的原始值。与元素访问类似,vector的update方法的运行时间也是“相对高效的固定时间”。对vector中的某一元素进行update操作可以通过从树的根节点开始拷贝该节点以及每一个指向该节点的节点中的元素来实现。这就意味着一次update操作能够创建1到5个包含至多32个元素或者子树的树节点。当然,这样做会比就地更新一个可变数组败家很多,但比起拷贝整个vector结构还是绿色环保了不少。 + +由于vector在快速随机选择和快速随机更新的性能方面做到很好的平衡,所以它目前正被用作不可变索引序列的默认实现方式。 + + scala> collection.immutable.IndexedSeq(1, 2, 3) + res2: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3) + Immutable stacks(不可变栈) + +如果您想要实现一个后入先出的序列,那您可以使用[Stack](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/Stack.html)。您可以使用push向栈中压入一个元素,用pop从栈中弹出一个元素,用top查看栈顶元素而不用删除它。所有的这些操作都仅仅耗费固定的运行时间。 + +这里提供几个简单的stack操作的例子: + + scala> val stack = scala.collection.immutable.Stack.empty + stack: scala.collection.immutable.Stack[Nothing] = Stack() + scala> val hasOne = stack.push(1) + hasOne: scala.collection.immutable.Stack[Int] = Stack(1) + scala> stack + stack: scala.collection.immutable.Stack[Nothing] = Stack() + scala> hasOne.top + res20: Int = 1 + scala> hasOne.pop + res19: scala.collection.immutable.Stack[Int] = Stack() + +不可变stack一般很少用在Scala编程中,因为List结构已经能够覆盖到它的功能:push操作同List中的::基本相同,pop则对应着tail。 + +## Immutable Queues(不可变队列) + +[Queue](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/Queue.html)是一种与stack很相似的数据结构,除了与stack的后入先出不同,Queue结构的是先入先出的。 + +这里给出一个创建空不可变queue的例子: + + scala> val empty = scala.collection.immutable.Queue[Int]() + empty: scala.collection.immutable.Queue[Int] = Queue() + +您可以使用enqueue方法在不可变Queue中加入一个元素: + + scala> val has1 = empty.enqueue(1) + has1: scala.collection.immutable.Queue[Int] = Queue(1) + +如果想要在queue中添加多个元素需要在调用enqueue方法时用一个collection对象作为参数: + + scala> val has123 = has1.enqueue(List(2, 3)) + has123: scala.collection.immutable.Queue[Int] + = Queue(1, 2, 3) + +如果想要从queue的头部删除一个元素,您可以使用dequeue方法: + + scala> val (element, has23) = has123.dequeue + element: Int = 1 + has23: scala.collection.immutable.Queue[Int] = Queue(2, 3) + +请注意,dequeue方法将会返回两个值,包括被删除掉的元素和queue中剩下的部分。 + +## Ranges (等差数列) + +[Range]表示的是一个有序的等差整数数列。比如说,“1,2,3,”就是一个Range,“5,8,11,14,”也是。在Scala中创建一个Range类,需要用到两个预定义的方法to和by。 + + scala> 1 to 3 + res2: scala.collection.immutable.Range.Inclusive + with scala.collection.immutable.Range.ByOne = Range(1, 2, 3) + scala> 5 to 14 by 3 + res3: scala.collection.immutable.Range = Range(5, 8, 11, 14) + +如果您想创建一个不包含范围上限的Range类,那么用until方法代替to更为方便: + + scala> 1 until 3 + res2: scala.collection.immutable.Range.Inclusive + with scala.collection.immutable.Range.ByOne = Range(1, 2) + +Range类的空间复杂度是恒定的,因为只需要三个数字就可以定义一个Range类:起始、结束和步长值。也正是因为有这样的特性,对Range类多数操作都非常非常的快。 + +## Hash Tries + +Hash try是高效实现不可变集合和关联数组(maps)的标准方法,[immutable.HashMap](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/HashMap.html)类提供了对Hash Try的支持。从表现形式上看,Hash Try和Vector比较相似,都是树结构,且每个节点包含32个元素或32个子树,差别只是用不同的hash code替换了指向各个节点的向量值。举个例子吧:当您要在一个映射表里找一个关键字,首先需要用hash code值替换掉之前的向量值;然后用hash code的最后5个bit找到第一层子树,然后每5个bit找到下一层子树。当存储在一个节点中所有元素的代表他们当前所在层的hash code位都不相同时,查找结束。 + +Hash Try对于快速查找和函数式的高效添加和删除操作上取得了很好的平衡,这也是Scala中不可变映射和集合采用Hash Try作为默认实现方式的原因。事实上,Scala对于大小小于5的不可变集合和映射做了更进一步的优化。只有1到4个元素的集合和映射被在现场会被存储在一个单独仅仅包含这些元素(对于映射则只是包含键值对)的对象中。空集合和空映射则视情况不同作为一个单独的对象,空的一般情况下就会一直空下去,所以也没有必要为他们复制一份拷贝。 + +## Red-Black Trees(红黑树) + +红黑树是一种平衡二叉树,树中一些节点被设计成红节点,其余的作为黑节点。同任何平衡二叉树一样,对红黑树的最长运行时间随树的节点数成对数(logarithmic)增长。 + +Scala隐含的提供了不可变集合和映射的红黑树实现,您可以在[TreeSet](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/TreeSet.html)和[TreeMap](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/TreeMap.html)下使用这些方法。 + + ## scala> scala.collection.immutable.TreeSet.empty[Int] + res11: scala.collection.immutable.TreeSet[Int] = TreeSet() + scala> res11 + 1 + 3 + 3 + res12: scala.collection.immutable.TreeSet[Int] = TreeSet(1, 3) + +红黑树在Scala中被作为SortedSet的标准实现,因为它提供了一个高效的迭代器,可以用来按照拍好的序列返回所有的元素。 + +## Immutable BitSets(不可变位集合) + +[BitSet](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/BitSet.html)代表一个由小整数构成的容器,这些小整数的值表示了一个大整数被置1的各个位。比如说,一个包含3、2和0的bit集合可以用来表示二进制数1101和十进制数13. + +BitSet内部的使用了一个64位long型的数组。数组中的第一个long表示整数0到63,第二个表示64到27,以此类推。所以只要集合中最大的整数在千以内BitSet的压缩率都是相当高的。 + +BitSet操作的运行时间是非常快的。查找测试仅仅需要固定时间。向集合内增加一个项所需时间同BitSet数组中long型的个数成正比,但这也通常是个非常小的值。这里有几个关于BitSet用法的例子: + + scala> val bits = scala.collection.immutable.BitSet.empty + bits: scala.collection.immutable.BitSet = BitSet() + scala> val moreBits = bits + 3 + 4 + 4 + moreBits: scala.collection.immutable.BitSet = BitSet(3, 4) + scala> moreBits(3) + res26: Boolean = true + scala> moreBits(0) + res27: Boolean = false + List Maps + +[ListMap](http://www.scala-lang.org/api/2.10.0/scala/collection/immutable/ListMap.html)被用来表示一个保存键-值映射的链表。一般情况下,ListMap操作都需要遍历整个列表,所以操作的运行时间也同列表长度成线性关系。实际上ListMap在Scala中很少使用,因为标准的不可变映射通常速度会更快。唯一的例外是,在构造映射时由于某种原因,链表中靠前的元素被访问的频率大大高于其他的元素。 + + scala> val map = scala.collection.immutable.ListMap(1->"one", 2->"two") + map: scala.collection.immutable.ListMap[Int,java.lang.String] = + Map(1 -> one, 2 -> two) + scala> map(2) + res30: String = "two" + diff --git a/cn/overviews/collections/Concrete_Mutable_Collection_Classes.md b/cn/overviews/collections/Concrete_Mutable_Collection_Classes.md new file mode 100644 index 0000000000..dee1935250 --- /dev/null +++ b/cn/overviews/collections/Concrete_Mutable_Collection_Classes.md @@ -0,0 +1,170 @@ +--- +layout: overview-large +title: 具体的可变容器类 + +disqus: true + +partof: collections +num: 9 +languages: [cn] +--- + + +目前你已经看过了Scala的不可变容器类,这些是标准库中最常用的。现在来看一下可变容器类。 + +## Array Buffers + +一个[ArrayBuffer](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/ArrayBuffer.html)缓冲包含数组和数组的大小。对数组缓冲的大多数操作,其速度与数组本身无异。因为这些操作直接访问、修改底层数组。另外,数组缓冲可以进行高效的尾插数据。追加操作均摊下来只需常量时间。因此,数组缓冲可以高效的建立一个有大量数据的容器,无论是否总有数据追加到尾部。 + + scala> val buf = scala.collection.mutable.ArrayBuffer.empty[Int] + buf: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer() + scala> buf += 1 + res32: buf.type = ArrayBuffer(1) + scala> buf += 10 + res33: buf.type = ArrayBuffer(1, 10) + scala> buf.toArray + res34: Array[Int] = Array(1, 10) + +## List Buffers + +[ListBuffer](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/ListBuffer.html) 类似于数组缓冲。区别在于前者内部实现是链表, 而非数组。如果你想把构造完的缓冲转换为列表,那就用列表缓冲,别用数组缓冲。 + + scala> val buf = scala.collection.mutable.ListBuffer.empty[Int] + buf: scala.collection.mutable.ListBuffer[Int] = ListBuffer() + scala> buf += 1 + res35: buf.type = ListBuffer(1) + scala> buf += 10 + res36: buf.type = ListBuffer(1, 10) + scala> buf.toList + res37: List[Int] = List(1, 10) + +## StringBuilders + +数组缓冲用来构建数组,列表缓冲用来创建列表。类似地,[StringBuilder](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/StringBuilder.html) 用来构造字符串。作为常用的类,字符串构造器已导入到默认的命名空间。直接用 new StringBuilder就可创建字符串构造器 ,像这样: + + scala> val buf = new StringBuilder + buf: StringBuilder = + scala> buf += 'a' + res38: buf.type = a + scala> buf ++= "bcdef" + res39: buf.type = abcdef + scala> buf.toString + res41: String = abcdef + +## 链表 + +链表是可变序列,它由一个个使用next指针进行链接的节点构成。它们的支持类是[LinkedList](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/LinkedList.html)。在大多数的编程语言中,null可以表示一个空链表,但是在Scalable集合中不是这样。因为就算是空的序列,也必须支持所有的序列方法。尤其是 `LinkedList.empty.isEmpty` 必须返回`true`,而不是抛出一个 `NullPointerException` 。空链表用一种特殊的方式编译: + +它们的 next 字段指向它自身。链表像他们的不可变对象一样,是最佳的顺序遍历序列。此外,链表可以很容易去插入一个元素或链接到另一个链表。 + +## 双向链表 + +双向链表和单向链表相似,只不过它们除了具有 next字段外,还有一个可变字段 prev用来指向当前节点的上一个元素 。这个多出的链接的好处主要在于可以快速的移除元素。双向链表的支持类是[DoubleLinkedList](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/DoubleLinkedList.html). + +## 可变列表 + +[MutableList](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/MutableList.html) 由一个单向链表和一个指向该链表终端空节点的指针构成。因为避免了贯穿整个列表去遍历搜索它的终端节点,这就使得列表压缩了操作所用的时间。MutableList 目前是Scala中[mutable.LinearSeq](http://www.scala-lang.org/api/2.10.0/scala/collection/LinearSeq.html) 的标准实现。 + +## 队列 + +Scala除了提供了不可变队列之外,还提供了可变队列。你可以像使用一个不可变队列一样地使用一个可变队列,但你需要使用+= 和++=操作符进行添加的方式来替代排队方法。 +当然,在一个可变队列中,出队方法将只移除头元素并返回该队列。这里是一个例子: + + scala> val queue = new scala.collection.mutable.Queue[String] + queue: scala.collection.mutable.Queue[String] = Queue() + scala> queue += "a" + res10: queue.type = Queue(a) + scala> queue ++= List("b", "c") + res11: queue.type = Queue(a, b, c) + scala> queue + res12: scala.collection.mutable.Queue[String] = Queue(a, b, c) + scala> queue.dequeue + res13: String = a + scala> queue + res14: scala.collection.mutable.Queue[String] = Queue(b, c) + +## 数组序列 + +Array Sequences 是具有固定大小的可变序列。在它的内部,用一个 `Array[Object]`来存储元素。在Scala 中,[ArraySeq](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/ArraySeq.html) 是它的实现类。 + +如果你想拥有 Array 的性能特点,又想建立一个泛型序列实例,但是你又不知道其元素的类型,在运行阶段也无法提供一个`ClassManifest` ,那么你通常可以使用 `ArraySeq` 。这些问题在[arrays](http://docs.scala-lang.org/overviews/collections/arrays.html)一节中有详细的说明。 + +## 堆栈 + +你已经在前面看过了不可变栈。还有一个可变栈,支持类是[mutable.Stack](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/Stack.html)。它的工作方式与不可变栈相同,只是适当的做了修改。 + + scala> val stack = new scala.collection.mutable.Stack[Int] + stack: scala.collection.mutable.Stack[Int] = Stack() + scala> stack.push(1) + res0: stack.type = Stack(1) + scala> stack + res1: scala.collection.mutable.Stack[Int] = Stack(1) + scala> stack.push(2) + res0: stack.type = Stack(1, 2) + scala> stack + res3: scala.collection.mutable.Stack[Int] = Stack(1, 2) + scala> stack.top + res8: Int = 2 + scala> stack + res9: scala.collection.mutable.Stack[Int] = Stack(1, 2) + scala> stack.pop + res10: Int = 2 + scala> stack + res11: scala.collection.mutable.Stack[Int] = Stack(1) + +## 数组堆栈 + +[ArrayStack](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/ArrayStack.html) 是另一种可变栈的实现,用一个可根据需要改变大小的数组做为支持。它提供了快速索引,使其通常在大多数的操作中会比普通的可变堆栈更高效一点。 + +## 哈希表 + +Hash Table 用一个底层数组来存储元素,每个数据项在数组中的存储位置由这个数据项的Hash Code 来决定。添加一个元素到Hash Table不用花费多少时间,只要数组中不存在与其含有相同Hash Code的另一个元素。因此,只要Hash Table能够根据一种良好的hash codes分配机制来存放对象,Hash Table的速度会非常快。所以在Scala中默认的可变map和set都是基于Hash Table的。你也可以直接用[mutable.HashSet](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/HashSet.html) 和 [mutable.HashMap](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/HashMap.html) 来访问它们。 + +Hash Set 和 Map 的使用和其他的Set和Map是一样的。这里有一些简单的例子: + + scala> val map = scala.collection.mutable.HashMap.empty[Int,String] + map: scala.collection.mutable.HashMap[Int,String] = Map() + scala> map += (1 -> "make a web site") + res42: map.type = Map(1 -> make a web site) + scala> map += (3 -> "profit!") + res43: map.type = Map(1 -> make a web site, 3 -> profit!) + scala> map(1) + res44: String = make a web site + scala> map contains 2 + res46: Boolean = false + +Hash Table的迭代并不是按特定的顺序进行的。它是按任何可能的顺序,依次处理底层数组的数据。为了保证迭代的次序,可以使用一个Linked Hash Map 或 Set 来做为替代。Linked Hash Map 或 Set 像标准的Hash Map 或 Set一样,只不过它包含了一个Linked List,其中的元素按添加的顺序排列。在这种容器中的迭代都是具有相同的顺序,就是按照元素最初被添加的顺序进行迭代。 + +## Weak Hash Maps + +Weak Hash Map 是一种特殊的Hash Map,垃圾回收器会忽略从Map到存储在其内部的Key值的链接。这也就是说,当一个key不再被引用的时候,这个键和对应的值会从map中消失。Weak Hash Map 可以用来处理缓存,比如当一个方法被同一个键值重新调用时,你想重用这个大开销的方法返回值。如果Key值和方法返回值存储在一个常规的Hash Map里,Map会无限制的扩展,Key值也永远不会被垃圾回收器回收。用Weak Hash Map会避免这个问题。一旦有一个Key对象不再被引用,那它的实体会从Weak Hash Map中删除。在Scala中,[WeakHashMap](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/WeakHashMap.html)类是Weak Hash Map的实现类,封装了底层的Java实现类`java.util.WeakHashMap`。 + +## Concurrent Maps + +Concurrent Map可以同时被多个线程访问。除了[Map](http://www.scala-lang.org/api/2.10.0/scala/collection/Map.html)的通用方法,它提供了下列的原子方法: + +### Concurrent Map类中的方法: + +|WHAT IT IS | WHAT IT DOES | +|-----------------------|----------------------| +|m putIfAbsent(k, v) | 添加 键/值 绑定 k -> m ,如果k在m中没有被定义过 | +|m remove (k, v) | 如果当前 k 映射于 v,删除k对应的实体。 | +|m replace (k, old, new) | 如果k先前绑定的是old,则把键k 关联的值替换为new。 | +|m replace (k, v) | 如果k先前绑定的是其他值,则把键k对应的值替换为v | + + +`ConcurrentMap`体现了Scala容器库的特性。目前,它的实现类只有Java的`java.util.concurrent.ConcurrentMap`, 它可以用[standard Java/Scala collection conversions](http://docs.scala-lang.org/overviews/collections/conversions-between-java-and-scala-collections.html)(标准的java/Scala容器转换器)来自动转换成一个Scala map。 + +## Mutable Bitsets + +一个类型为[mutable.BitSet](http://www.scala-lang.org/api/2.10.0/scala/collection/mutable/BitSet.html)的可变bit集合和不可变的bit集合很相似,它只是做了适当的修改。Mutable bit sets在更新的操作上比不可变bit set 效率稍高,因为它不必复制没有发生变化的 Long值。 + + scala> val bits = scala.collection.mutable.BitSet.empty + bits: scala.collection.mutable.BitSet = BitSet() + scala> bits += 1 + res49: bits.type = BitSet(1) + scala> bits += 3 + res50: bits.type = BitSet(1, 3) + scala> bits + res51: scala.collection.mutable.BitSet = BitSet(1, 3) + diff --git a/cn/overviews/collections/Conversions_Between_Java_and_Scala_Collections.md b/cn/overviews/collections/Conversions_Between_Java_and_Scala_Collections.md new file mode 100644 index 0000000000..0f436cd2d6 --- /dev/null +++ b/cn/overviews/collections/Conversions_Between_Java_and_Scala_Collections.md @@ -0,0 +1,58 @@ +--- +layout: overview-large +title: Java和Scala容器的转换 + +disqus: true + +partof: collections +num: 17 +languages: [cn] +--- + + +和Scala一样,Java同样提供了丰富的容器库,Scala和Java容器库有很多相似点,例如,他们都包含迭代器、可迭代结构、集合、 映射和序列。但是他们有一个重要的区别。Scala的容器库特别强调不可变性,因此提供了大量的新方法将一个容器变换成一个新的容器。 + +某些时候,你需要将一种容器类型转换成另外一种类型。例如,你可能想要像访问Scala容器一样访问某个Java容器,或者你可能想将一个Scala容器像Java容器一样传递给某个Java方法。在Scala中,这是很容易的,因为Scala提供了大量的方法来隐式转换所有主要的Java和Scala容器类型。其中提供了如下的双向类型转换: + + Iterator <=> java.util.Iterator + Iterator <=> java.util.Enumeration + Iterable <=> java.lang.Iterable + Iterable <=> java.util.Collection + mutable.Buffer <=> java.util.List + mutable.Set <=> java.util.Set + mutable.Map <=> java.util.Map + mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap + +使用这些转换很简单,只需从JavaConversions对象中import它们即可。 + + scala> import collection.JavaConversions._ + import collection.Java.Conversions._ + +import之后,就可以在Scala容器和与之对应的Java容器之间进行隐式转换了 + + scala> import collection.mutable._ + import collection.mutable._ + scala> val jul: java.util.List[Int] = ArrayBuffer(1, 2, 3) + jul: java.util.List[Int] = [1, 2, 3] + scala> val buf: Seq[Int] = jul + buf: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3) + scala> val m: java.util.Map[String, Int] = HashMap("abc" -> 1, "hello" -> 2) + m: java.util.Map[String, Int] = {hello=2, abc=1} + +在Scala内部,这些转换是通过一系列“包装”对象完成的,这些对象会将相应的方法调用转发至底层的容器对象。所以容器不会在Java和Scala之间拷贝来拷贝去。一个值得注意的特性是,如果你将一个Java容器转换成其对应的Scala容器,然后再将其转换回同样的Java容器,最终得到的是一个和一开始完全相同的容器对象(译注:这里的相同意味着这两个对象实际上是指向同一片内存区域的引用,容器转换过程中没有任何的拷贝发生)。 + +还有一些Scala容器类型可以转换成对应的Java类型,但是并没有将相应的Java类型转换成Scala类型的能力,它们是: + + Seq => java.util.List + mutable.Seq => java.util.List + Set => java.util.Set + Map => java.util.Map + +因为Java并未区分可变容器不可变容器类型,所以,虽然能将`scala.immutable.List`转换成`java.util.List`,但所有的修改操作都会抛出“UnsupportedOperationException”。参见下例: + + scala> jul = List(1, 2, 3) + jul: java.util.List[Int] = [1, 2, 3] + scala> jul.add(7) + java.lang.UnsupportedOperationException + at java.util.AbstractList.add(AbstractList.java:131) + diff --git a/cn/overviews/collections/Creating_Collections_From_Scratch.md b/cn/overviews/collections/Creating_Collections_From_Scratch.md new file mode 100644 index 0000000000..06541f4dd5 --- /dev/null +++ b/cn/overviews/collections/Creating_Collections_From_Scratch.md @@ -0,0 +1,59 @@ +--- +layout: overview-large +title: 从头定义新容器 + +disqus: true + +partof: collections +num: 16 +languages: [cn] +--- + + +我们已经知道`List(1, 2, 3)`可以创建出含有三个元素的列表,用`Map('A' -> 1, 'C' -> 2)`可以创建含有两对绑定的映射。实际上各种Scala容器都支持这一功能。任意容器的名字后面都可以加上一对带参数列表的括号,进而生成一个以这些参数为元素的新容器。不妨再看一些例子: + + Traversable() // 一个空的Traversable对象 + List() // 空列表 + List(1.0, 2.0) // 一个以1.0、2.0为元素的列表 + Vector(1.0, 2.0) // 一个以1.0、2.0为元素的Vector + Iterator(1, 2, 3) // 一个迭代器,可返回三个整数 + Set(dog, cat, bird) // 一个包含三个动物的集合 + HashSet(dog, cat, bird) // 一个包含三个同样动物的HashSet + Map('a' -> 7, 'b' -> 0) // 一个将字符映射到整数的Map + +实际上,上述每个例子都被“暗地里”转换成了对某个对象的apply方法的调用。例如,上述第三行会展开成如下形式: + + List.apply(1.0, 2.0) + +可见,这里调用的是List类的伴生对象的apply方法。该方法可以接受任意多个参数,并将这些参数作为元素,生成一个新的列表。在Scala标准库中,无论是List、Stream、Vector等具体的实现类还是Seq、Set、Traversable等抽象基类,每个容器类都伴一个带apply方法的伴生对象。针对后者,调用apply方法将得到对应抽象基类的某个默认实现,例如: + + scala > List(1,2,3) + res17: List[Int] = List(1, 2, 3) + scala> Traversable(1, 2, 3) + res18: Traversable[Int] = List(1, 2, 3) + scala> mutable.Traversable(1, 2, 3) + res19: scala.collection.mutable.Traversable[Int] = ArrayBuffer(1, 2, 3) + +除了apply方法,每个容器类的伴生对象还定义了一个名为empty的成员方法,该方法返回一个空容器。也就是说,`List.empty`可以代替`List()`,`Map.empty`可以代替`Map()`,等等。 + +Seq的子类同样在它们伴生对象中提供了工厂方法,总结如下表。简而言之,有这么一些: + +concat,将任意多个Traversable容器串联起来 +fill 和 tabulate,用于生成一维或者多维序列,并用给定的初值或打表函数来初始化。 +range,用于生成步长为step的整型序列,并且iterate,将某个函数反复应用于某个初始元素,从而产生一个序列。 + +## 序列的工厂方法 + +| WHAT IT IS | WHAT IT DOES | +|-------------------|---------------------| +| S.emtpy | 空序列 | +| S(x, y, z) | 一个包含x、y、z的序列 | +| S.concat(xs, ys, zs) | 将xs、ys、zs串街起来形成一个新序列。 | +| S.fill(n) {e} | 以表达式e的结果为初值生成一个长度为n的序列。 | +| S.fill(m, n){e} | 以表达式e的结果为初值生成一个维度为m x n的序列(还有更高维度的版本) | +| S.tabulate(n) {f} | 生成一个厂素为n、第i个元素为f(i)的序列。 | +| S.tabulate(m, n){f} | 生成一个维度为m x n,第(i, j)个元素为f(i, j)的序列(还有更高维度的版本)。 | +| S.range(start, end) | start, start + 1, ... end-1的序列。(译注:注意始左闭右开区间) | +| S.range(start, end, step) | 生成以start为起始元素、step为步长、最大值不超过end的递增序列(左闭右开)。 | +| S.iterate(x, n)(f) | 生成一个长度为n的序列,其元素值分别为x、f(x)、f(f(x))、…… | + diff --git a/cn/overviews/collections/Equality.md b/cn/overviews/collections/Equality.md new file mode 100644 index 0000000000..13e15fe21c --- /dev/null +++ b/cn/overviews/collections/Equality.md @@ -0,0 +1,32 @@ +--- +layout: overview-large +title: 等价性 + +disqus: true + +partof: collections +num: 13 +languages: [cn] +--- + + +容器库有标准的等价性和散列法。首先,这个想法是为了将容器划分为集合,序列。不同范畴的容器总是不相等的。例如,即使包含相同的元素,`Set(1, 2, 3)` 与 `List(1, 2, 3)` 不等价。另一方面,在同一范畴下的容器是相等的,当且仅当它们具有相同的元素(对于序列:元素要相同,顺序要相同)。例如`List(1, 2, 3) == Vector(1, 2, 3)`, `HashSet(1, 2) == TreeSet(2, 1)`。 + +一个容器可变与否对等价性校验没有任何影响。对于一个可变容器,在执行等价性测试的同时,你可以简单地思考下它的当前元素。意思是,一个可变容器可能在不同时间等价于不同容器,这是由增加或移除了哪些元素所决定的。当你使用可变容器作为一个hashmap的键时,这将是一个潜在的陷阱。例如: + + scala> import collection.mutable.{HashMap, ArrayBuffer} + import collection.mutable.{HashMap, ArrayBuffer} + scala> val buf = ArrayBuffer(1, 2, 3) + buf: scala.collection.mutable.ArrayBuffer[Int] = + ArrayBuffer(1, 2, 3) + scala> val map = HashMap(buf -> 3) + map: scala.collection.mutable.HashMap[scala.collection。 + mutable.ArrayBuffer[Int],Int] = Map((ArrayBuffer(1, 2, 3),3)) + scala> map(buf) + res13: Int = 3 + scala> buf(0) += 1 + scala> map(buf) + java.util.NoSuchElementException: key not found: + ArrayBuffer(2, 2, 3) + +在这个例子中,由于从第二行到最后一行的数组散列码xs已经发生了改变,最后一行的选择操作将很有可能失败。因此,基于散列码的查找函数将会查找另一个位置,而不是xs所存储的位置。 diff --git a/cn/overviews/collections/Introduction.md b/cn/overviews/collections/Introduction.md new file mode 100644 index 0000000000..b9ebf3b6f4 --- /dev/null +++ b/cn/overviews/collections/Introduction.md @@ -0,0 +1,37 @@ +--- +layout: overview-large +title: 简介 + +disqus: true + +partof: collections +num: 1 +languages: [cn] +--- + +## Martin Odersky和Lex Spoon + +在许多人看来,新的集合框架是Scala 2.8中最显著的改进。此前Scala也有集合(实际上新框架大部分地兼容了旧框架),但2.8中的集合类在通用性、一致性和功能的丰富性上更胜一筹。 + +即使粗看上去集合新增的内容比较微妙,但这些改动却足以对开发者的编程风格造成深远的影响。实际上,就好像你从事一个高层次的程序,而此程序的基本的构建块的元素被整个集合代替。适应这种新的编程风格需要一个过程。幸运的是,新的Scala集合得益于几个新的几个漂亮的属性,从而它们易于使用、简洁、安全、快速、通用。 + +- **易用性**:由20-50个方法的小词汇量,足以在几个操作内解决大部分的集合问题。没有必要被复杂的循环或递归所困扰。持久化的集合类和无副作用的操作意味着你不必担心新数据会意外的破坏已经存在的集合。迭代器和集合更新之间的干扰会被消除! + +- **简洁**:你可以通过单独的一个词来执行一个或多个循环。你也可以用轻量级的语法和组合轻松地快速表达功能性的操作,以致结果看起来像一个自定义的代数。 + +- **安全**:这一问题必须被熟练的理解,Scala集合的静态类型和函数性质意味着你在编译的时候就可以捕获绝大多数错误。原因是(1)、集合操作本身被大量的使用,是测试良好的。(2)、集合的用法要求输入和输出要显式作为函数参数和结果。(3)这些显式的输入输出会受到静态类型检查。最终,绝大部分的误用将会显示为类型错误。这是很少见的的有数百行的程序的首次运行。 + +- **快速**:集合操作已经在类库里是调整和优化过。因此,使用集合通常是比较高效的。你也许能够通过手动调整数据结构和操作来做的好一点,但是你也可能会由于一些次优的实现而做的更糟。不仅如此,集合类最近已经能支持在多核处理器上并行运算。并行集合类支持有序集合的相同操作,因此没有新的操作需要学习也没有代码需要重写。你可以简单地通过调用标准方法来把有序集合优化为一个并行集合。 + +- **通用**:集合类提供了相同的操作在一些类型上,确实如此。所以,你可以用相当少的词汇完成大量的工作。例如,一个字符串从概念上讲就是一个字符序列。因此,在Scala集合类中,字符串支持所有的序列操作。同样的,数组也是支持的。 + +例子:这有一行代码演示了Scala集合类的先进性。 + +`val (minors, adults) = people partition (_.age < 18)` + +这个操作是清晰的:通过他们的age(年龄)把这个集合people拆分到到miors(未成年人)和adults(成年人)中。由于这个拆分方法是被定义在根集合类型TraversableLike类中,这部分代码服务于任何类型的集合,包括数组。例子运行的结果就是miors和adults集合与people集合的类型相同。 + +这个代码比使用传统的类运行一到三个循环更加简明(三个循环处理一个数组,是由于中间结果需要有其它地方做缓存)。一旦你已经学习了基本的集合词汇,你将也发现写这种代码显式的循环更简单和更安全。而且,这个拆分操作是非常快速,并且在多核处理器上采用并行集合类达到更快的速度(并行集合类已经Scala 2.9的一部分发布)。 + +本文档从一个用户的角度出发,提供了一个关于Scala集合类的 API的深入讨论。它将带你体验它定义的所有的基础类和方法。 + diff --git a/cn/overviews/collections/Iterators.md b/cn/overviews/collections/Iterators.md new file mode 100644 index 0000000000..0d1f6eb2a3 --- /dev/null +++ b/cn/overviews/collections/Iterators.md @@ -0,0 +1,175 @@ +--- +layout: overview-large +title: Iterators + +disqus: true + +partof: collections +num: 15 +languages: [cn] +--- + +迭代器不是一个容器,更确切的说是逐一访问容器内元素的方法。迭代器it的两个基本操作是next和hasNext。调用it.next()会返回迭代器的下一个元素,并且更新迭代器的状态。在同一个迭代器上再次调用next,会产生一个新元素来覆盖之前返回的元素。如果没有元素可返回,调用next方法会抛出一个NoSuchElementException异常。你可以调用[迭代器]的hasNext方法来查询容器中是否有下一个元素可供返回。 + +让迭代器it逐个返回所有元素最简单的方法是使用while循环: + + while (it.hasNext) + println(it.next()) + +Scala为Traversable, Iterable和Seq类中的迭代器提供了许多类似的方法。比如:这些类提供了foreach方法以便在迭代器返回的每个元素上执行指定的程序。使用foreach方法可以将上面的循环缩写为: + + it foreach println + +与往常一样,for表达式可以作为foreach、map、withFilter和flatMap表达式的替代语法,所以另一种打印出迭代器返回的所有元素的方式会是这样: + + for (elem <- it) println(elem) + +在迭代器或traversable容器中调用foreach方法的最大区别是:当在迭代器中完成调用foreach方法后会将迭代器保留在最后一个元素的位置。所以在这个迭代器上再次调用next方法时会抛出NoSuchElementException异常。与此不同的是,当在容器中调用foreach方法后,容器中的元素数量不会变化(除非被传递进来的函数删除了元素,但不赞成这样做,因为这会导致意想不到的结果)。 + +迭代器的其他操作跟Traversable一样具有相同的特性。例如:迭代器提供了map方法,该方法会返回一个新的迭代器: + + scala> val it = Iterator("a", "number", "of", "words") + it: Iterator[java.lang.String] = non-empty iterator + scala> it.map(_.length) + res1: Iterator[Int] = non-empty iterator + scala> res1 foreach println + 1 + 6 + 2 + 5 + scala> it.next() + java.util.NoSuchElementException: next on empty iterator + +如你所见,在调用了it.map方法后,迭代器it移动到了最后一个元素的位置。 + +另一个例子是关于dropWhile方法,它用来在迭代器中找到第一个具有某些属性的元素。比如:在上文所说的迭代器中找到第一个具有两个以上字符的单词,你可以这样写: + + scala> val it = Iterator("a", "number", "of", "words") + it: Iterator[java.lang.String] = non-empty iterator + scala> it dropWhile (_.length < 2) + res4: Iterator[java.lang.String] = non-empty iterator + scala> it.next() + res5: java.lang.String = number + +再次注意it在调用dropWhile方法后发生的变化:现在it指向了list中的第二个单词"number"。实际上,it和dropWhile返回的结果res4将会返回相同的元素序列。 + +只有一个标准操作允许重用同一个迭代器: + + val (it1, it2) = it.duplicate + +这个操作返回两个迭代器,每个都相当于迭代器it的完全拷贝。这两个iterator相互独立;一个发生变化不会影响到另外一个。相比之下,原来的迭代器it则被指定到元素的末端而无法再次使用。 + +总的来说,如果调用完迭代器的方法后就不再访问它,那么迭代器的行为方式与容器是比较相像的。Scala容器库中的抽象类TraversableOnce使这一特质更加明显,它是 Traversable 和 Iterator 的公共父类。顾名思义,TraversableOnce 对象可以用foreach来遍历,但是没有指定该对象遍历之后的状态。如果TraversableOnce对象是一个迭代器,它遍历之后会位于最后一个元素,但如果是Traversable则不会发生变化。TraversableOnce的一个通常用法是作为一个方法的参数类型,传递的参数既可以是迭代器,也可以是traversable。Traversable类中的追加方法++就是一个例子。它有一个TraversableOnce 类型的参数,所以你要追加的元素既可以来自于迭代器也可以来自于traversable容器。 + +下面汇总了迭代器的所有操作。 + +## Iterator类的操作 + +| WHAT IT IS | WHAT IT DOES | +|--------------|---------------| +| 抽象方法: | | +| it.next() | 返回迭代器中的下一个元素,并将位置移动至该元素之后。 | +| it.hasNext | 如果还有可返回的元素,返回true。 | +| 变量: | | +| it.buffered | 被缓存的迭代器返回it的所有元素。 | +| it grouped size | 迭代器会生成由it返回元素组成的定长序列块。 | +| xs sliding size | 迭代器会生成由it返回元素组成的定长滑动窗口序列。 | +| 复制: | | +| it.duplicate | 会生成两个能分别返回it所有元素的迭代器。 | +| 加法: | | +| it ++ jt | 迭代器会返回迭代器it的所有元素,并且后面会附加迭代器jt的所有元素。 | +| it padTo (len, x) | 首先返回it的所有元素,追加拷贝x直到长度达到len。 | +| Maps: | | +| it map f | 将it中的每个元素传入函数f后的结果生成新的迭代器。 | +| it flatMap f | 针对it指向的序列中的每个元素应用函数f,并返回指向结果序列的迭代器。 | +| it collect f | 针对it所指向的序列中的每一个在偏函数f上有定义的元素应用f,并返回指向结果序列的迭代器。 | +| 转换(Conversions): | | +| it.toArray | 将it指向的所有元素归入数组并返回。 | +| it.toList | 把it指向的所有元素归入列表并返回 | +| it.toIterable | 把it指向的所有元素归入一个Iterable容器并返回。 | +| it.toSeq | 将it指向的所有元素归入一个Seq容器并返回。 | +| it.toIndexedSeq | 将it指向的所有元素归入一个IndexedSeq容器并返回。 | +| it.toStream | 将it指向的所有元素归入一个Stream容器并返回。 | +| it.toSet | 将it指向的所有元素归入一个Set并返回。 | +| it.toMap | 将it指向的所有键值对归入一个Map并返回。 | +| 拷贝: | | +| it copyToBuffer buf | 将it指向的所有元素拷贝至缓冲区buf。| +| it copyToArray(arr, s, n) | 将it指向的从第s个元素开始的n个元素拷贝到数组arr,其中后两个参数是可选的。 | +| 尺寸信息: | | +| it.isEmpty | 检查it是否为空(与hasNext相反)。 | +| it.nonEmpty | 检查容器中是否包含元素(相当于 hasNext)。 | +| it.size | it可返回的元素数量。注意:这个操作会将it置于终点! | +| it.length | 与it.size相同。 | +| it.hasDefiniteSize | 如果it指向的元素个数有限则返回true(缺省等同于isEmpty) | +| 按下标检索元素: | | +| it find p | 返回第一个满足p的元素或None。注意:如果找到满足条件的元素,迭代器会被置于该元素之后;如果没有找到,会被置于终点。 | +| it indexOf x | 返回it指向的元素中index等于x的第一个元素。注意:迭代器会越过这个元素。 | +| it indexWhere p | 返回it指向的元素中下标满足条件p的元素。注意:迭代器会越过这个元素。 | +| 子迭代器: | | +| it take n | 返回一个包含it指向的前n个元素的新迭代器。注意:it的位置会步进至第n个元素之后,如果it指向的元素数不足n个,迭代器将指向终点。 | +| it drop n | 返回一个指向it所指位置之后第n+1个元素的新迭代器。注意:it将步进至相同位置。 | +| it slice (m,n) | 返回一个新的迭代器,指向it所指向的序列中从开始于第m个元素、结束于第n个元素的片段。 | +| it takeWhile p | 返回一个迭代器,指代从it开始到第一个不满足条件p的元素为止的片段。 | +| it dropWhile p | 返回一个新的迭代器,指向it所指元素中第一个不满足条件p的元素开始直至终点的所有元素。 | +| it filter p | 返回一个新迭代器 ,指向it所指元素中所有满足条件p的元素。 | +| it withFilter p | 同it filter p 一样,用于for表达式。 | +| it filterNot p | 返回一个迭代器,指向it所指元素中不满足条件p的元素。 | +| 拆分(Subdivision): | | +| it partition p | 将it分为两个迭代器;一个指向it所指元素中满足条件谓词p的元素,另一个指向不满足条件谓词p的元素。 | +| 条件元素(Element Conditions): | | +| it forall p | 返回一个布尔值,指明it所指元素是否都满足p。 | +| it exists p | 返回一个布尔值,指明it所指元素中是否存在满足p的元素。 | +| it count p | 返回it所指元素中满足条件谓词p的元素总数。 | +| 折叠(Fold): | | +| (z /: it)(op) | 自左向右在it所指元素的相邻元素间应用二元操作op,初始值为z。| +| (it :\ z)(op) | 自右向左在it所指元素的相邻元素间应用二元操作op,初始值为z。 | +| it.foldLeft(z)(op) | 与(z /: it)(op)相同。 | +| it.foldRight(z)(op) | 与(it :\ z)(op)相同。 | +| it reduceLeft op | 自左向右对非空迭代器it所指元素的相邻元素间应用二元操作op。 | +| it reduceRight op | 自右向左对非空迭代器it所指元素的相邻元素间应用二元操作op。 | +| 特殊折叠(Specific Fold): | | +| it.sum | 返回迭代器it所指数值型元素的和。 | +| it.product | 返回迭代器it所指数值型元素的积。 | +| it.min | 返回迭代器it所指元素中最小的元素。 | +| it.max | 返回迭代器it所指元素中最大的元素。 | +| 拉链方法(Zippers): | | +| it zip jt | 返回一个新迭代器,指向分别由it和jt所指元素一一对应而成的二元组序列。 | +| it zipAll (jt, x, y) | 返回一个新迭代器,指向分别由it和jt所指元素一一对应而成的二元组序列,长度较短的迭代器会被追加元素x或y,以匹配较长的迭代器。 | +| it.zipWithIndex | 返回一个迭代器,指向由it中的元素及其下标共同构成的二元组序列。 | +| 更新: | | +| it patch (i, jt, r) | 由it返回一个新迭代器,其中自第i个元素开始的r个元素被迭代器jt所指元素替换。 | +| 比对: | | +| it sameElements jt | 判断迭代器it和jt是否依次返回相同元素注意:it和jt中至少有一个会步进到终点。 | +|字符串(String): | | +| it addString (b, start, sep, end) | 添加一个字符串到StringBuilder b,该字符串以start为前缀、以end为后缀,中间是以sep分隔的it所指向的所有元素。start、end和sep都是可选项。 | +| it mkString (start, sep, end) | 将it所指所有元素转换成以start为前缀、end为后缀、按sep分隔的字符串。start、sep、end都是可选项。 | + +## 带缓冲的迭代器 + +有时候你可能需要一个支持“预览”功能的迭代器,这样我们既可以看到下一个待返回的元素,又不会令迭代器跨过这个元素。比如有这样一个任务,把迭代器所指元素中的非空元素转化成字符串。你可能会这样写: + + def skipEmptyWordsNOT(it: Iterator[String]) = + while (it.next().isEmpty) {} + +但仔细看看这段代码,就会发现明显的错误:代码确实会跳过空字符串,但同时它也跳过了第一个非空字符串! + +要解决这个问题,可以使用带缓冲能力的迭代器。[BufferedIterator]类是[Iterator]的子类,提供了一个附加的方法,head。在BufferedIterator中调用head 会返回它指向的第一个元素,但是不会令迭代器步进。使用BufferedIterator,跳过空字符串的方法可以写成下面这样: + + def skipEmptyWords(it: BufferedIterator[String]) = + while (it.head.isEmpty) { it.next() } + +通过调用buffered方法,所有迭代器都可以转换成BufferedIterator。参见下例: + + scala> val it = Iterator(1, 2, 3, 4) + it: Iterator[Int] = non-empty iterator + scala> val bit = it.buffered + bit: java.lang.Object with scala.collection. + BufferedIterator[Int] = non-empty iterator + scala> bit.head + res10: Int = 1 + scala> bit.next() + res11: Int = 1 + scala> bit.next() + res11: Int = 2 + +注意,调用`BufferedIterator bit`的head方法不会令它步进。因此接下来的`bit.next()`返回的元素跟`bit.head`相同。 diff --git a/cn/overviews/collections/Maps.md b/cn/overviews/collections/Maps.md new file mode 100644 index 0000000000..6f3e116729 --- /dev/null +++ b/cn/overviews/collections/Maps.md @@ -0,0 +1,168 @@ +--- +layout: overview-large +title: 映射 + +disqus: true + +partof: collections +num: 7 +languages: [cn] +--- + + +映射(Map)是一种可迭代的键值对结构(也称映射或关联)。Scala的Predef类提供了隐式转换,允许使用另一种语法:`key -> value`,来代替`(key, value)`。如:`Map("x" -> 24, "y" -> 25, "z" -> 26)`等同于`Map(("x", 24), ("y", 25), ("z", 26))`,却更易于阅读。 + +映射(Map)的基本操作与集合(Set)类似。下面的表格分类总结了这些操作: + +- **查询类操作:**apply、get、getOrElse、contains和DefinedAt。它们都是根据主键获取对应的值映射操作。例如:def get(key): Option[Value]。“m get key” 返回m中是否用包含了key值。如果包含了,则返回对应value的Some类型值。否则,返回None。这些映射中也包括了apply方法,该方法直接返回主键对应的值。apply方法不会对值进行Option封装。如果该主键不存在,则会抛出异常。 +- **添加及更新类操作:**+、++、updated,这些映射操作允许你添加一个新的绑定或更改现有的绑定。 +- **删除类操作:**-、--,从一个映射(Map)中移除一个绑定。 +- **子集类操作:**keys、keySet、keysIterator、values、valuesIterator,可以以不同形式返回映射的键和值。 +- **filterKeys、mapValues等**变换用于对现有映射中的绑定进行过滤和变换,进而生成新的映射。 + +## Map类的操作 + +| WHAT IT IS | WHAT IT DOES | +|---------------|---------------------| +| **查询:** | | +| ms get k | 返回一个Option,其中包含和键k关联的值。若k不存在,则返回None。 | +| ms(k) | (完整写法是ms apply k)返回和键k关联的值。若k不存在,则抛出异常。 | +| ms getOrElse (k, d) | 返回和键k关联的值。若k不存在,则返回默认值d。 | +| ms contains k | 检查ms是否包含与键k相关联的映射。 | +| ms isDefinedAt k | 同contains。 | +| **添加及更新:** | | +| ms + (k -> v) | 返回一个同时包含ms中所有键值对及从k到v的键值对k -> v的新映射。 | +| ms + (k -> v, l -> w) | 返回一个同时包含ms中所有键值对及所有给定的键值对的新映射。 | +| ms ++ kvs | 返回一个同时包含ms中所有键值对及kvs中的所有键值对的新映射。 | +| ms updated (k, v) | 同ms + (k -> v)。 | +| **移除:** | | +| ms - k | 返回一个包含ms中除键k以外的所有映射关系的映射。 | +| ms - (k, 1, m) | 返回一个滤除了ms中与所有给定的键相关联的映射关系的新映射。 | +| ms -- ks | 返回一个滤除了ms中与ks中给出的键相关联的映射关系的新映射。 | +| **子容器(Subcollection):** | | +| ms.keys | 返回一个用于包含ms中所有键的iterable对象(译注:请注意iterable对象与iterator的区别) | +| ms.keySet | 返回一个包含ms中所有的键的集合。 | +| ms.keyIterator | 返回一个用于遍历ms中所有键的迭代器。 | +| ms.values | 返回一个包含ms中所有值的iterable对象。 | +| ms.valuesIterator | 返回一个用于遍历ms中所有值的迭代器。 | +| **变换:** | | +| ms filterKeys p | 一个映射视图(Map View),其包含一些ms中的映射,且这些映射的键满足条件p。用条件谓词p过滤ms中所有的键,返回一个仅包含与过滤出的键值对的映射视图(view)。| +|ms mapValues f | 用f将ms中每一个键值对的值转换成一个新的值,进而返回一个包含所有新键值对的映射视图(view)。| + + +可变映射(Map)还支持下表中列出的操作。 + +## mutable.Map类中的操作 + +| WHAT IT IS | WHAT IT DOES | +|-------------------------|-------------------------| +| **添加及更新** | | +| ms(k) = v | (完整形式为ms.update(x, v))。向映射ms中新增一个以k为键、以v为值的映射关系,ms先前包含的以k为值的映射关系将被覆盖。 | +| ms += (k -> v) | 向映射ms增加一个以k为键、以v为值的映射关系,并返回ms自身。 | +| ms += (k -> v, l -> w) | 向映射ms中增加给定的多个映射关系,并返回ms自身。 | +| ms ++= kvs | 向映射ms增加kvs中的所有映射关系,并返回ms自身。 | +| ms put (k, v) | 向映射ms增加一个以k为键、以v为值的映射,并返回一个Option,其中可能包含此前与k相关联的值。 | +| ms getOrElseUpdate (k, d) | 如果ms中存在键k,则返回键k的值。否则向ms中新增映射关系k -> v并返回d。 | +| **移除:** | | +| ms -= k | 从映射ms中删除以k为键的映射关系,并返回ms自身。 | +| ms -= (k, l, m) | 从映射ms中删除与给定的各个键相关联的映射关系,并返回ms自身。 | +| ms --= ks | 从映射ms中删除与ks给定的各个键相关联的映射关系,并返回ms自身。 | +| ms remove k | 从ms中移除以k为键的映射关系,并返回一个Option,其可能包含之前与k相关联的值。 | +| ms retain p | 仅保留ms中键满足条件谓词p的映射关系。 | +| ms.clear() | 删除ms中的所有映射关系 | +| **变换:** | | +| ms transform f | 以函数f转换ms中所有键值对(译注:原文比较含糊,transform中参数f的类型是(A, B) => B,即对ms中的所有键值对调用f,得到一个新的值,并用该值替换原键值对中的值)。 | +| **克隆:** | | +| ms.clone | 返回一个新的可变映射(Map),其中包含与ms相同的映射关系。 | + +映射(Map)的添加和删除操作与集合(Set)的相关操作相同。同集合(Set)操作一样,可变映射(mutable maps)也支持非破坏性(non-destructive)修改操作+、-、和updated。但是这些操作涉及到可变映射的复制,因此较少被使用。而利用两种变形`m(key) = value和m += (key -> value)`, 我们可以“原地”修改可变映射m。此外,存还有一种变形`m put (key, value)`,该调用返回一个Option值,其中包含此前与键相关联的值,如果不存在这样的值,则返回None。 + +getOrElseUpdate特别适合用于访问用作缓存的映射(Map)。假设调用函数f开销巨大: + + scala> def f(x: String) = { + println("taking my time."); sleep(100) + x.reverse } + f: (x: String)String + +此外,再假设f没有副作用,即反复以相同参数调用f,得到的结果都相同。那么,我们就可以将之前的调用参数和计算结果保存在一个映射(Map)内,今后仅在映射中查不到对应参数的情况下实际调用f,这样就可以节约时间。这个映射便可以认为是函数f的缓存。 + + val cache = collection.mutable.Map[String, String]() + cache: scala.collection.mutable.Map[String,String] = Map() + +现在,我们可以写出一个更高效的带缓存的函数f: + + scala> def cachedF(s: String) = cache.getOrElseUpdate(s, f(s)) + cachedF: (s: String)String + scala> cachedF("abc") + +稍等片刻。 + + res3: String = cba + scala> cachedF("abc") + res4: String = cba + +注意,getOrElseUpdate的第2个参数是“按名称(by-name)"传递的,所以,仅当在缓存映射中找不到第1个参数,而getOrElseUpdate需要其第2个参数的值时,上述的f("abc")才会被执行。当然我们也可以利用Map的基本操作直接实现cachedF,但那样写就要冗长很多了。 + + def cachedF(arg: String) = cache get arg match { + case Some(result) => result + case None => + val result = f(x) + cache(arg) = result + result + } + +## 同步集合(Set)和映射(Map) + +无论什么样的Map实现,只需混入`SychronizedMap trait`,就可以得到对应的线程安全版的Map。例如,我们可以像下述代码那样在HashMap中混入SynchronizedMap。这个示例一上来先从`scala.colletion.mutable`包中import了两个trait:Map、SynchronizedMap,和一个类:HashMap。接下来,示例中定义了一个单例对象MapMaker,其中定义了一个方法makeMap。该方法的返回值类型是一个同时以String为键值类型的可变映射。 + + import scala.collection.mutable.{Map, + SynchronizedMap, HashMap} + object MapMaker { + def makeMap: Map[String, String] = { + new HashMap[String, String] with + SynchronizedMap[String, String] { + override def default(key: String) = + "Why do you want to know?" + } + } + } + +混入SynchronizedMap trait + +makeMap方法中的第1个语句构造了一个新的混入了SynchronizedMap trait的可变映射: + + new HashMap[String, String] with + SynchronizedMap[String, String] + +针对这段代码,Scala编译器会合成HashMap的一个混入了SynchronizedMap trait的子类,同时生成(并返回)该合成子类的一个实例。处于下面这段代码的缘故,这个合成类还覆写了default方法: + + override def default(key: String) = + "Why do you want to know?" + +当向某个Map查询给定的键所对应的值,而Map中不存在与该键相关联的值时,默认情况下会触发一个NoSuchElementException异常。不过,如果自定义一个Map类并覆写default方法,便可以针对不存在的键返回一个default方法返回的值。所以,编译器根据上述代码合成的HashMap子类在碰到不存在的键时将会反过来质问你“Why do you want to know?” + +makeMap方法返回的可变映射混入了 SynchronizedMap trait,因此可以用在多线程环境下。对该映射的每次访问都是同步的。以下示例展示的是从解释器内以单个线程访问该映射: + + scala> val capital = MapMaker.makeMap + capital: scala.collection.mutable.Map[String,String] = Map() + scala> capital ++ List("US" -> "Washington", + "Paris" -> "France", "Japan" -> "Tokyo") + res0: scala.collection.mutable.Map[String,String] = + Map(Paris -> France, US -> Washington, Japan -> Tokyo) + scala> capital("Japan") + res1: String = Tokyo + scala> capital("New Zealand") + res2: String = Why do you want to know? + scala> capital += ("New Zealand" -> "Wellington") + scala> capital("New Zealand") + res3: String = Wellington + +同步集合(synchronized set)的创建方法与同步映射(synchronized map)类似。例如,我们可以通过混入SynchronizedSet trait来创建同步哈希集: + + import scala.collection.mutable //导入包scala.collection.mutable + val synchroSet = + new mutable.HashSet[Int] with + mutable.SynchronizedSet[Int] + +最后,如有使用同步容器(synchronized collection)的需求,还可以考虑使用`java.util.concurrent`中提供的并发容器(concurrent collections)。 + diff --git a/cn/overviews/collections/Migrating_from_Scala_2_7.md b/cn/overviews/collections/Migrating_from_Scala_2_7.md new file mode 100644 index 0000000000..138750d6d3 --- /dev/null +++ b/cn/overviews/collections/Migrating_from_Scala_2_7.md @@ -0,0 +1,45 @@ +--- +layout: overview-large +title: Scala 2.7迁移指南 + +disqus: true + +partof: collections +num: 18 +outof: 18 +languages: [cn] +--- + + +现有应用中新旧Scala容器类型的移植基本上是自动的。只有几种情况需要特别注意。 + +Scala 2.7中容器的旧有功能基本上全部予以保留。某些功能被标记为deprecated,这意味着今后版本可能会删除它们。如果在Scala 2.8中使用这些方法,将会得到一个deprecation警告。在2.8下编译时,这些情况被视作迁移警告(migration warnings)。要得到完整的deprecation和迁移警告以及代码修改建议,请在编译时给Scala编译器scalac加上-deprecation和-Xmigration参数(注意,-Xmigration是扩展参数,因此以X开头)。你也可以将参数传给Scala REPL,从而在交互式环境中得到警告,例如: + + >scala -deprecation -Xmigration + Welcome to Scala version 2.8.0.final + 键入表达式来运行 + 键入 :help来看更多信息 + scala> val xs = List((1, 2), (3, 4)) + xs: List[(Int, Int)] = List((1, 2), (3, 4)) + scala> List.unzip(xs) + :7: warning: method unzip in object List is deprecated: use xs.unzip instead of List.unzip(xs) + List.unzip(xs) + ^ + res0: (List[Int], List[Int]) = (List(1, 3), List(2, 4)) + scala> xs.unzip + res1: (List[Int], List[Int]) = (List(1, 3), List(2, 4)) + scala> val m = xs.toMap + m: scala.collection.immutable.Map[Int, Int] = Map((1, 2), (3, 4)) + scala> m.keys + :8: warning: method keys in trait MapLike has changed semantics: + As of 2.8 keys returns Iterable[A] rather than Iterator[A]. + m.keys + ^ + res2: Iterable[Int] = Set(1, 3) + +老版本的库中有两个部分被整个移除,所以在deprecation警告中看不到它们。 + +scala.collection.jcl包被移除了。这个包试图在Scala中模拟某些Java的容器,但是该包破坏了Scala的一些对称性。绝大多数人,当他们需要Java容器的时候,他们会直接选用java.util。 +Scala 2.8通过JavaConversions对象提供了自动的在Java和Scala容器类型间转换的机制,这一机制替代了老的jcl包。 +各种投影操作被泛化整理成了视图。从实际情况来看,投影的用处并不大,因此受影响的代码应该不多。 +所以,如果你的代码用了jcl包或者投影(projections),你将不得不进行一些小的修改。 diff --git a/cn/overviews/collections/Mutable_and_Immutable_Collections.md b/cn/overviews/collections/Mutable_and_Immutable_Collections.md new file mode 100644 index 0000000000..e1b996b158 --- /dev/null +++ b/cn/overviews/collections/Mutable_and_Immutable_Collections.md @@ -0,0 +1,95 @@ +--- +layout: overview-large +title: Mutable 和 Immutable 集合 + +disqus: true + +partof: collections +num: 2 +language: [cn] +--- + + +Scala 集合类系统地区分了可变的和不可变的集合。可变集合可以在适当的地方被更新或扩展。这意味着你可以修改,添加,移除一个集合的元素。而不可变集合类,相比之下,永远不会改变。不过,你仍然可以模拟添加,移除或更新操作。但是这些操作将在每一种情况下都返回一个新的集合,同时使原来的集合不发生改变。 + +所有的集合类都可以在包`scala.collection` 或`scala.collection.mutable`,`scala.collection.immutable`,`scala.collection.generic`中找到。客户端代码需要的大部分集合类都独立地存在于3种变体中,它们位于`scala.collection`, `scala.collection.immutable`, `scala.collection.mutable`包。每一种变体在可变性方面都有不同的特征。 + +`scala.collection.immutable`包是的集合类确保不被任何对象改变。例如一个集合创建之后将不会改变。因此,你可以相信一个事实,在不同的点访问同一个集合的值,你将总是得到相同的元素。。 + +`scala.collection.mutable`包的集合类则有一些操作可以修改集合。所以处理可变集合意味着你需要去理解哪些代码的修改会导致集合同时改变。 + +`scala.collection`包中的集合,既可以是可变的,也可以是不可变的。例如:[collection.IndexedSeq[T]](http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html)] 就是 [collection.immutable.IndexedSeq[T]](http://www.scala-lang.org/api/current/scala/collection/immutable/IndexedSeq.html) 和[collection.mutable.IndexedSeq[T]](http://www.scala-lang.org/api/current/scala/collection/mutable/IndexedSeq.html)这两类的超类。`scala.collection`包中的根集合类中定义了相同的接口作为不可变集合类,同时,`scala.collection.mutable`包中的可变集合类代表性的添加了一些有辅助作用的修改操作到这个immutable 接口。 + +根集合类与不可变集合类之间的区别是不可变集合类的客户端可以确保没有人可以修改集合。然而,根集合类的客户端仅保证不修改集合本身。即使这个集合类没有提供修改集合的静态操作,它仍然可能在运行时作为可变集合被其它客户端所修改。 + +默认情况下,Scala 一直采用不可变集合类。例如,如果你仅写了`Set` 而没有任何加前缀也没有从其它地方导入`Set`,你会得到一个不可变的`set`,另外如果你写迭代,你也会得到一个不可变的迭代集合类,这是由于这些类在从scala中导入的时候都是默认绑定的。为了得到可变的默认版本,你需要显式的声明`collection.mutable.Set`或`collection.mutable.Iterable`. + +一个有用的约定,如果你想要同时使用可变和不可变集合类,只导入collection.mutable包即可。 + + import scala.collection.mutable //导入包scala.collection.mutable + +然而,像没有前缀的Set这样的关键字, 仍然指的是一个不可变集合,然而`mutable.Set`指的是可变的副本(可变集合)。 + +集合树的最后一个包是`collection.generic`。这个包包含了集合的构建块。集合类延迟了`collection.generic`类中的部分操作实现,另一方面集合框架的用户需要引用`collection.generic`中类在异常情况中。 + +为了方便和向后兼容性,一些导入类型在包scala中有别名,所以你能通过简单的名字使用它们而不需要import。这有一个例子是List 类型,它可以用以下两种方法使用,如下: + + scala.collection.immutable.List // 这是它的定义位置 + scala.List //通过scala 包中的别名 + List // 因为scala._ + // 总是是被自动导入。 + +其它类型的别名有: [Traversable](http://www.scala-lang.org/api/current/scala/collection/Traversable.html), [Iterable](http://www.scala-lang.org/api/current/scala/collection/Iterable.html), [Seq](http://www.scala-lang.org/api/current/scala/collection/Seq.html), [IndexedSeq](http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html), [Iterator](http://www.scala-lang.org/api/current/scala/collection/Iterator.html), [Stream](http://www.scala-lang.org/api/current/scala/collection/immutable/Stream.html), [Vector](http://www.scala-lang.org/api/current/scala/collection/immutable/Vector.html), [StringBuilder](http://www.scala-lang.org/api/current/scala/collection/mutable/StringBuilder.html), [Range](http://www.scala-lang.org/api/current/scala/collection/immutable/Range.html)。 + +下面的图表显示了`scala.collection`包中所有的集合类。这些都是高级抽象类或特性,它们通常具备和不可变实现一样的可变实现。 + + +![collections.png](/pictures/collections.png) + + +下面的图表显示scala.collection.immutable中的所有集合类。 + + +![collections.immutable.png](/pictures/collections.immutable.png) + + +下面的图表显示scala.collection.mutable中的所有集合类。 + + +![collections.mutable.png](/pictures/collections.mutable.png) + + +(以上三个图表由Matthias生成, 来自decodified.com)。 + +## 集合API概述 + +大多数重要的集合类都被展示在了上表。而且这些类有很多的共性。例如,每一种集合都能用相同的语法创建,写法是集合类名紧跟着元素。 + + Traversable(1, 2, 3) + Iterable("x", "y", "z") + Map("x" -> 24, "y" -> 25, "z" -> 26) + Set(Color.red, Color.green, Color.blue) + SortedSet("hello", "world") + Buffer(x, y, z) + IndexedSeq(1.0, 2.0) + LinearSeq(a, b, c) + +相同的原则也应用于特殊的集合实现,例如: + + List(1, 2, 3) + HashMap("x" -> 24, "y" -> 25, "z" -> 26) + +所有这些集合类都通过相同的途径,用toString方法展示出来。 + +Traversable类提供了所有集合支持的API,同时,对于特殊类型也是有意义的。例如,Traversable类 的map方法会返回另一个Traversable对象作为结果,但是这个结果类型在子类中被重写了。例如,在一个List上调用map会又生成一个List,在Set上调用会再生成一个Set,以此类推。 + + scala> List(1, 2, 3) map (_ + 1) + res0: List[Int] = List(2, 3, 4) + scala> Set(1, 2, 3) map (_ * 2) + res0: Set[Int] = Set(2, 4, 6) + +在集合类库中,这种在任何地方都实现了的行为,被称之为返回类型一致原则。 + +大多数类在集合树中存在这于三种变体:root, mutable 和immutable。唯一的例外是缓冲区特征,它仅在于mutable集合。 + +下面我们将一个个的回顾这些类。 diff --git a/cn/overviews/collections/Performance_Characteristics.md b/cn/overviews/collections/Performance_Characteristics.md new file mode 100644 index 0000000000..caea0471ac --- /dev/null +++ b/cn/overviews/collections/Performance_Characteristics.md @@ -0,0 +1,86 @@ +--- +layout: overview-large +title: 性能特点 + +disqus: true + +partof: collections +num: 12 +languages: [cn] +--- + + +前面的解释明确说明了不同的容器类型具有不同的性能特点。这通常是选择容器类型的首要依据。以下的两张表格,总结了一些关于容器类型常用操作的性能特点。 + +## 序列类型的性能特点 + +| head | tail | apply | update | prepend | append | insert | +|------|------|-------|--------|---------|--------|--------| +|**不可变序列**| | | | | | | +| List | C | C | L | L | C | L | - | +|Stream | C | C | L | L | C | L | - | +|Vector | eC | eC | eC | eC | eC | eC | - | +|Stack | C | C | L | L | C | C | L | +|Queue | aC | aC | L | L | L | C | - | +|Range | C | C | C | - | - | - | - | +|String | C | L | C | L | L | L | - | +|**可变序列**| | | | | | | +|ArrayBuffer | C | L | C | C | L | aC | L | +|ListBuffer | C | L | L | L | C | C | L | +|StringBuilder | C | L | C | C | L | aC | L | +|MutableList | C | L | L | L | C | C | L | +|Queue | C | L | L | L | C | C | L | +|ArraySeq | C | L | C | C | - | - | - | +|Stack | C | L | L | L | C | L | L | +|ArrayStack | C | L | C | C | aC | L | L | +|Array | C | L | C | C | - | - | - | + +## 集合和映射类型的性能特点 + +|lookup | add | remove | min | +|-------|-----|--------|-----| +|**不可变序列**| | | | +|HashSet/HashMap | eC | eC | eC | L | +|TreeSet/TreeMap | Log | Log | Log | Log | +|BitSet | C | L | L | eC1 | +|ListMap | L | L | L | L | +|可变序列| | | | +|HashSet/HashMap | eC | eC | eC | L | +|WeakHashMap | eC | eC | eC | L | +|BitSet | C | aC | C | eC1 | +|TreeSet | Log | Log | Log | Log | + +标注:1 假设位是密集分布的 + +这两个表中的条目: + +|解释如下| | +|--------|-----------------| +|C | 指操作的时间复杂度为常数 | +|eC | 指操作的时间复杂度实际上为常数,但可能依赖于诸如一个向量最大长度或是哈希键的分布情况等一些假设。 | +|aC | 该操作的均摊运行时间为常数。某些调用的可能耗时较长,但多次调用之下,每次调用的平均耗时是常数。 | +|Log | 操作的耗时与容器大小的对数成正比。 | +|L | 操作是线性的,耗时与容器的大小成正比。 | +|- | 操作不被支持。 | + +第一张表处理序列类型——无论可变还是不可变——: + +| 使用以下操作 | | +|--------|-----------------| +|head | 选择序列的第一个元素。 | +|tail | 生成一个包含除第一个元素以外所有其他元素的新的列表。 | +|apply | 索引。 | +|update | 功能性更新不可变序列,同步更新可变序列。 | +|prepend | 添加一个元素到序列头。对于不可变序列,操作会生成个新的序列。对于可变序列,操作会修改原有的序列。 | +|append | 在序列尾部插入一个元素。对于不可变序列,这将产生一个新的序列。对于可变序列,这将修改原有的序列。 | +|insert | 在序列的任意位置上插入一个元素。只有可变序列支持该操作。 | + +第二个表处理可变和不可变集与映射 + +| 使用以下操作:| | +|--------|-----------------| +|lookup | 测试一个元素是否被包含在集合中,或者找出一个键对应的值 | +|add | 添加一个新的元素到一个集合中或者添加一个键值对到一个映射中。 | +|remove | 移除一个集合中的一个元素或者移除一个映射中一个键。 | +|min | 集合中的最小元素,或者映射中的最小键。 | + diff --git a/cn/overviews/collections/Sets.md b/cn/overviews/collections/Sets.md new file mode 100644 index 0000000000..230726036a --- /dev/null +++ b/cn/overviews/collections/Sets.md @@ -0,0 +1,149 @@ +--- +layout: overview-large +title: 集合 + +disqus: true + +partof: collections +num: 6 +languages: [cn] +--- + + +集合是不包含重复元素的可迭代对象。下面的通用集合表和可变集合表中概括了集合类型适用的运算。分为几类: + +- **测试型的方法:**contains,apply,subsetOf。contains方法用于判断集合是否包含某元素。集合的apply方法和contains方法的作用相同,因此 set(elem) 等同于set constains elem。这意味着集合对象的名字能作为其自身是否包含某元素的测试函数。 + +例如 + + val fruit = Set("apple", "orange", "peach", "banana") + fruit: scala.collection.immutable.Set[java.lang.String] = + Set(apple, orange, peach, banana) + scala> fruit("peach") + res0: Boolean = true + scala> fruit("potato") + res1: Boolean = false + +- **加法类型的方法:** + 和 ++ 。添加一个或多个元素到集合中,产生一个新的集合。 +- **减法类型的方法:** - 、--。它们实现从一个集合中移除一个或多个元素,产生一个新的集合。 +- **Set运算包括并集、交集和差集**。每一种运算都存在两种书写形式:字母和符号形式。字母形式:intersect、union和diff,符号形式:&、|和&~。事实上,Set中继承自Traversable的++也能被看做union或|的另一个别名。区别是,++的参数为Traversable对象,而union和|的参数是集合。 + +## Set 类的操作 + +| WHAT IT IS | WHAT IT DOES | +|------------------------|--------------------------| +|**实验代码:** | | +|xs contains x | 测试x是否是xs的元素。 | +|xs(x) | 与xs contains x相同。 | +|xs subsetOf ys | 测试xs是否是ys的子集。 | +|**加法:** | | +|xs + x | 包含xs中所有元素以及x的集合。 | +|xs + (x, y, z) | 包含xs中所有元素及附加元素的集合 | +|xs ++ ys | 包含xs中所有元素及ys中所有元素的集合 | +|**实验代码:** | | +|xs - x | 包含xs中除x以外的所有元素的集合。 | +|xs - x | 包含xs中除去给定元素以外的所有元素的集合。 | +|xs -- ys | 集合内容为:xs中所有元素,去掉ys中所有元素后剩下的部分。 | +|xs.empty | 与xs同类的空集合。 | +|**二进制操作:** | | +|xs & ys | 集合xs和ys的交集。 | +|xs intersect ys | 等同于 xs & ys。 | +|xs | ys | 集合xs和ys的并集。 | +|xs union ys | 等同于xs | ys。 | +|xs &~ ys | 集合xs和ys的差集。 | +|xs diff ys | 等同于 xs &~ ys。 | + + +可变集合提供加法类方法,可以用来添加、删除或更新元素。下面对这些方法做下总结。 + +## mutable.Set 类的操作 + +| WHAT IT IS | WHAT IT DOES | +|------------------|------------------------| +| **加法:** | | +| xs += x | 把元素x添加到集合xs中。该操作有副作用,它会返回左操作符,这里是xs自身。 | +| xs += (x, y, z) | 添加指定的元素到集合xs中,并返回xs本身。(同样有副作用) | +| xs ++= ys | 添加集合ys中的所有元素到集合xs中,并返回xs本身。(表达式有副作用) | +| xs add x | 把元素x添加到集合xs中,如集合xs之前没有包含x,该操作返回true,否则返回false。 | +| **移除:** | | +| xs -= x | 从集合xs中删除元素x,并返回xs本身。(表达式有副作用) | +| xs -= (x, y, z) | 从集合xs中删除指定的元素,并返回xs本身。(表达式有副作用) | +| xs --= ys | 从集合xs中删除所有属于集合ys的元素,并返回xs本身。(表达式有副作用) | +| xs remove x | 从集合xs中删除元素x。如之前xs中包含了x元素,返回true,否则返回false。 | +| xs retain p | 只保留集合xs中满足条件p的元素。 | +| xs.clear() | 删除集合xs中的所有元素。 | +| **更新: ** | | +| xs(x) = b | ( 同 xs.update(x, b) )参数b为布尔类型,如果值为true就把元素x加入集合xs,否则从集合xs中删除x。 | +| **克隆:** | | +| xs.clone | 产生一个与xs具有相同元素的可变集合。 | + + +与不变集合一样,可变集合也提供了`+`和`++`操作符来添加元素,`-`和`--`用来删除元素。但是这些操作在可变集合中通常很少使用,因为这些操作都要通过集合的拷贝来实现。可变集合提供了更有效率的更新方法,`+=`和`-=`。 `s += elem`,添加元素elem到集合s中,并返回产生变化后的集合作为运算结果。同样的,`s -= elem `执行从集合s中删除元素elem的操作,并返回产生变化后的集合作为运算结果。除了`+=`和`-=`之外还有从可遍历对象集合或迭代器集合中添加和删除所有元素的批量操作符`++=`和`--=`。 + +选用`+=`和`-=`这样的方法名使得我们得以用非常近似的代码来处理可变集合和不可变集合。先看一下以下处理不可变集合s的REPL会话: + + scala> var s = Set(1, 2, 3) + s: scala.collection.immutable.Set[Int] = Set(1, 2, 3) + scala> s += 4 + scala> s -= 2 + scala> s + res2: scala.collection.immutable.Set[Int] = Set(1, 3, 4) + +我们在`immutable.Set`类型的变量中使用`+=`和`-= `。诸如 `s += 4` 的表达式是 `s = s + 4 `的缩写,它的作用是,在集合s上运用方法`+`,并把结果赋回给变量s。下面我们来分析可变集合上的类似操作。 + + scala> val s = collection.mutable.Set(1, 2, 3) + s: scala.collection.mutable.Set[Int] = Set(1, 2, 3) + scala> s += 4 + res3: s.type = Set(1, 4, 2, 3) + scala> s -= 2 + res4: s.type = Set(1, 4, 3) + +最后结果看起来和之前的在非可变集合上的操作非常相似;从`Set(1, 2, 3)`开始,最后得到`Set(1, 3, 4)`。然而,尽管相似,但它们在实现上其实是不同的。 这里`s += 4 `是在可变集合值s上调用`+=`方法,它会改变s的内容。同样的,`s -= 2` 也是在s上调用 `-= `方法,也会修改s集合的内容。 + +通过比较这两种方式得出一个重要的原则。我们通常能用一个非可变集合的变量来替换可变集合的常量,反之亦然。这一原则至少在没有别名的引用添加到Collection时起作用。别名引用主要是用来观察操作在Collection上直接做的修改还是生成了一个新的Collection。 + +可变集合同样提供作为`+=`和`-=`的变型方法,add和remove,它们的不同之处在于add和remove会返回一个表明运算是否对集合有作用的Boolean值 + +目前可变集合默认使用哈希表来存储集合元素,非可变集合则根据元素个数的不同,使用不同的方式来实现。空集用单例对象来表示。元素个数小于等于4的集合可以使用单例对象来表达,元素作为单例对象的字段来存储。 元素超过4个,非可变集合就用哈希前缀树(hash trie)来实现。 + +采用这种表示方法,较小的不可变集合(元素数不超过4)往往会比可变集合更加紧凑和高效。所以,在处理小尺寸的集合时,不妨试试不可变集合。 + +集合的两个特质是SortedSet和 BitSet。 + +## 有序集(SortedSet) + + [SortedSet](http://www.scala-lang.org/api/current/scala/collection/SortedSet.html) 是指以特定的顺序(这一顺序可以在创建集合之初自由的选定)排列其元素(使用iterator或foreach)的集合。 [SortedSet](http://www.scala-lang.org/api/current/scala/collection/SortedSet.html) 的默认表示是有序二叉树,即左子树上的元素小于所有右子树上的元素。这样,一次简单的顺序遍历能按增序返回集合中的所有元素。Scala的类 `immutable.TreeSet` 使用红黑树实现,它在维护元素顺序的同时,也会保证二叉树的平衡,即叶节点的深度差最多为1。 + +创建一个空的 [TreeSet](http://www.scala-lang.org/api/current/scala/collection/immutable/TreeSet.html) ,可以先定义排序规则: + + scala> val myOrdering = Ordering.fromLessThan[String](_ > _) + myOrdering: scala.math.Ordering[String] = ... + +然后,用这一排序规则创建一个空的树集: + + scala> TreeSet.empty(myOrdering) + res1: scala.collection.immutable.TreeSet[String] = TreeSet() + +或者,你也可以不指定排序规则参数,只需要给定一个元素类型或空集合。在这种情况下,将使用此元素类型默认的排序规则。 + + scala> TreeSet.empty[String] + res2: scala.collection.immutable.TreeSet[String] = TreeSet() + +如果通过已有的TreeSet来创建新的集合(例如,通过串联或过滤操作),这些集合将和原集合保持相同的排序规则。例如, + + scala> res2 + ("one", "two", "three", "four") + res3: scala.collection.immutable.TreeSet[String] = TreeSet(four, one, three, two) + +有序集合同样支持元素的范围操作。例如,range方法返回从指定起始位置到结束位置(不含结束元素)的所有元素,from方法返回大于等于某个元素的所有元素。调用这两种方法的返回值依然是有序集合。例如: + + scala> res3 range ("one", "two") + res4: scala.collection.immutable.TreeSet[String] = TreeSet(one, three) + scala> res3 from "three" + res5: scala.collection.immutable.TreeSet[String] = TreeSet(three, two) + +## 位集合(Bitset) + +位集合是由单字或多字的紧凑位实现的非负整数的集合。其内部使用Long型数组来表示。第一个Long元素表示的范围为0到63,第二个范围为64到127,以此类推(值为0到127的非可变位集合通过直接将值存储到第一个或第两个Long字段的方式,优化掉了数组处理的消耗)。对于每个Long,如果有相应的值包含于集合中则它对应的位设置为1,否则该位为0。这里遵循的规律是,位集合的大小取决于存储在该集合的最大整数的值的大小。假如N是为集合所要表示的最大整数,则集合的大小就是N/64个长整形字,或者N/8个字节,再加上少量额外的状态信息字节。 + +因此当位集合包含的元素值都比较小时,它比其他的集合类型更紧凑。位集合的另一个优点是它的contains方法(成员测试)、+=运算(添加元素)、-=运算(删除元素)都非常的高效。 + diff --git a/cn/overviews/collections/Strings.md b/cn/overviews/collections/Strings.md new file mode 100644 index 0000000000..7fd1ef458d --- /dev/null +++ b/cn/overviews/collections/Strings.md @@ -0,0 +1,27 @@ +--- +layout: overview-large +title: 字符串 + +disqus: true + +partof: collections +num: 11 +languages: [cn] +--- + +像数组,字符串不是直接的序列,但是他们可以转换为序列,并且他们也支持所有的在字符串上的序列操作这里有些例子让你可以理解在字符串上操作。 + + scala> val str = "hello" + str: java.lang.String = hello + scala> str.reverse + res6: String = olleh + scala> str.map(_.toUpper) + res7: String = HELLO + scala> str drop 3 + res8: String = lo + scala> str slice (1, 4) + res9: String = ell + scala> val s: Seq[Char] = str + s: Seq[Char] = WrappedString(h, e, l, l, o) + +这些操作依赖于两种隐式转换。第一种,低优先级转换映射一个String到WrappedString,它是`immutable.IndexedSeq`的子类。在上述代码中这种转换应用在一个string转换为一个Seq。另一种,高优先级转换映射一个string到StringOps 对象,从而在immutable 序列到strings上增加了所有的方法。在上面的例子里,这种隐式转换插入在reverse,map,drop和slice的方法调用中。 diff --git a/cn/overviews/collections/The_sequence_traits.md b/cn/overviews/collections/The_sequence_traits.md new file mode 100644 index 0000000000..b791093225 --- /dev/null +++ b/cn/overviews/collections/The_sequence_traits.md @@ -0,0 +1,107 @@ +--- +layout: overview-large +title: 序列trait:Seq、IndexedSeq及LinearSeq + +disqus: true + +partof: collections +num: 5 +languages: [cn] +--- + + +[Seq](http://www.scala-lang.org/api/current/scala/collection/Seq.html) trait用于表示序列。所谓序列,指的是一类具有一定长度的可迭代访问的对象,其中每个元素均带有一个从0开始计数的固定索引位置。 + +序列的操作有以下几种,如下表所示: + +- **索引和长度的操作** apply、isDefinedAt、length、indices,及lengthCompare。序列的apply操作用于索引访问;因此,Seq[T]类型的序列也是一个以单个Int(索引下标)为参数、返回值类型为T的偏函数。换言之,Seq[T]继承自Partial Function[Int, T]。序列各元素的索引下标从0开始计数,最大索引下标为序列长度减一。序列的length方法是collection的size方法的别名。lengthCompare方法可以比较两个序列的长度,即便其中一个序列长度无限也可以处理。 +- **索引检索操作**(indexOf、lastIndexOf、indexofSlice、lastIndexOfSlice、indexWhere、lastIndexWhere、segmentLength、prefixLength)用于返回等于给定值或满足某个谓词的元素的索引。 +- **加法运算**(+:,:+,padTo)用于在序列的前面或者后面添加一个元素并作为新序列返回。 +- **更新操作**(updated,patch)用于替换原序列的某些元素并作为一个新序列返回。 +- **排序操作**(sorted, sortWith, sortBy)根据不同的条件对序列元素进行排序。 +- **反转操作**(reverse, reverseIterator, reverseMap)用于将序列中的元素以相反的顺序排列。 +- **比较**(startsWith, endsWith, contains, containsSlice, corresponds)用于对两个序列进行比较,或者在序列中查找某个元素。 +- **多集操作**(intersect, diff, union, distinct)用于对两个序列中的元素进行类似集合的操作,或者删除重复元素。 + +如果一个序列是可变的,它提供了另一种更新序列中的元素的,但有副作用的update方法,Scala中常有这样的语法,如seq(idx) = elem。它只是seq.update(idx, elem)的简写,所以update 提供了方便的赋值语法。应注意update 和updated之间的差异。update 再原来基础上更改序列中的元素,并且仅适用于可变序列。而updated 适用于所有的序列,它总是返回一个新序列,而不会修改原序列。 + +## Set类的操作 + +| WHAT IT IS | WHAT IT DOES | +|------------------ | -------------------| +| **索引和长度** | | +| xs(i) | (或者写作xs apply i)。xs的第i个元素 | +| xs isDefinedAt i | 测试xs.indices中是否包含i。 | +| xs.length | 序列的长度(同size)。 | +| xs.lengthCompare ys | 如果xs的长度小于ys的长度,则返回-1。如果xs的长度大于ys的长度,则返回+1,如果它们长度相等,则返回0。即使其中一个序列是无限的,也可以使用此方法。 | +| xs.indices | xs的索引范围,从0到xs.length - 1。 | +| **索引搜索** | | +| xs indexOf x | 返回序列xs中等于x的第一个元素的索引(存在多种变体)。 | +| xs lastIndexOf x | 返回序列xs中等于x的最后一个元素的索引(存在多种变体)。 | +| xs indexOfSlice ys | 查找子序列ys,返回xs中匹配的第一个索引。 | +| xs indexOfSlice ys | 查找子序列ys,返回xs中匹配的倒数一个索引。 | +| xs indexWhere p | xs序列中满足p的第一个元素。(有多种形式) | +| xs segmentLength (p, i) | xs中,从xs(i)开始并满足条件p的元素的最长连续片段的长度。 | +| xs prefixLength p | xs序列中满足p条件的先头元素的最大个数。 | +| **加法:** | | +| x +: xs | 由序列xs的前方添加x所得的新序列。 | +| xs :+ x | 由序列xs的后方追加x所得的新序列。 | +| xs padTo (len, x) | 在xs后方追加x,直到长度达到len后得到的序列。 | +| **更新** | | +| xs patch (i, ys, r) | 将xs中第i个元素开始的r个元素,替换为ys所得的序列。 | +| xs updated (i, x) | 将xs中第i个元素替换为x后所得的xs的副本。 | +| xs(i) = x | (或写作 xs.update(i, x),仅适用于可变序列)将xs序列中第i个元素修改为x。 | +| **排序** | | +| xs.sorted | 通过使用xs中元素类型的标准顺序,将xs元素进行排序后得到的新序列。 | +| xs sortWith lt | 将lt作为比较操作,并以此将xs中的元素进行排序后得到的新序列。 | +| xs sortBy f | 将序列xs的元素进行排序后得到的新序列。参与比较的两个元素各自经f函数映射后得到一个结果,通过比较它们的结果来进行排序。 | +| **反转** | | +| xs.reverse | 与xs序列元素顺序相反的一个新序列。 | +| xs.reverseIterator | 产生序列xs中元素的反序迭代器。 | +| xs reverseMap f | 以xs的相反顺序,通过f映射xs序列中的元素得到的新序列。 | +| **比较** | | +| xs startsWith ys | 测试序列xs是否以序列ys开头(存在多种形式)。 | +| xs endsWith ys | 测试序列xs是否以序列ys结束(存在多种形式)。 | +| xs contains x | 测试xs序列中是否存在一个与x相等的元素。 | +| xs containsSlice ys | 测试xs序列中是否存在一个与ys相同的连续子序列。 | +| (xs corresponds ys)(p) | 测试序列xs与序列ys中对应的元素是否满足二元的判断式p。 | +| **多集操作** | | +| xs intersect ys | 序列xs和ys的交集,并保留序列xs中的顺序。 | +| xs diff ys | 序列xs和ys的差集,并保留序列xs中的顺序。 | +| xs union ys | 并集;同xs ++ ys。 | +| xs.distinct | 不含重复元素的xs的子序列。 | +| | | + + +特性(trait) [Seq](http://www.scala-lang.org/api/current/scala/collection/Seq.html) 具有两个子特征(subtrait) [LinearSeq](http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html)和[IndexedSeq](http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html)。它们不添加任何新的操作,但都提供不同的性能特点:线性序列具有高效的 head 和 tail 操作,而索引序列具有高效的apply, length, 和 (如果可变) update操作。 + +常用线性序列有 `scala.collection.immutable.List`和`scala.collection.immutable.Stream`。常用索引序列有 `scala.Array scala.collection.mutable.ArrayBuffer`。Vector 类提供一个在索引访问和线性访问之间有趣的折中。它同时具有高效的恒定时间的索引开销,和恒定时间的线性访问开销。正因为如此,对于混合访问模式,vector是一个很好的基础。后面将详细介绍vector。 + +## 缓冲器 + +Buffers是可变序列一个重要的种类。它们不仅允许更新现有的元素,而且允许元素的插入、移除和在buffer尾部高效地添加新元素。buffer 支持的主要新方法有:用于在尾部添加元素的 `+=` 和 `++=`;用于在前方添加元素的`+=: `和` ++=:` ;用于插入元素的 `insert`和`insertAll`;以及用于删除元素的` remove` 和 `-=`。如下表所示。 + +ListBuffer和ArrayBuffer是常用的buffer实现 。顾名思义,ListBuffer依赖列表(List),支持高效地将它的元素转换成列表。而ArrayBuffer依赖数组(Array),能快速地转换成数组。 + +## Buffer类的操作 + +| WHAT IT IS | WHAT IT DOES | +|--------------------- | -----------------------| +| **加法:** | | +| buf += x | 将元素x追加到buffer,并将buf自身作为结果返回。 | +| buf += (x, y, z) | 将给定的元素追加到buffer。 | +| buf ++= xs | 将xs中的所有元素追加到buffer。 | +| x +=: buf | 将元素x添加到buffer的前方。 | +| xs ++=: buf | 将xs中的所有元素都添加到buffer的前方。 | +| buf insert (i, x) | 将元素x插入到buffer中索引为i的位置。 | +| buf insertAll (i, xs) | 将xs的所有元素都插入到buffer中索引为i的位置。 | +| **移除:** | | +| buf -= x | 将元素x从buffer中移除。 | +| buf remove i | 将buffer中索引为i的元素移除。 | +| buf remove (i, n) | 将buffer中从索引i开始的n个元素移除。 | +| buf trimStart n | 移除buffer中的前n个元素。 | +| buf trimEnd n | 移除buffer中的后n个元素。 | +| buf.clear() | 移除buffer中的所有元素。 | +| **克隆:** | | +| buf.clone | 与buf具有相同元素的新buffer。 | + diff --git a/cn/overviews/collections/Trait_Iterable.md b/cn/overviews/collections/Trait_Iterable.md new file mode 100644 index 0000000000..e37fb8de7b --- /dev/null +++ b/cn/overviews/collections/Trait_Iterable.md @@ -0,0 +1,67 @@ +--- +layout: overview-large +title: Trait Iterable + +disqus: true + +partof: collections +num: 4 +languages: [cn] +--- + +自下而上的容器(collection)层次结构具有可迭代的Trait。Trait的所有方法可定义为一个抽象方法,逐个生成容器(collection)元素迭代器。Traversable Trait的foreach方法实现了迭代器的Iterable。下面是具体的实现。 + + def foreach[U](f: Elem => U): Unit = { + val it = iterator + while (it.hasNext) f(it.next()) + } + +许多Iterable 的子类覆写了Iteable的foreach标准实现,因为它们提供了更多有效的实现。记住,由于性能问题,foreach是Traversable所有操作能够实现的基础。 + +Iterable有两个方法返回迭代器:grouped和sliding。然而,这些迭代器返回的不是单个元素,而是原容器(collection)元素的全部子序列。这些最大的子序列作为参数传给这些方法。grouped方法返回元素的增量分块,sliding方法生成一个滑动元素的窗口。两者之间的差异通过REPL的作用能够清楚看出。 + + scala> val xs = List(1, 2, 3, 4, 5) + xs: List[Int] = List(1, 2, 3, 4, 5) + scala> val git = xs grouped 3 + git: Iterator[List[Int]] = non-empty iterator + scala> git.next() + res3: List[Int] = List(1, 2, 3) + scala> git.next() + res4: List[Int] = List(4, 5) + scala> val sit = xs sliding 3 + sit: Iterator[List[Int]] = non-empty iterator + scala> sit.next() + res5: List[Int] = List(1, 2, 3) + scala> sit.next() + res6: List[Int] = List(2, 3, 4) + scala> sit.next() + res7: List[Int] = List(3, 4, 5) + +当只有一个迭代器可用时,Trait Iterable增加了一些其他方法,为了能被有效的实现的可遍历的情况。这些方法总结在下面的表中。 + +## Trait Iterable操作 + +| WHAT IT IS | WHAT IT DOES | +|--------------|--------------| +| **抽象方法:** | | +| xs.iterator | xs迭代器生成的每一个元素,以相同的顺序就像foreach一样遍历元素。 | +| **其他迭代器:** | | +| xs grouped size | 一个迭代器生成一个固定大小的容器(collection)块。 | +| xs sliding size | 一个迭代器生成一个固定大小的滑动窗口作为容器(collection)的元素。 | +| **子容器(Subcollection):** | | +| xs takeRight n | 一个容器(collection)由xs的最后n个元素组成(或,若定义的元素是无序,则由任意的n个元素组成)。 | +| xs dropRight n | 一个容器(collection)由除了xs 被取走的(执行过takeRight ()方法)n个元素外的其余元素组成。 | +| **拉链方法(Zippers):** | | +| xs zip ys | 把一对容器 xs和ys的包含的元素合成到一个iterabale。 | +| xs zipAll (ys, x, y) | 一对容器 xs 和ys的相应的元素合并到一个iterable ,实现方式是通过附加的元素x或y,把短的序列被延展到相对更长的一个上。 | +| xs.zip WithIndex | 把一对容器xs和它的序列,所包含的元素组成一个iterable 。 | +| **比对:** | | +| xs sameElements ys | 测试 xs 和 ys 是否以相同的顺序包含相同的元素。 | + + +在Iterable下的继承层次结构你会发现有三个traits:[Seq](http://www.scala-lang.org/docu/files/collections-api/collections_5.html),[Set](http://www.scala-lang.org/docu/files/collections-api/collections_7.html),和 [Map](http://www.scala-lang.org/docu/files/collections-api/collections_10.html)。这三个Traits有一个共同的特征,它们都实现了[PartialFunction](http://www.scala-lang.org/api/current/scala/PartialFunction.html) trait以及它的应用和isDefinedAt 方法。然而,每一个trait实现的[PartialFunction](http://www.scala-lang.org/api/current/scala/PartialFunction.html) 方法却各不相同。 + +例如序列,使用用的是位置索引,它里面的元素的总是从0开始编号。即`Seq(1, 2, 3)(1) `为2。例如sets,使用的是成员测试。例如`Set('a', 'b', 'c')('b') `算出来的是true,而`Set()('a')`为false。最后,maps使用的是选择。比如`Map('a' -> 1, 'b' -> 10, 'c' -> 100)('b')` 得到的是10。 + +接下来,我们将详细的介绍三种类型的容器(collection)。 + diff --git a/cn/overviews/collections/Trait_Traversable.md b/cn/overviews/collections/Trait_Traversable.md new file mode 100644 index 0000000000..63fa895497 --- /dev/null +++ b/cn/overviews/collections/Trait_Traversable.md @@ -0,0 +1,122 @@ +--- +layout: overview-large +title: Trait Traversable + +disqus: true + +partof: collections +num: 3 +languages: [cn] +--- + +Traversable(遍历)是容器(collection)类的最高级别特性,它唯一的抽象操作是foreach: + +`def foreach[U](f: Elem => U) ` + +需要实现Traversable的容器(collection)类仅仅需要定义与之相关的方法,其他所有方法可都可以从Traversable中继承。 + +foreach方法用于遍历容器(collection)内的所有元素和每个元素进行指定的操作(比如说f操作)。操作类型是Elem => U,其中Elem是容器(collection)中元素的类型,U是一个任意的返回值类型。对f的调用仅仅是容器遍历的副作用,实际上所有函数f的计算结果都被foreach抛弃了。 + +Traversable同时定义的很多具体方法,如下表所示。这些方法可以划分为以下类别: + +- **相加操作++(addition)**表示把两个traversable对象附加在一起或者把一个迭代器的所有元素添加到traversable对象的尾部。 + +- **Map**操作有map,flatMap和collect,它们可以通过对容器中的元素进行某些运算来生成一个新的容器。 + +- **转换器(Conversion)**操作包括toArray,toList,toIterable,toSeq,toIndexedSeq,toStream,toSet,和toMap,它们可以按照某种特定的方法对一个Traversable 容器进行转换。等容器类型已经与所需类型相匹配的时候,所有这些转换器都会不加改变的返回该容器。例如,对一个list使用toList,返回的结果就是list本身。 + +- **拷贝(Copying)**操作有copyToBuffer和copyToArray。从字面意思就可以知道,它们分别用于把容器中的元素元素拷贝到一个缓冲区或者数组里。 + +- **Size info**操作包括有isEmpty,nonEmpty,size和hasDefiniteSize。Traversable容器有有限和无限之分。比方说,自然数流Stream.from(0)就是一个无限的traversable 容器。hasDefiniteSize方法能够判断一个容器是否可能是无限的。若hasDefiniteSize返回值为ture,容器肯定有限。若返回值为false,根据完整信息才能判断容器(collection)是无限还是有限。 + +- **元素检索(Element Retrieval)**操作有head,last,headOption,lastOption和find。这些操作可以查找容器的第一个元素或者最后一个元素,或者第一个符合某种条件的元素。注意,尽管如此,但也不是所有的容器都明确定义了什么是“第一个”或”最后一个“。例如,通过哈希值储存元素的哈希集合(hashSet),每次运行哈希值都会发生改变。在这种情况下,程序每次运行都可能会导致哈希集合的”第一个“元素发生变化。如果一个容器总是以相同的规则排列元素,那这个容器是有序的。大多数容器都是有序的,但有些不是(例如哈希集合)-- 排序会造成一些额外消耗。排序对于重复性测试和辅助调试是不可或缺的。这就是为什么Scala容器中的所有容器类型都把有序作为可选项。例如,带有序性的HashSet就是LinkedHashSet。 + +- **子容器检索(sub-collection Retrieval)**操作有tail,init,slice,take,drop,takeWhilte,dropWhile,filter,filteNot和withFilter。它们都可以通过范围索引或一些论断的判断返回某些子容器。 + +- **拆分(Subdivision)**操作有splitAt,span,partition和groupBy,它们用于把一个容器(collection)里的元素分割成多个子容器。 + +- **元素测试(Element test)**包括有exists,forall和count,它们可以用一个给定论断来对容器中的元素进行判断。 + +- **折叠(Folds)**操作有foldLeft,foldRight,/:,:\,reduceLeft和reduceRight,用于对连续性元素的二进制操作。 + +- **特殊折叠(Specific folds)**包括sum, product, min, max。它们主要用于特定类型的容器(数值或比较)。 + +- **字符串(String)**操作有mkString,addString和stringPrefix,可以将一个容器通过可选的方式转换为字符串。 + +- **视图(View)**操作包含两个view方法的重载体。一个view对象可以当作是一个容器客观地展示。接下来将会介绍更多有关视图内容。 + +## Traversable对象的操作 + +| WHAT IT IS |WHAT IT DOES | +|------------------------|------------------------------| +| **抽象方法:** | | +| xs foreach f | 对xs中的每一个元素执行函数f | +| **加运算(Addition):** | | +| xs ++ ys | 生成一个由xs和ys中的元素组成容器。ys是一个TraversableOnce容器,即Taversable类型或迭代器。 +| **Maps:** | | +| xs map f | 通过函数xs中的每一个元素调用函数f来生成一个容器。 | +| xs flatMap f | 通过对容器xs中的每一个元素调用作为容器的值函数f,在把所得的结果连接起来作为一个新的容器。 | +| xs collect f | 通过对每个xs中的符合定义的元素调用偏函数f,并把结果收集起来生成一个集合。 | +| **转换(Conversions):** | | +| xs.toArray | 把容器转换为一个数组 | +| xs.toList | 把容器转换为一个list | +| xs.toIterable | 把容器转换为一个迭代器。 | +| xs.toSeq | 把容器转换为一个序列 | +| xs.toIndexedSeq | 把容器转换为一个索引序列 | +| xs.toStream | 把容器转换为一个延迟计算的流。 | +| xs.toSet | 把容器转换为一个集合(Set)。 | +| xs.toMap | 把由键/值对组成的容器转换为一个映射表(map)。如果该容器并不是以键/值对作为元素的,那么调用这个操作将会导致一个静态类型的错误。 | +| **拷贝(Copying):** | | +| xs copyToBuffer buf | 把容器的所有元素拷贝到buf缓冲区。 | +| xs copyToArray(arr, s, n) | 拷贝最多n个元素到数组arr的坐标s处。参数s,n是可选项。 | +| **大小判断(Size info):** | | +| xs.isEmpty | 测试容器是否为空。 | +| xs.nonEmpty | 测试容器是否包含元素。 | +| xs.size | 计算容器内元素的个数。 | +| xs.hasDefiniteSize | 如果xs的大小是有限的,则为true。 | +| **元素检索(Element Retrieval):** | | +| xs.head | 返回容器内第一个元素(或其他元素,若当前的容器无序)。 | +| xs.headOption | xs选项值中的第一个元素,若xs为空则为None。 | +| xs.last | 返回容器的最后一个元素(或某个元素,如果当前的容器无序的话)。 | +| xs.lastOption | xs选项值中的最后一个元素,如果xs为空则为None。 | +| xs find p | 查找xs中满足p条件的元素,若存在则返回第一个元素;若不存在,则为空。 | +| **子容器(Subcollection):** | | +| xs.tail | 返回由除了xs.head外的其余部分。 | +| xs.init | 返回除xs.last外的其余部分。 | +| xs slice (from, to) | 返回由xs的一个片段索引中的元素组成的容器(从from到to,但不包括to)。 | +| xs take n | 由xs的第一个到第n个元素(或当xs无序时任意的n个元素)组成的容器。 | +| xs drop n | 由除了xs take n以外的元素组成的容器。 | +| xs takeWhile p | 容器xs中最长能够满足断言p的前缀。 | +| xs dropWhile p | 容器xs中除了xs takeWhile p以外的全部元素。 | +| xs filter p | 由xs中满足条件p的元素组成的容器。 | +| xs withFilter p | 这个容器是一个不太严格的过滤器。子容器调用map,flatMap,foreach和withFilter只适用于xs中那些的满足条件p的元素。 | +| xs filterNot p | 由xs中不满足条件p的元素组成的容器。 | +| **拆分(Subdivision):** | | +| xs splitAt n | 把xs从指定位置的拆分成两个容器(xs take n和xs drop n)。 | +| xs span p | 根据一个断言p将xs拆分为两个容器(xs takeWhile p, xs.dropWhile p)。 | +| xs partition p | 把xs分割为两个容器,符合断言p的元素赋给一个容器,其余的赋给另一个(xs filter p, xs.filterNot p)。 | +| xs groupBy f | 根据判别函数f把xs拆分一个到容器(collection)的map中。 | +| **条件元素(Element Conditions):** | | +| xs forall p | 返回一个布尔值表示用于表示断言p是否适用xs中的所有元素。 | +| xs exists p | 返回一个布尔值判断xs中是否有部分元素满足断言p。 | +| xs count p | 返回xs中符合断言p条件的元素个数。 | +| **折叠(Fold):** | | +| (z /: xs)(op) | 在xs中,对由z开始从左到右的连续元素应用二进制运算op。 | +| (xs :\ z)(op) | 在xs中,对由z开始从右到左的连续元素应用二进制运算op | +| xs.foldLeft(z)(op) | 与(z /: xs)(op)相同。 | +| xs.foldRight(z)(op) | 与 (xs :\ z)(op)相同。 | +| xs reduceLeft op | 非空容器xs中的连续元素从左至右调用二进制运算op。 | +| xs reduceRight op | 非空容器xs中的连续元素从右至左调用二进制运算op。 | +| **特殊折叠(Specific Fold):** | | +| xs.sum | 返回容器xs中数字元素的和。 | +| xs.product | xs返回容器xs中数字元素的积。 | +| xs.min | 容器xs中有序元素值中的最小值。 | +| xs.max | 容器xs中有序元素值中的最大值。 | +| **字符串(String):** | | +| xs addString (b, start, sep, end) | 把一个字符串加到StringBuilder对象b中,该字符串显示为将xs中所有元素用分隔符sep连接起来并封装在start和end之间。其中start,end和sep都是可选的。 | +| xs mkString (start, sep, end) | 把容器xs转换为一个字符串,该字符串显示为将xs中所有元素用分隔符sep连接起来并封装在start和end之间。其中start,end和sep都是可选的。 | +| xs.stringPrefix | 返回一个字符串,该字符串是以容器名开头的xs.toString。 | +| **视图(View):** | | +| xs.view | 通过容器xs生成一个视图。 | +| xs view (from, to) | 生成一个表示在指定索引范围内的xs元素的视图。 | + diff --git a/cn/overviews/collections/Views.md b/cn/overviews/collections/Views.md new file mode 100644 index 0000000000..36c1baf125 --- /dev/null +++ b/cn/overviews/collections/Views.md @@ -0,0 +1,125 @@ +--- +layout: overview-large +title: 视图 + +disqus: true + +partof: collections +num: 14 +languages: [cn] +--- + +各种容器类自带一些用于开发新容器的方法。例如map、filter和++。我们将这类方法称为转换器(transformers),喂给它们一个或多个容器,它们就会输入一个新容器。 + +有两个主要途径实现转换器(transformers)。一个途径叫紧凑法,就是一个容器及其所有单元构造成这个转换器(transformers)。另一个途径叫松弛法或惰性法(lazy),就是一个容器及其所有单元仅仅是构造了结果容器的代理,并且结果容器的每个单元都是按单一需求构造的。 + +作为一个松弛法转换器的例子,分析下面的 lazy map操作: + + def lazyMap[T, U](coll: Iterable[T], f: T => U) = new Iterable[T] { + def iterator = coll.iterator map f + } + +注意lazyMap构造了一个没有遍历容器coll(collection coll)所有单元的新容器Iterable。当需要时,函数f 可作用于一个该新容器的迭代器单元。 + +除了Stream的转换器是惰性实现的外,Scala的其他容器默认都是用紧凑法实现它们的转换器。 +然而,通常基于容器视图,可将容器转换成惰性容器,反之亦可。视图是代表一些基容器但又可以惰性得构成转换器(transformers)的一种特殊容器。 + +从容器转换到其视图,可以使用容器相应的视图方法。如果xs是个容器,那么xs.view就是同一个容器,不过所有的转换器都是惰性的。若要从视图转换回紧凑型容器,可以使用强制性方法。 + +让我们看一个例子。假设你有一个带有int型数据的vector对象,你想用map函数对它进行两次连续的操作 + + scala> val v = Vector(1 to 10: _*) + v: scala.collection.immutable.Vector[Int] = + Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + scala> v map (_ + 1) map (_ * 2) + res5: scala.collection.immutable.Vector[Int] = + Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22) + +在最后一条语句中,表达式`v map (_ + 1) ` 构建了一个新的vector对象,该对象被map第二次调用`(_ * 2)`而转换成第3个vector对象。很多情况下,从map的第一次调用构造一个中间结果有点浪费资源。上述示例中,将map的两次操作结合成一次单一的map操作执行得会更快些。如果这两次操作同时可行,则可亲自将它们结合成一次操作。但通常,数据结构的连续转换出现在不同的程序模块里。融合那些转换将会破坏其模块性。更普遍的做法是通过把vector对象首先转换成其视图,然后把所有的转换作用于该视图,最后强制将视图转换成vector对象,从而避开出现中间结果这种情况。 + + scala> (v.view map (_ + 1) map (_ * 2)).force + res12: Seq[Int] = Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22) + +让我们按这个步骤一步一步再做一次: + + scala> val vv = v.view + vv: scala.collection.SeqView[Int,Vector[Int]] = + SeqView(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + v.view 给出了SeqView对象,它是一个延迟计算的Seq。SeqView有两个参数,第一个是整型(Int)表示视图单元的类型。第二个Vector[Int]数组表示当需要强制将视图转回时构造函数的类型。 + +将第一个map 转换成视图可得到: + + scala> vv map (_ + 1) + res13: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...) + +map的结果是输出`SeqViewM(...)`的值。实质是记录函数`map (_ + 1)`应用在vector v数组上的封装。除非视图被强制转换,否则map不会被执行。然而,`SeqView `后面的 `‘’M‘’`表示这个视图包含一个map操作。其他字母表示其他延迟操作。比如`‘’S‘’`表示一个延迟的slice操作,而`‘’R‘’`表示reverse操作。现在让我们将第二个map操作作用于最后的结果。 + + scala> res13 map (_ * 2) + res14: scala.collection.SeqView[Int,Seq[_]] = SeqViewMM(...) + +现在得到了包含2个map操作的`SeqView`对象,这将输出两个`‘’M‘’: SeqViewMM(...)`。最后强制转换最后结果: + + scala> res14.force res15: Seq[Int] = Vector(4, 6, 8, 10, 12, 14, 16, 18, 20, 22) + +两个存储函数应用于强制操作的执行部分并构造一个新的矢量数组。这样,没有中间数据结构是必须的。 + +需要注意的是静态类型的最终结果是Seq对象而不是Vector对象。跟踪类型后我们看到一旦第一个延迟map被应用,就会得到一个静态类型的`SeqViewM[Int, Seq[_]`。就是说,应用于特定序列类型的矢量数组的"knowledge"会被丢失。一些类的视图的实现需要大量代码,于是Scala 容器链接库仅主要为一般的容器类型而不是特殊功能(一个例外是数组:将数组操作延迟会再次给予静态类型数组的结果)的实现提供视图。 + +有2个理由使您考虑使用视图。首先是性能。你已经看到,通过转换容器为视图可以避免中间结果。这些节省是非常重要的。就像另一个例子,考虑到在一个单词列表找到第一个回文问题。回文就是顺读或倒读都一样的单词。以下是必要的定义: + + def isPalindrome(x: String) = x == x.reverse + def findPalidrome(s: Seq[String]) = s find isPalindrome + +现在,假设你有一个很长序列的单词表,你想在这个序列的第一百万个字内找到回文。你能复用findPalidrome么?当然,你可以写: + + findPalindrome(words take 1000000) + +这很好地解决了两个方面问题:提取序列的第一个百万单词,找到一个回文结构。但缺点是,它总是构建由一百万个字组成的中间序列,即使该序列的第一个单词已经是一个回文。所以可能,999 '999个单词在根本没被检查就复制到中间的结果(数据结构中)。很多程序员会在这里放弃转而编写给定参数前缀的寻找回文的自定义序列。但对于视图(views),这没必要。简单地写: + + findPalindrome(words.view take 1000000) + +这同样是一个很好的分选,但不是一个序列的一百万个元素,它只会构造一个轻量级的视图对象。这样,你无需在性能和模块化之间衡量取舍。 + +第二个案例适用于遍历可变序列的视图。许多转换器函数在那些视图提供视窗给部分元素可以非常规更新的原始序列。通过一个示例看看这种情形。让我们假定有一个数组arr: + + scala> val arr = (0 to 9).toArray + arr: Array[Int] = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + +你可以在arr数组视图的一部分里创建一个子窗体。 + + scala> val subarr = arr.view.slice(3, 6) + subarr: scala.collection.mutable.IndexedSeqView[ + Int,Array[Int]] = IndexedSeqViewS(...) + +这里给出了一个视图subarr指向从数组arr的第三个元素开始的5个元素组成的子数组。这个视图没有拷贝这些元素,而只是提供了它们的一个映射。现在,假设你有一个修改序列元素的方法。例如,下面的negate方法将对给定整数序列的所有元素取反操作: + + scala> def negate(xs: collection.mutable.Seq[Int]) = + for (i <- 0 until xs.length) xs(i) = -xs(i) + negate: (xs: scala.collection.mutable.Seq[Int])Unit + +假定现在你要对数组arr里从第3个元素开始的5个元素取反操作。你能够使用negate方法来做么?使用视图,就这么简单: + + scala> negate(subarr) + scala> arr + res4: Array[Int] = Array(0, 1, 2, -3, -4, -5, 6, 7, 8, 9) + +看看发生什么了,negate方法改变了从数组arr截取元素生成的数组subarr里面的所有元素。你再次看到视图(views)在保持模块化方面的功效。上面的代码完美地分离了使用方法时如何安排下标顺序和使用什么方法的问题。 + +看了这些漂亮的视图应用示例你可能会困惑于为什么怎么还是会有 strict型容器存在了?一个原因是 lazy型容器性能不总是优于strict型容器的。对于较小的容器在视图里创建和关闭应用附加开销通常是大于从避免中间数据结构的增益。一个更重要的原因是,如果延迟操作有副作用,可能导致视图非常混乱。 + +这里有一个使用2.8版本以前的Scala的几个用户的例子。在这些版本中, Range类型是延迟型的。所以它表现的效果就像一个视图。人们试图创造一些对象像这样: + + val actors = for (i <- 1 to 10) yield actor { ... } + +令他们吃惊的是,没有对象被执行。甚至在后面括号里的代码里无法创建和启动对象方法。对于为什么什么都没发生,记住,对上述表达式等价于map应用: + + val actors = (1 to 10) map (i => actor { ... }) + +由于先前的范围由(1~10)表现得像一个视图,map的结果又是一个视图。那就是,没有元素计算,并且,因此,没有对象的构建!对象会在整个表达的范围内被强制创建,但这并不就是对象要完成的工作。 + +为了避免这样的疑惑,Scala 2.8版容器链接库有了更严格的规定。除streams 和 views 外所有容器都是strict型的。只有一种途径将strict型容器转换成lazy型,那就是采用视图(view)方法。而唯一可逆的途径(from lazy to strict)就是采用强制。因此在Scala 2.8版里actors 对象上面的定义会像预期的那样,这将创建和启动10个actors对象。回到先前疑惑处,你可以增加一个明确的视图方法调用: + + val actors = for (i <- (1 to 10).view) yield actor { ... } + +总之,视图是协调性能和模块化的一个强大工具。但为了不被延迟利弊评估方面的纠缠,应该在2个方面对视图进行约束。要么你将在容器转换器不产生副作用的纯粹的功能代码里使用视图。要么你将它们应用在所有的修改都是明确的可变容器。最好的规避就是混合视图和操作,创建新的根接口,同时消除片面影响。 diff --git a/cn/overviews/collections/pictures/collections.immutable.png b/cn/overviews/collections/pictures/collections.immutable.png new file mode 100644 index 0000000000..5069e79f6c Binary files /dev/null and b/cn/overviews/collections/pictures/collections.immutable.png differ diff --git a/cn/overviews/collections/pictures/collections.mutable.png b/cn/overviews/collections/pictures/collections.mutable.png new file mode 100644 index 0000000000..46db480b69 Binary files /dev/null and b/cn/overviews/collections/pictures/collections.mutable.png differ diff --git a/cn/overviews/collections/pictures/collections.png b/cn/overviews/collections/pictures/collections.png new file mode 100644 index 0000000000..f0cc19138f Binary files /dev/null and b/cn/overviews/collections/pictures/collections.png differ diff --git a/cn/overviews/core/Futures-and-Promises.md b/cn/overviews/core/Futures-and-Promises.md new file mode 100644 index 0000000000..5354ae703a --- /dev/null +++ b/cn/overviews/core/Futures-and-Promises.md @@ -0,0 +1,504 @@ +--- +layout: overview-large +title: Futures and Promises + +disqus: true + +partof: core +num: 5 +languages: [cn] +--- + +Philipp Haller, Aleksandar Prokopec, Heather Miller, Viktor Klang, Roland Kuhn, and Vojin Jovanovic著 + +## 简介 + +Future提供了一套高效便捷的非阻塞并行操作管理方案。其基本思想很简单,所谓Future,指的是一类占位符对象,用于指代某些尚未完成的计算的结果。一般来说,由Future指代的计算都是并行执行的,计算完毕后可另行获取相关计算结果。以这种方式组织并行任务,便可以写出高效、异步、非阻塞的并行代码。 + +默认情况下,future和promise并不采用一般的阻塞操作,而是依赖回调进行非阻塞操作。为了在语法和概念层面更加简明扼要地使用这些回调,Scala还提供了flatMap、foreach和filter等算子,使得我们能够以非阻塞的方式对future进行组合。当然,future仍然支持阻塞操作——必要时,可以阻塞等待future(不过并不鼓励这样做)。 + +## Future + +所谓Future,是一种用于指代某个尚未就绪的值的对象。而这个值,往往是某个计算过程的结果: + +- 若该计算过程尚未完成,我们就说该Future未就位; +- 若该计算过程正常结束,或中途抛出异常,我们就说该Future已就位。 + +Future的就位分为两种情况: + +- 当Future带着某个值就位时,我们就说该Future携带计算结果成功就位。 +- 当Future因对应计算过程抛出异常而就绪,我们就说这个Future因该异常而失败。 + +Future的一个重要属性在于它只能被赋值一次。一旦给定了某个值或某个异常,future对象就变成了不可变对象——无法再被改写。 + +创建future对象最简单的方法是调用future方法,该future方法启用异步(asynchronous)计算并返回保存有计算结果的futrue,一旦该future对象计算完成,其结果就变的可用。 + +注意_Future[T]_ 是表示future对象的类型,而future是方法,该方法创建和调度一个异步计算,并返回随着计算结果而完成的future对象。 + +这最好通过一个例子予以说明。 + +假设我们使用某些流行的社交网络的假定API获取某个用户的朋友列表,我们将打开一个新对话(session),然后发送一个请求来获取某个特定用户的好友列表。 + + import scala.concurrent._ + import ExecutionContext.Implicits.global + + val session = socialNetwork.createSessionFor("user", credentials) + val session = socialNetwork.createSessionFor("user", credentials) + session.getFriends() + } + +以上,首先导入scala.concurrent 包使得Future类型和future构造函数可见。我们将马上解释第二个导入。 + +然后我们初始化一个session变量来用作向服务器发送请求,用一个假想的 createSessionFor 方法来返回一个List[Friend]。为了获得朋友列表,我们必须通过网络发送一个请求,这个请求可能耗时很长。这能从调用getFriends方法得到解释。为了更好的利用CPU,响应到达前不应该阻塞(block)程序的其他部分执行,于是在计算中使用异步。future方法就是这样做的,它并行地执行指定的计算块,在这个例子中是向服务器发送请求和等待响应。 + +一旦服务器响应,future f 中的好友列表将变得可用。 + +未成功的尝试可能会导致一个异常(exception)。在下面的例子中,session的值未被正确的初始化,于是在future的计算中将抛出NullPointerException,future f 不会圆满完成,而是以此异常失败。 + + val session = null + val session = socialNetwork.createSessionFor("user", credentials) + session.getFriends + } + +`import ExecutionContext.Implicits.global` 上面的线条导入默认的全局执行上下文(global execution context),执行上下文执行执行提交给他们的任务,也可把执行上下文看作线程池,这对于future方法来说是必不可少的,因为这可以处理异步计算如何及何时被执行。我们可以定义自己的执行上下文,并在future上使用它,但是现在只需要知道你能够通过上面的语句导入默认执行上下文就足够了。 + +我们的例子是基于一个假定的社交网络API,此API的计算包含发送网络请求和等待响应。提供一个涉及到你能试着立即使用的异步计算的例子是公平的。假设你有一个文本文件,你想找出一个特定的关键字第一次出现的位置。当磁盘正在检索此文件内容时,这种计算可能会陷入阻塞,因此并行的执行该操作和程序的其他部分是合理的(make sense)。 + + val firstOccurrence: Future[Int] = future { + val source = scala.io.Source.fromFile("myText.txt") + source.toSeq.indexOfSlice("myKeyword") + } + +### Callbacks(回调函数) + +现在我们知道如何开始一个异步计算来创建一个新的future值,但是我们没有展示一旦此结果变得可用后如何来使用,以便我们能够用它来做一些有用的事。我们经常对计算结果感兴趣而不仅仅是它的副作用。 + +在许多future的实现中,一旦future的client对future的结果感兴趣,它不得不阻塞它自己的计算直到future完成——然后才能使用future的值继续它自己的计算。虽然这在Scala的Future API(在后面会展示)中是允许的,但是从性能的角度来看更好的办法是一种完全非阻塞的方法,即在future中注册一个回调,future完成后这个回调称为异步回调。如果当注册回调时future已经完成,则回调可能是异步执行的,或在相同的线程中循序执行。 + +注册回调最通常的形式是使用OnComplete方法,即创建一个`Try[T] => U`类型的回调函数。如果future成功完成,回调则会应用到Success[T]类型的值中,否则应用到` Failure[T] `类型的值中。 + + `Try[T]` 和`Option[T]`或 `Either[T, S]`相似,因为它是一个可能持有某种类型值的单子。然而,它是特意设计来保持一个值或某个可抛出(throwable)对象。`Option[T]` 既可以是一个值(如:`Some[T]`)也可以是完全无值(如:`None`),如果`Try[T]`获得一个值则它为`Success[T]` ,否则为`Failure[T]`的异常。 `Failure[T]` 获得更多的关于为什么这儿没值的信息,而不仅仅是None。同时也可以把`Try[T]`看作一种特殊版本的`Either[Throwable, T]`,专门用于左值为可抛出类型(Throwable)的情形。 + +回到我们的社交网络的例子,假设我们想要获取我们最近的帖子并显示在屏幕上,我们通过调用getRecentPosts方法获得一个返回值List[String]——一个近期帖子的列表文本: + + val f: Future[List[String]] = future { + session.getRecentPosts + } + + f onComplete { + case Success(posts) => for (post <- posts) println(post) + case Success(posts) => for (post <- posts) println(post) + } + +onComplete方法一般在某种意义上它允许客户处理future计算出的成功或失败的结果。对于仅仅处理成功的结果,onSuccess 回调使用如下(该回调以一个偏函数(partial function)为参数): + + val f: Future[List[String]] = future { + session.getRecentPosts + } + + f onSuccess { + case posts => for (post <- posts) println(post) + } + +对于处理失败结果,onFailure回调使用如下: + + val f: Future[List[String]] = future { + session.getRecentPosts + } + + f onFailure { + case t => println("An error has occured: " + t.getMessage) + } + + f onSuccess { + case posts => for (post <- posts) println(post) + } + +如果future失败,即future抛出异常,则执行onFailure回调。 + +因为偏函数具有 isDefinedAt方法, onFailure方法只有在特定的Throwable类型对象中被定义才会触发。下面例子中的onFailure回调永远不会被触发: + + val f = future { + 2 / 0 + } + + f onFailure { + case npe: NullPointerException => + println("I'd be amazed if this printed out.") + } + +回到前面查找某个关键字第一次出现的例子,我们想要在屏幕上打印出此关键字的位置: + + val firstOccurrence: Future[Int] = future { + val source = scala.io.Source.fromFile("myText.txt") + source.toSeq.indexOfSlice("myKeyword") + } + + firstOccurrence onSuccess { + case idx => println("The keyword first appears at position: " + idx) + } + + firstOccurrence onFailure { + case t => println("Could not process file: " + t.getMessage) + } + + onComplete,、onSuccess 和 onFailure 方法都具有Unit的结果类型,这意味着不能链接使用这些方法的回调。注意这种设计是为了避免暗示而刻意为之的,因为链接回调也许暗示着按照一定的顺序执行注册回调(回调注册在同一个future中是无序的)。 + +也就是说,我们现在应讨论论何时调用callback。因为callback需要future的值是可用的,所有回调只能在future完成之后被调用。然而,不能保证callback在完成future的线程或创建callback的线程中被调用。反而, 回调(callback)会在future对象完成之后的一些线程和一段时间内执行。所以我们说回调(callback)最终会被执行。 + +此外,回调(callback)执行的顺序不是预先定义的,甚至在相同的应用程序中callback的执行顺序也不尽相同。事实上,callback也许不是一个接一个连续的调用,但是可能会在同一时间同时执行。这意味着在下面的例子中,变量totalA也许不能在计算上下文中被设置为正确的大写或者小写字母。 + + @volatile var totalA = 0 + + val text = future { + "na" * 16 + "BATMAN!!!" + } + + text onSuccess { + case txt => totalA += txt.count(_ == 'a') + } + + text onSuccess { + case txt => totalA += txt.count(_ == 'a') + } + +以上,这两个回调(callbacks)可能是一个接一个地执行的,这样变量totalA得到的预期值为18。然而,它们也可能是并发执行的,于是totalA最终可能是16或2,因为+= 是一个不可分割的操作符(即它是由一个读和一个写的步骤组成,这样就可能使其与其他的读和写任意交错执行)。 + +考虑到完整性,回调的使用情景列在这儿: + +- 在future中注册onComplete回调的时候要确保最后future执行完成之后调用相应的终止回调。 + +- 注册onSuccess或者onFailure回调时也和注册onComplete一样,不同之处在于future执行成功或失败分别调用onSuccess或onSuccess的对应的闭包。 + +- 注册一个已经完成的future的回调最后将导致此回调一直处于执行状态(1所隐含的)。 + +- 在future中注册多个回调的情况下,这些回调的执行顺序是不确定的。事实上,这些回调也许是同时执行的,然而,特定的ExecutionContext执行可能导致明确的顺序。 + +- 在一些回调抛出异常的情况下,其他的回调的执行不受影响。 + +- 在一些情况下,回调函数永远不能结束(例如,这些回调处于无限循环中),其他回调可能完全不会执行。在这种情况下,对于那些潜在的阻塞回调要使用阻塞的构造(例子如下)。 + +- 一旦执行完,回调将从future对象中移除,这样更适合JVM的垃圾回收机制(GC)。 + +### 函数组合(Functional Composition)和For解构(For-Comprehensions) + +尽管前文所展示的回调机制已经足够把future的结果和后继计算结合起来的,但是有些时候回调机制并不易于使用,且容易造成冗余的代码。我们可以通过一个例子来说明。假设我们有一个用于进行货币交易服务的API,我们想要在有盈利的时候购进一些美元。让我们先来看看怎样用回调来解决这个问题: + + val rateQuote = future { + connection.getCurrentValue(USD) + } + + rateQuote onSuccess { case quote => + val purchase = future { + if (isProfitable(quote)) connection.buy(amount, quote) + else throw new Exception("not profitable") + } + + purchase onSuccess { + case _ => println("Purchased " + amount + " USD") + } + } + +首先,我们创建一个名为rateQuote的future对象并获得当前的汇率。在服务器返回了汇率且该future对象成功完成了之后,计算操作才会从onSuccess回调中执行,这时我们就可以开始判断买还是不买了。所以我们创建了另一个名为purchase的future对象,用来在可盈利的情况下做出购买决定,并在稍后发送一个请求。最后,一旦purchase运行结束,我们会在标准输出中打印一条通知消息。 + +这确实是可行的,但是有两点原因使这种做法并不方便。其一,我们不得不使用onSuccess,且不得不在其中嵌套purchase future对象。试想一下,如果在purchase执行完成之后我们可能会想要卖掉一些其他的货币。这时我们将不得不在onSuccess的回调中重复这个模式,从而可能使代码过度嵌套,过于冗长,并且难以理解。 + +其二,purchase只是定义在局部范围内--它只能被来自onSuccess内部的回调响应。这也就是说,这个应用的其他部分看不到purchase,而且不能为它注册其他的onSuccess回调,比如说卖掉些别的货币。 + +为解决上述的两个问题,futures提供了组合器(combinators)来使之具有更多易用的组合形式。映射(map)是最基本的组合器之一。试想给定一个future对象和一个通过映射来获得该future值的函数,映射方法将创建一个新Future对象,一旦原来的Future成功完成了计算操作,新的Future会通过该返回值来完成自己的计算。你能够像理解容器(collections)的map一样来理解future的map。 + +让我们用map的方法来重构一下前面的例子: + + val rateQuote = future { + connection.getCurrentValue(USD) + } + + val purchase = rateQuote map { quote => + if (isProfitable(quote)) connection.buy(amount, quote) + else throw new Exception("not profitable") + } + + purchase onSuccess { + case _ => println("Purchased " + amount + " USD") + } + +通过对rateQuote的映射我们减少了一次onSuccess的回调,更重要的是避免了嵌套。这时如果我们决定出售一些货币就可以再次使用purchase方法上的映射了。 + +可是如果isProfitable方法返回了false将会发生些什么?会引发异常?这种情况下,purchase的确会因为异常而失败。不仅仅如此,想象一下,链接的中断和getCurrentValue方法抛出异常会使rateQuote的操作失败。在这些情况下映射将不会返回任何值,而purchase也会会自动的以和rateQuote相同的异常而执行失败。 + +总之,如果原Future的计算成功完成了,那么返回的Future将会使用原Future的映射值来完成计算。如果映射函数抛出了异常则Future也会带着该异常完成计算。如果原Future由于异常而计算失败,那么返回的Future也会包含相同的异常。这种异常的传导方式也同样适用于其他的组合器(combinators)。 + +使之能够在For-comprehensions原则下使用,是设计Future的目的之一。也正是因为这个原因,Future还拥有flatMap,filter和foreach等组合器。其中flatMap方法可以构造一个函数,它可以把值映射到一个姑且称为g的新future,然后返回一个随g的完成而完成的Future对象。 + +让我们假设我们想把一些美元兑换成瑞士法郎。我们必须为这两种货币报价,然后再在这两个报价的基础上确定交易。下面是一个在for-comprehensions中使用flatMap和withFilter的例子: + + val usdQuote = future { connection.getCurrentValue(USD) } + val chfQuote = future { connection.getCurrentValue(CHF) } + + val purchase = for { + usd <- usdQuote + chf <- chfQuote + if isProfitable(usd, chf) + } yield connection.buy(amount, chf) + + purchase onSuccess { + case _ => println("Purchased " + amount + " CHF") + } + +purchase只有当usdQuote和chfQuote都完成计算以后才能完成-- 它以其他两个Future的计算值为前提所以它自己的计算不能更早的开始。 + +上面的for-comprhension将被转换为: + + val purchase = usdQuote flatMap { + usd => + chfQuote + .withFilter(chf => isProfitable(usd, chf)) + .map(chf => connection.buy(amount, chf)) + } + +这的确是比for-comprehension稍微难以把握一些,但是我们这样分析有助于您更容易的理解flatMap的操作。FlatMap操作会把自身的值映射到其他future对象上,并随着该对象计算完成的返回值一起完成计算。在我们的例子里,flatMap用usdQuote的值把chfQuote的值映射到第三个futrue对象里,该对象用于发送一定量瑞士法郎的购入请求。只有当通过映射返回的第三个future对象完成了计算,purchase才能完成计算。 + +这可能有些难以置信,但幸运的是faltMap操作在for-comprhensions模式以外很少使用,因为for-comprehensions本身更容易理解和使用。 + +再说说filter,它可以用于创建一个新的future对象,该对象只有在满足某些特定条件的前提下才会得到原始future的计算值,否则就会抛出一个NoSuchElementException的异常而失败。调用了filter的future,其效果与直接调用withFilter完全一样。 + +作为组合器的collect同filter之间的关系有些类似容器(collections)API里的那些方法之间的关系。 + +值得注意的是,调用foreach组合器并不会在计算值可用的时候阻塞当前的进程去获取计算值。恰恰相反,只有当future对象成功计算完成了,foreach所迭代的函数才能够被异步的执行。这意味着foreach与onSuccess回调意义完全相同。 + +由于Future trait(译注: trait有点类似java中的接口(interface)的概念)从概念上看包含两种类型的返回值(计算结果和异常),所以组合器会有一个处理异常的需求。 + +比方说我们准备在rateQuote的基础上决定购入一定量的货币,那么`connection.buy`方法需要知道购入的数量和期望的报价值,最终完成购买的数量将会被返回。假如报价值偏偏在这个节骨眼儿改变了,那buy方法将会抛出一个`QuoteChangedExecption`,并且不会做任何交易。如果我们想让我们的Future对象返回0而不是抛出那个该死的异常,那我们需要使用recover组合器: + + val purchase: Future[Int] = rateQuote map { + quote => connection.buy(amount, quote) + } recover { + case QuoteChangedException() => 0 + } + +这里用到的recover能够创建一个新future对象,该对象当计算完成时持有和原future对象一样的值。如果执行不成功则偏函数的参数会被传递给使原Future失败的那个Throwable异常。如果它把Throwable映射到了某个值,那么新的Future就会成功完成并返回该值。如果偏函数没有定义在Throwable中,那么最终产生结果的future也会失败并返回同样的Throwable。 + +组合器recoverWith能够创建一个新future对象,当原future对象成功完成计算时,新future对象包含有和原future对象相同的计算结果。若原future失败或异常,偏函数将会返回造成原future失败的相同的Throwable异常。如果此时Throwable又被映射给了别的future,那么新Future就会完成并返回这个future的结果。recoverWith同recover的关系跟flatMap和map之间的关系很像。 + +fallbackTo组合器生成的future对象可以在该原future成功完成计算时返回结果,如果原future失败或异常返回future参数对象的成功值。在原future和参数future都失败的情况下,新future对象会完成并返回原future对象抛出的异常。正如下面的例子中,本想打印美元的汇率,但是在获取美元汇率失败的情况下会打印出瑞士法郎的汇率: + + val usdQuote = future { + connection.getCurrentValue(USD) + } map { + usd => "Value: " + usd + "$" + } + val chfQuote = future { + connection.getCurrentValue(CHF) + } map { + chf => "Value: " + chf + "CHF" + } + + al anyQuote = usdQuote fallbackTo chfQuote + + anyQuote onSuccess { println(_) } + +组合器andThen的用法是出于纯粹的side-effecting目的。经andThen返回的新Future无论原Future成功或失败都会返回与原Future一模一样的结果。一旦原Future完成并返回结果,andThen后跟的代码块就会被调用,且新Future将返回与原Future一样的结果,这确保了多个andThen调用的顺序执行。正如下例所示,这段代码可以从社交网站上把近期发出的帖子收集到一个可变集合里,然后把它们都打印在屏幕上: + + val allposts = mutable.Set[String]() + + future { + session.getRecentPosts + } andThen { + posts => allposts ++= posts + } andThen { + posts => + clearAll() + for (post <- allposts) render(post) + } + +综上所述,Future的组合器功能是纯函数式的,每种组合器都会返回一个与原Future相关的新Future对象。 + +### 投影(Projections) + +为了确保for解构(for-comprehensions)能够返回异常,futures也提供了投影(projections)。如果原future对象失败了,失败的投影(projection)会返回一个带有Throwable类型返回值的future对象。如果原Future成功了,失败的投影(projection)会抛出一个NoSuchElementException异常。下面就是一个在屏幕上打印出异常的例子: + + val f = future { + 2 / 0 + } + for (exc <- f.failed) println(exc) + +下面的例子不会在屏幕上打印出任何东西: + + val f = future { + 4 / 2 + } + for (exc <- f.failed) println(exc) + +### Future的扩展 + +用更多的实用方法来对Futures API进行扩展支持已经被提上了日程,这将为很多外部框架提供更多专业工具。 + +## Blocking + +正如前面所说的,在future的blocking非常有效地缓解性能和预防死锁。虽然在futures中使用这些功能方面的首选方式是Callbacks和combinators,但在某些处理中也会需要用到blocking,并且它也是被Futures and Promises API所支持的。 + +在之前的并发交易(concurrency trading)例子中,在应用的最后有一处用到block来确定是否所有的futures已经完成。这有个如何使用block来处理一个future结果的例子: + + import scala.concurrent._ + import scala.concurrent.duration._ + + def main(args: Array[String]) { + val rateQuote = future { + connection.getCurrentValue(USD) + } + + val purchase = rateQuote map { quote => + if (isProfitable(quote)) connection.buy(amount, quote) + else throw new Exception("not profitable") + } + + Await.result(purchase, 0 nanos) + } + +在这种情况下这个future是不成功的,这个调用者转发出了该future对象不成功的异常。它包含了失败的投影(projection)-- 阻塞(blocking)该结果将会造成一个NoSuchElementException异常在原future对象被成功计算的情况下被抛出。 + +相反的,调用`Await.ready`来等待这个future直到它已完成,但获不到它的结果。同样的方式,调用那个方法时如果这个future是失败的,它将不会抛出异常。 + +The Future trait实现了Awaitable trait还有其`ready()`和`result()`方法。这些方法不能被客户端直接调用,它们只能通过执行环境上下文来进行调用。 + +为了允许程序调用可能是阻塞式的第三方代码,而又不必实现Awaitable特质,原函数可以用如下的方式来调用: + + blocking { + potentiallyBlockingCall() + } + +这段blocking代码也可以抛出一个异常。在这种情况下,这个异常会转发给调用者。 + +## 异常(Exceptions) + +当异步计算抛出未处理的异常时,与那些计算相关的futures就失败了。失败的futures存储了一个Throwable的实例,而不是返回值。Futures提供onFailure回调方法,它用一个PartialFunction去表示一个Throwable。下列特殊异常的处理方式不同: + +`scala.runtime.NonLocalReturnControl[_]` --此异常保存了一个与返回相关联的值。通常情况下,在方法体中的返回结构被调用去抛出这个异常。相关联的值将会存储到future或一个promise中,而不是一直保存在这个异常中。 + +ExecutionException-当因为一个未处理的中断异常、错误或者`scala.util.control.ControlThrowable`导致计算失败时会被存储起来。这种情况下,ExecutionException会为此具有未处理的异常。这些异常会在执行失败的异步计算线程中重新抛出。这样做的目的,是为了防止正常情况下没有被客户端代码处理过的那些关键的、与控制流相关的异常继续传播下去,同时告知客户端其中的future对象是计算失败的。 + +更精确的语义描述请参见 [NonFatal]。 + +## Promises + +到目前为止,我们仅考虑了通过异步计算的方式创建future对象来使用future的方法。尽管如此,futures也可以使用promises来创建。 + +如果说futures是为了一个还没有存在的结果,而当成一种只读占位符的对象类型去创建,那么promise就被认为是一个可写的,可以实现一个future的单一赋值容器。这就是说,promise通过这种success方法可以成功去实现一个带有值的future。相反的,因为一个失败的promise通过failure方法就会实现一个带有异常的future。 + +一个promise p通过p.future方式返回future。 这个futrue对象被指定到promise p。根据这种实现方式,可能就会出现p.future与p相同的情况。 + +考虑下面的生产者 - 消费者的例子,其中一个计算产生一个值,并把它转移到另一个使用该值的计算。这个传递中的值通过一个promise来完成。 + + import scala.concurrent.{ future, promise } + import scala.concurrent.ExecutionContext.Implicits.global + + val p = promise[T] + val f = p.future + + val producer = future { + val r = produceSomething() + p success r + continueDoingSomethingUnrelated() + } + + val consumer = future { + startDoingSomething() + f onSuccess { + case r => doSomethingWithResult() + } + } + +在这里,我们创建了一个promise并利用它的future方法获得由它实现的Future。然后,我们开始了两种异步计算。第一种做了某些计算,结果值存放在r中,通过执行promise p,这个值被用来完成future对象f。第二种做了某些计算,然后读取实现了future f的计算结果值r。需要注意的是,在生产者完成执行`continueDoingSomethingUnrelated()` 方法这个任务之前,消费者可以获得这个结果值。 + +正如前面提到的,promises具有单赋值语义。因此,它们仅能被实现一次。在一个已经计算完成的promise或者failed的promise上调用success方法将会抛出一个IllegalStateException异常。 + +下面的这个例子显示了如何fail a promise。 + + val p = promise[T] + val f = p.future + + val producer = future { + val r = someComputation + if (isInvalid(r)) + p failure (new IllegalStateException) + else { + val q = doSomeMoreComputation(r) + p success q + } + } + +如上,生产者计算出一个中间结果值r,并判断它的有效性。如果它不是有效的,它会通过返回一个异常实现promise p的方式fails the promise,关联的future f是failed。否则,生产者会继续它的计算,最终使用一个有效的结果值实现future f,同时实现 promise p。 + +Promises也能通过一个complete方法来实现,这个方法采用了一个`potential value Try[T]`,这个值要么是一个类型为`Failure[Throwable]`的失败的结果值,要么是一个类型为`Success[T]`的成功的结果值。 + +类似success方法,在一个已经完成(completed)的promise对象上调用failure方法和complete方法同样会抛出一个IllegalStateException异常。 + +应用前面所述的promises和futures方法的一个优点是,这些方法是单一操作的并且是没有副作用(side-effects)的,因此程序是具有确定性的(deterministic)。确定性意味着,如果该程序没有抛出异常(future的计算值被获得),无论并行的程序如何调度,那么程序的结果将会永远是一样的。 + +在一些情况下,客户端也许希望能够只在promise没有完成的情况下完成该promise的计算(例如,如果有多个HTTP请求被多个不同的futures对象来执行,并且客户端只关心地一个HTTP应答(response),该应答对应于地一个完成该promise的future)。因为这个原因,future提供了tryComplete,trySuccess和tryFailure方法。客户端需要意识到调用这些的结果是不确定的,调用的结果将以来从程序执行的调度。 + +completeWith方法将用另外一个future完成promise计算。当该future结束的时候,该promise对象得到那个future对象同样的值,如下的程序将打印1: + + val f = future { 1 } + val p = promise[Int] + + p completeWith f + + p.future onSuccess { + case x => println(x) + } + +当让一个promise以异常失败的时候,三总子类型的Throwable异常被分别的处理。如果中断该promise的可抛出(Throwable)一场是`scala.runtime.NonLocalReturnControl`,那么该promise将以对应的值结束;如果是一个Error的实例,`InterruptedException`或者`scala.util.control.ControlThrowable`,那么该可抛出(Throwable)异常将会封装一个ExecutionException异常,该ExectionException将会让该promise以失败结束。 + +通过使用promises,futures的onComplete方法和future的构造方法,你能够实现前文描述的任何函数式组合组合器(compition combinators)。让我们来假设一下你想实现一个新的组合起,该组合器首先使用两个future对象f和,产生第三个future,该future能够用f或者g来完成,但是只在它能够成功完成的情况下。 + +这里有个关于如何去做的实例: + + def first[T](f: Future[T], g: Future[T]): Future[T] = { + val p = promise[T] + + f onSuccess { + case x => p.trySuccess(x) + } + + g onSuccess { + case x => p.trySuccess(x) + } + + p.future + } + +注意,在这种实现方式中,如果f与g都不是成功的,那么`first(f, g)`将不会实现(即返回一个值或者返回一个异常)。 + +## 工具(Utilities) + +为了简化在并发应用中处理时序(time)的问题,`scala.concurrent`引入了Duration抽象。Duration不是被作为另外一个通常的时间抽象存在的。他是为了用在并发(concurrency)库中使用的,Duration位于`scala.concurrent`包中。 + +Duration是表示时间长短的基础类,其可以是有限的或者无限的。有限的duration用FiniteDuration类来表示,并通过时间长度`(length)`和`java.util.concurrent.TimeUnit`来构造。无限的durations,同样扩展了Duration,只在两种情况下存在,`Duration.Inf`和`Duration.MinusInf`。库中同样提供了一些Durations的子类用来做隐式的转换,这些子类不应被直接使用。 + +抽象的Duration类包含了如下方法: + +到不同时间单位的转换`(toNanos, toMicros, toMillis, toSeconds, toMinutes, toHours, toDays and toUnit(unit: TimeUnit))`。 +durations的比较`(<,<=,>和>=)`。 +算术运算符`(+, -, *, / 和单值运算_-)` +duration的最大最小方法`(min,max)`。 +测试duration是否是无限的方法`(isFinite)`。 +Duration能够用如下方法实例化`(instantiated)`: + +隐式的通过Int和Long类型转换得来 `val d = 100 millis`。 +通过传递一个`Long length`和`java.util.concurrent.TimeUnit`。例如`val d = Duration(100, MILLISECONDS)`。 +通过传递一个字符串来表示时间区间,例如 `val d = Duration("1.2 µs")`。 +Duration也提供了unapply方法,因此可以i被用于模式匹配中,例如: + + import scala.concurrent.duration._ + import java.util.concurrent.TimeUnit._ + + // instantiation + val d1 = Duration(100, MILLISECONDS) // from Long and TimeUnit + val d2 = Duration(100, "millis") // from Long and String + val d3 = 100 millis // implicitly from Long, Int or Double + val d4 = Duration("1.2 µs") // from String + + // pattern matching + val Duration(length, unit) = 5 millis + diff --git a/cn/overviews/core/Implicit-Classes.md b/cn/overviews/core/Implicit-Classes.md new file mode 100644 index 0000000000..6a84b6a982 --- /dev/null +++ b/cn/overviews/core/Implicit-Classes.md @@ -0,0 +1,83 @@ +--- +layout: overview-large +title: Implicit Classes + +disqus: true + +partof: core +num: 4 +languages: [cn] +--- + +Josh Suereth + +## 介绍 + +Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。 + +隐式类型是在[SIP-13](http://docs.scala-lang.org/sips/pending/implicit-classes.html)中提出的。 + +## 用法 + +创建隐式类时,只需要在对应的类前加上implicit关键字。比如: + + object Helpers { + implicit class IntWithTimes(x: Int) { + def times[A](f: => A): Unit = { + def loop(current: Int): Unit = + if(current > 0) { + f + loop(current - 1) + } + loop(x) + } + } + } + +这个例子创建了一个名为IntWithTimes的隐式类。这个类包含一个int值和一个名为times的方法。要使用这个类,只需将其导入作用域内并调用times方法。比如: + + scala> import Helpers._ + import Helpers._ + + scala> 5 times println("HI") + HI + HI + HI + HI + HI + +使用隐式类时,类名必须在当前作用域内可见且无歧义,这一要求与隐式值等其他隐式类型转换方式类似。 + +## 限制条件 + +隐式类有以下限制条件: + +1. 只能在别的trait/类/对象内部定义。 + +```` + object Helpers { + implicit class RichInt(x: Int) // 正确! + } + implicit class RichDouble(x: Double) // 错误! +```` + +2. 构造函数只能携带一个非隐式参数。 +```` + implicit class RichDate(date: java.util.Date) // 正确! + implicit class Indexer[T](collecton: Seq[T], index: Int) // 错误! + implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 正确! +```` + +虽然我们可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。 + +3. 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。 + +注意:这意味着隐式类不能是case class。 + + object Bar + implicit class Bar(x: Int) // 错误! + + val x = 5 + implicit class x(y: Int) // 错误! + + implicit case class Baz(x: Int) // 错误! diff --git a/cn/overviews/core/String_Interpolation.md b/cn/overviews/core/String_Interpolation.md new file mode 100644 index 0000000000..aeb2532f14 --- /dev/null +++ b/cn/overviews/core/String_Interpolation.md @@ -0,0 +1,121 @@ +--- +layout: overview-large +title: 字符串插值 + +disqus: true + +partof: core +num: 3 +languages: [cn] +--- + +Josh Suereth + +## 简介 + +自2.10.0版本开始,Scala提供了一种新的机制来根据数据生成字符串:字符串插值。字符串插值允许使用者将变量引用直接插入处理过的字面字符中。如下例: + + val name="James" + println(s"Hello,$name")//Hello,James + +在上例中, s"Hello,$name" 是待处理字符串字面,编译器会对它做额外的工作。待处理字符串字面通过“号前的字符来标示(例如:上例中是s)。字符串插值的实现细节在 [SIP-13](http://docs.scala-lang.org/sips/pending/string-interpolation.html) 中有全面介绍。 + +## 用法 + +Scala 提供了三种创新的字符串插值方法:s,f 和 raw. + +### s 字符串插值器 + +在任何字符串前加上s,就可以直接在串中使用变量了。你已经见过这个例子: + + val name="James" + println(s"Hello,$name")//Hello,James +此例中,$name嵌套在一个将被s字符串插值器处理的字符串中。插值器知道在这个字符串的这个地方应该插入这个name变量的值,以使输出字符串为Hello,James。使用s插值器,在这个字符串中可以使用任何在处理范围内的名字。 + +字符串插值器也可以处理任意的表达式。例如: + + println(s"1+1=${1+1}") +将会输出字符串1+1=2。任何表达式都可以嵌入到${}中。 + +### f 插值器 + +在任何字符串字面前加上 f,就可以生成简单的格式化串,功能相似于其他语言中的 printf 函数。当使用 f 插值器的时候,所有的变量引用都应当后跟一个printf-style格式的字符串,如%d。看下面这个例子: + + val height=1.9d + val name="James" + println(f"$name%s is $height%2.2f meters tall")//James is 1.90 meters tall +f 插值器是类型安全的。如果试图向只支持 int 的格式化串传入一个double 值,编译器则会报错。例如: + + val height:Double=1.9d + + scala>f"$height%4d" + :9: error: type mismatch; + found : Double + required: Int + f"$height%4d" + ^ +f 插值器利用了java中的字符串数据格式。这种以%开头的格式在 [Formatter javadoc] 中有相关概述。如果在具体变量后没有%,则格式化程序默认使用 %s(串型)格式。 + +### raw 插值器 + +除了对字面值中的字符不做编码外,raw 插值器与 s 插值器在功能上是相同的。如下是个被处理过的字符串: + + scala>s"a\nb" + res0:String= + a + b +这里,s 插值器用回车代替了\n。而raw插值器却不会如此处理。 + + scala>raw"a\nb" + res1:String=a\nb +当不想输入\n被转换为回车的时候,raw 插值器是非常实用的。 + +除了以上三种字符串插值器外,使用者可以自定义插值器。 + +### 高级用法 + +在Scala中,所有处理过的字符串字面值都进行了简单编码转换。任何时候编译器遇到一个如下形式的字符串字面值: + + id"string content" +它都会被转换成一个StringContext实例的call(id)方法。这个方法在隐式范围内仍可用。只需要简单得 +建立一个隐类,给StringContext实例增加一个新方法,便可以定义我们自己的字符串插值器。如下例: + + //注意:为了避免运行时实例化,我们从AnyVal中继承。 + //更多信息请见值类的说明 + implicit class JsonHelper(val sc:StringContext) extends AnyVal{ + def json(args:Any*):JSONObject=sys.error("TODO-IMPLEMENT") + } + + def giveMeSomeJson(x:JSONObject):Unit=... + + giveMeSomeJson(json"{name:$name,id:$id}") +在这个例子中,我们试图通过字符串插值生成一个JSON文本语法。隐类 JsonHelper 作用域内使用该语法,且这个JSON方法需要一个完整的实现。只不过,字符串字面值格式化的结果不是一个字符串,而是一个JSON对象。 + +当编译器遇到"{name:$name,id:$id"}",它将会被重写成如下表达式: + + new StringContext("{name:",",id:","}").json(name,id) + +隐类则被重写成如下形式 + + new JsonHelper(new StringContext("{name:",",id:","}")).json(name,id) + +所以,JSON方法可以访问字符串的原生片段而每个表达式都是一个值。这个方法的一个简单但又令人迷惑的例子: + + implicit class JsonHelper(val sc:StringContext) extends AnyVal{ + def json(args:Any*):JSONObject={ + val strings=sc.parts.iterator + val expressions=args.iterator + var buf=new StringBuffer(strings.next) + while(strings.hasNext){ + buf append expressions.next + buf append strings.next + } + parseJson(buf) + } + } + +被处理过的字符串的每部分都是StringContext的成员。每个表达式的值都将传入到JSON方法的args参数。JSON方法接受这些值并合成一个大字符串,然后再解析成JSON格式。有一种更复杂的实现可以避免合成字符串的操作,它只是简单的直接通过原生字符串和表达式值构建JSON。 + +## 限制 + +字符串插值目前对模式匹配语句不适用。此特性将在2.11版本中生效。 diff --git a/cn/overviews/core/The-Scala-Actors-Migration-Guide.md b/cn/overviews/core/The-Scala-Actors-Migration-Guide.md new file mode 100644 index 0000000000..14fd7a9aaf --- /dev/null +++ b/cn/overviews/core/The-Scala-Actors-Migration-Guide.md @@ -0,0 +1,467 @@ +--- +layout: overview-large +title: The-Scala-Actors-Migration-Guide + +disqus: true + +partof: core +num: 1 +languages: [cn] +--- + + +Vojin Jovanovic 和 Philipp Haller + +## 概述 + +从Scala的2.11.0版本开始,Scala的Actors库已经过时了。早在Scala2.10.0的时候,默认的actor库即是Akka。 + +为了方便的将Scala Actors迁移到Akka,我们提供了Actor迁移工具包(AMK)。通过在一个项目的类路径中添加scala-actors-migration.jar,AMK包含了一个针对Scala Actors扩展。此外,Akka 2.1包含一些特殊功能,比如ActorDSL singleton,可以实现更简单的转换功能,使Scala Actors代码变成Akka代码。本章内容的目的是用来指导用户完成迁移过程,并解释如何使用AMK。 + +本指南包括以下内容:在“迁移工具的局限性”章节中,我们在此概述了迁移工具的主要局限性。在“迁移概述”章节中我们描述了迁移过程和谈论了Scala的变化分布,使得迁移成为一种可能。最后,在“一步一步指导迁移到Akka”章节里,我们展示了一些迁移工作的例子,以及各个步骤,如果需要从Scala Actors迁移至Akka's actors,本节是推荐阅读的。 + +免责声明:并发代码是臭名昭著的,当出现bug时很难调试和修复。由于两个actor的不同实现,这种差异导致可能出现错误。迁移过程没一步后都建议进行完全的代码测试。 + +## 迁移工具的局限性 + +由于Akka和Scala的actor模型的完整功能不尽相同导致两者之间不能平滑地迁移。下面的列表解释了很难迁移的部分行为: + +1. 依靠终止原因和双向行为链接方法 - Scala和Akka actors有不同的故障处理和actor monitoring模型。在Scala actors模型中,如果一个相关联部分异常终止,相关联的actors终止。如果终止是显式跟踪(通过self.trapExit),actor可以从失败的actor收到终止的原因。通过Akka这个功能不能迁移到AMK。AMK允许迁移的只是[Akka monitoring](http://doc.akka.io/docs/akka/2.1.0/general/supervision.html#What_Lifecycle_Monitoring_Means)机制。Monitoring不同于连接,因为它是单向(unindirectional)的并且终止的原因是现在已知的。如果仅仅是monitoring机制是无法满足需求的,迁移的链接必须推迟到最后一刻(步骤5的迁移)。然后,当迁移到Akka,用户必须创建一个[监督层次(supervision hierarchy)](http://doc.akka.io/docs/akka/2.1.0/general/supervision.html),处理故障。 + +2. 使用restart方法——Akka不提供显式的重启actors,因此上述例子我们不能提供平滑迁移。用户必须更改系统,所以没有使用重启方法(restart method)。 + +3. 使用getState方法 - Akka actors没有显式状态,此功能无法迁移。用户代码必须没有getState调用。 + +4. 实例化后没有启动actors - Akka actors模型会在实例化后自动启动actors,所以用户不需要重塑系统来显式的在实例化后启动actors。 + +5. mailboxSize方法不存在Akka中,因此不能迁移。这种方法很少使用,很容易被删除。 + +## 迁移概述 + +### 迁移工具 + +在Scal 2.10.0 actors 是在[Scala distribution](http://www.scala-lang.org/downloads)中作为一个单独包(scala-actors.jar)存在的,并且他们的接口已被弃用。这种分布也包含在Akka actors的akka-actor.jar里。AMK同时存在Scala actors 和 akka-actor.jar之中。未来的主要版本的Scala将不包含Scala actors和AMK。 + +开始迁移,用户需要添加scala-actors.jar和scala-actors-migration.jar来构建他们的项目。添加scala-actors.jar和scala-actors-migration.jar允许使用下面描述的AMK。这些jar位于[Scala Tools](https://oss.sonatype.org/content/groups/scala-tools/org/scala-lang/)库和[Scala distribution](http://www.scala-lang.org/downloads)库中。 + +### 一步一步来迁移 + +Actor迁移工具使用起来应该有5步骤。每一步都设计为引入的基于代码的最小变化。在前四个迁移步骤的代码中将使用Scala actors来实现,并在该步完成后运行所有的系统测试。然而,方法和类的签名将被转换为与Akka相似。迁移工具在Scal方面引入了一种新的actor类型(ActWithStash)和强制执行actors的ActorRef接口。 + +该结果同样强制通过一个特殊的方法在ActorDSL 对象上创建actors。在这些步骤可以每次迁移一个actor。这降低了在同一时刻引入多个bug的可能性,同样降低了bug的复杂程度。 + +在Scala方面迁移完成后,用户应该改变import语句并变成使用Akka库。在Akka方面,ActorDSL和ActWithStash允许对Scala Actors和他们的生态系的react construct进行建模。这个步骤迁移所有actors到Akka的后端,会在系统中引入bug。一旦代码迁移到Akka,用户将能够使用Akka的所有的功能的。 + +### 一步一步指导迁移到Akka + +在这一章中,我们将通过actor迁移的5个步骤。在每一步之后的代码都要为可能的错误进行检测。在前4个步骤中可以一边迁移一个actor和一边测试功能。然而,最后一步迁移所有actors到Akka后它只能作为一个整体进行测试。在这个步骤之后系统应该具有和之前一样相同的功能,不过它将使用Akka actor库。 + +### 步骤1——万物皆是Actor + +Scala actors库提供了公共访问多个类型的actors。他们被组织在类层次结构和每个子类提供了稍微更丰富的功能。为了进一步的使迁移步骤更容易,我们将首先更改Actor类型系统中的每一个actor。这种迁移步骤很简单,因为Actor类位于层次结构的底部,并提供了广泛的功能。 + +来自Scala库的Actors应根据以下规则进行迁移: + +1. class MyServ extends Reactor[T] -> class MyServ extends Actor + +注意,反应器提供了一个额外的类型参数代表了类型的消息收到。如果用户代码中使用这些信息,那么一个需要:i)应用模式匹配与显式类型,或者ii)做一个向下的消息来自任何泛型T。 + +1. class MyServ extends ReplyReactor -> class MyServ extends Actor + +2. class MyServ extends DaemonActor -> class MyServ extends Actor + +为了为DaemonActor提供配对功能,将下列代码添加到类的定义。 + + override def scheduler: IScheduler = DaemonScheduler + +### 步骤2 - 实例化 + +在Akka中,actors可以访问只有通过ActorRef接口。ActorRef的实例可以通过在ActorDSL对象上调用actor方法或者通过调用ActorRefFactory实例的actorOf方法来获得。在Scala的AMK工具包中,我们提供了Akka ActorRef和ActorDSL的一个子集,该子集实际上是Akka库的一个单例对象(singleton object)。 + +这一步的迁移使所有actors访问通过ActorRefs。首先,我们现实如何迁移普通模式的实例化Sacla Actors。然后,我们将展示如何分别克服问题的ActorRef和Actor的不同接口。 + +#### Actor实例化 + +actor实例的转换规则(以下规则需要import scala.actors.migration._): + +1. 构造器调用实例化 + + val myActor = new MyActor(arg1, arg2) + myActor.start() + +应该被替换 + + ActorDSL.actor(new MyActor(arg1, arg2)) + +2. 用于创建Actors的DSL(译注:领域专用语言(Domain Specific Language)) + + val myActor = actor { + // actor 定义 + } +应该被替换 + + val myActor = ActorDSL.actor(new Actor { + def act() { + // actor 定义 + } + }) + +3. 从Actor Trait扩展来的对象 + + object MyActor extends Actor { + // MyActor 定义 + } + MyActor.start() +应该被替换 + + class MyActor extends Actor { + // MyActor 定义 + } + + object MyActor { + val ref = ActorDSL.actor(new MyActor) + } +所有的MyActor地想都应该被替换成MyActor.ref。 + +需要注意的是Akka actors在实例化的同时开始运行。actors创建并开始在迁移的系统的情况下,actors在不同的位置以及改变这可能会影响系统的行为,用户需要更改代码,以使得actors在实例化后立即开始执行。 + +远程actors也需要被获取作为ActorRefs。为了得到一个远程actor ActorRef需使用方法selectActorRef。 + +#### 不同的方法签名(signatures) + +至此为止我们已经改变了所有的actor实例化,返回ActorRefs,然而,我们还没有完成迁移工作。有不同的接口在ActorRefs和Actors中,因此我们需要改变在每个迁移实例上触发的方法。不幸的是,Scala Actors提供的一些方法不能迁移。对下列方法的用户需要找到一个解决方案: + +1. getState()——Akka中的actors 默认情况下由其监管actors(supervising actors)负责管理和重启。在这种情况下,一个actor的状态是不相关的。 + +2. restart() - 显式的重启一个Scala actor。在Akka中没有相应的功能。 + +所有其他Actor方法需要转换为两个ActorRef中的方法。转换是通过下面描述的规则。请注意,所有的规则需要导入以下内容: + + import scala.concurrent.duration._ + import scala.actors.migration.pattern.ask + import scala.actors.migration._ + import scala.concurrent._ +额外规则1-3的作用域定义在无限的时间需要一个隐含的超时。然而,由于Akka不允许无限超时,我们会使用100年。例如: + + implicit val timeout = Timeout(36500 days) + +规则: + +1. !!(msg: Any): Future[Any] 被?替换。这条规则会改变一个返回类型到scala.concurrent.Future这可能导致类型不匹配。由于scala.concurrent.Future比过去的返回值具有更广泛的功能,这种类型的错误可以很容易地固定在与本地修改: + + actor !! message -> respActor ? message + +2. !![A] (msg: Any, handler: PartialFunction[Any, A]): Future[A] 被?取代。处理程序可以提取作为一个单独的函数,并用来生成一个future对象结果。处理的结果应给出另一个future对象结果,就像在下面的例子: + + val handler: PartialFunction[Any, T] = ... // handler + actor !! (message, handler) -> (respActor ? message) map handler + +3. !? (msg: Any):任何被?替换都将阻塞在返回的future对象上 + + actor !? message -> + Await.result(respActor ? message, Duration.Inf) + +4. !? (msec: Long, msg: Any): Option[Any]任何被?替换都将显式的阻塞在future对象 + + actor !? (dur, message) -> + val res = respActor.?(message)(Timeout(dur milliseconds)) + val optFut = res map (Some(_)) recover { case _ => None } + Await.result(optFut, Duration.Inf) + +这里没有提到的公共方法是为了actors DSL被申明为公共的。他们只能在定义actor时使用,所以他们的这一步迁移是不相关的。 + +###第3步 - 从Actor 到 ActWithStash + +到目前为止,所有的控制器都继承自Actor trait。我们通过指定的工厂方法来实例化控制器,所有的控制器都可以通过接口ActorRef 来进行访问。现在我们需要把所有的控制器迁移的AMK 的 ActWithStash 类上。这个类的行为方式和Scala的Actor几乎完全一致,它提供了另外一些方法,对应于Akka的Actor trait。这使得控制器更易于逐步的迁移到Akka。 + +为了达到这个目的,所有的从Actor继承的类,按照下列的方式,需要改为继承自ActWithStash: + + class MyActor extends Actor -> class MyActor extends ActWithStash + +经过这样修改以后,代码会无法通过编译。因为ActWithStash中的receive 方法不能在act中像原来那样使用。要使代码通过编译,需要在所有的 receive 调用中加上类型参数。例如: + + receive { case x: Int => "Number" } -> + receive[String] { case x: Int => "Number" } + +另外,要使代码通过编译,还要在act方法前加上 override关键字,并且定义一个空的receive方法。act方法需要被重写,因为它在ActWithStash 的实现中模拟了Akka的消息处理循环。需要修改的地方请看下面的例子: + + class MyActor extends ActWithStash { + + // 空的 receive 方法 (现在还没有用) + def receive = {case _ => } + + override def act() { + // 原来代码中的 receive 方法改为 react。 + } + } +ActWithStash 的实例中,变量trapExit 的缺省值是true。如果希望改变,可以在初始化方法中把它设置为false。 + +远程控制器在ActWithStash 下无法直接使用,register('name, this)方法需要被替换为: + + registerActorRef('name, self) + +在后面的步骤中, registerActorRef 和 alive 方法的调用与其它方法一样。 + +现在,用户可以测试运行,整个系统的运行会和原来一样。ActWithStash 和Actor 拥有相同的基本架构,所以系统的运行会与原来没有什么区别。 + +### 第4步 - 去掉act 方法 + +在这一节,我们讨论怎样从ActWithStash中去掉act方法,以及怎样修改其他方法,使它与Akka更加契合. 这一环节会比较繁杂,所以我们建议最好一次只修改一个控制器。在Scala中,控制器的行为主要是在act方法的中定义。逻辑上来说,控制器是一个并发执行act方法的过程,执行完成后过程终止。在Akka中,控制器用一个全局消息处理器来依次处理它的的消息队列中的消息。这个消息处理器是一个receive函数返回的偏函数(partial function),该偏函数被应用与每一条消息上。 + +因为ActWithStash中Akka方法的行为依赖于移除的act方法,所以我们首先要做的是去掉act方法。然后,我们需要按照给定的规则修改scala.actors.Actor中每个方法的。 + +#### 怎样去除act 方法 + +在下面的列表中,我们给出了通用消息处理模式的修改规则。这个列表并不包含所有的模式,它只是覆盖了其中一些通用的模式。然而用户可以通过参考这些规则,通过扩展简单规则,将act方法移植到Akka。 + +嵌套调用react/reactWithin需要注意:消息处理偏函数需要做结构扩展,使它更接近Akka模式。尽管这种修改会很复杂,但是它允许任何层次的嵌套被移植。下面有相关的例子。 + +在复杂控制流中使用receive/receiveWithin需要注意:这个移植会比较复杂,因为它要求重构act方法。在消息处理偏函数中使用react 和 andThen可以使receive的调用模型化。下面是一些简单的例子。 + +1. 如果在act方法中有一些代码在第一个包含react的loop之前被执行,那么这些代码应该被放在preStart方法中。 + + def act() { + //初始化的代码放在这里 + loop { + react { ... } + } + } +应该被替换 + + override def preStart() { + //初始化的代码放在这里 + } + + def act() { + loop { + react{ ... } + } + } +其他的模式,如果在第一个react 之前有一些代码,也可以使用这个规则。 + +2. 当act 的形式为:一个简单loop循环嵌套react,用下面的方法。 + + def act() = { + loop { + react { + // body + } + } + } +应该被替换 + + def receive = { + // body + } + +3. 当act包含一个loopWhile 结构,用下面的方法。 + + def act() = { + loopWhile(c) { + react { + case x: Int => + // do task + if (x == 42) { + c = false + } + } + } + } +应该被替换 + + def receive = { + case x: Int => + // do task + if (x == 42) { + context.stop(self) + } + } + +4. 当act包含嵌套的react,用下面的规则: + + def act() = { + var c = true + loopWhile(c) { + react { + case x: Int => + // do task + if (x == 42) { + c = false + } else { + react { + case y: String => + // do nested task + } + } + } + } + } +应该被替换 + + def receive = { + case x: Int => + // do task + if (x == 42) { + context.stop(self) + } else { + context.become(({ + case y: String => + // do nested task + }: Receive).andThen(x => { + unstashAll() + context.unbecome() + }).orElse { case x => stash(x) }) + } + } + +5. reactWithin方法使用下面的修改规则: + + loop { + reactWithin(t) { + case TIMEOUT => // timeout processing code + case msg => // message processing code + } + } +应该被替换 + + import scala.concurrent.duration._ + + context.setReceiveTimeout(t millisecond) + def receive = { + case ReceiveTimeout => // timeout processing code + case msg => // message processing code + } + +6. 在Akka中,异常处理用另一种方式完成。如果要模拟Scala控制器的方式,那就用下面的方法 + + def act() = { + loop { + react { + case msg => + // 可能会失败的代码 + } + } + } + + override def exceptionHandler = { + case x: Exception => println("got exception") + } +应该被替换 + + def receive = PFCatch({ + case msg => + // 可能会失败的代码 + }, { case x: Exception => println("got exception") }) + PFCatch 的定义 + + class PFCatch(f: PartialFunction[Any, Unit], + handler: PartialFunction[Exception, Unit]) + extends PartialFunction[Any, Unit] { + + def apply(x: Any) = { + try { + f(x) + } catch { + case e: Exception if handler.isDefinedAt(e) => + handler(e) + } + } + + def isDefinedAt(x: Any) = f.isDefinedAt(x) + } + + object PFCatch { + def apply(f: PartialFunction[Any, Unit], + handler: PartialFunction[Exception, Unit]) = + new PFCatch(f, handler) + } + +PFCatch并不包含在AMK之中,所以它可以保留在移植代码中,AMK将会在下一版本中被删除。当整个移植完成后,错误处理也可以改由Akka来监管。 + +#### 修改Actor的方法 + +当我们移除了act方法以后,我们需要替换在Akka中不存在,但是有相似功能的方法。在下面的列表中,我们给出了两者的区别和替换方法: + +1. exit()/exit(reason) - 需要由 context.stop(self) 替换 + +2. receiver - 需要由 self 替换 + +3. reply(msg) - 需要由 sender ! msg 替换 + +4. link(actor) - 在Akka中,控制器之间的链接一部分由[supervision](http://doc.akka.io/docs/akka/2.1.0/general/supervision.html#What_Supervision_Means)来完成,一部分由[actor monitoring](http://doc.akka.io/docs/akka/2.1.0/general/supervision.html#What_Lifecycle_Monitoring_Means)来完成。在AMK中,我们只支持监测方法。因此,这部分Scala功能可以被完整的移植。 + +linking 和 watching 之间的区别在于:watching actor总是接受结束通知。然而,不像Scala的Exit消息包含结束的原因,Akka的watching 返回Terminated(a: ActorRef)消息,只包含ActorRef。获取结束原因的功能无法被移植。在Akka中,这一步骤可以在第4步之后,通过组织控制器的监管层级 [supervision hierarchy](http://doc.akka.io/docs/akka/2.1.0/general/supervision.html)来完成。 + +如果watching actors收到的消息不撇陪结束消息,控制器会被终止并抛出DeathPactException异常。注意就算watching actors正常的结束,也会发生这种情况。在Scala中,linked actors只要一方不正常的终止,另一方就会以相同的原因终止。 + +如果系统不能单独的用 watch actors来 移植,用户可以像原来那样用link和exit(reason)来使用。然而,因为act()重载了Exit消息,需要做如下的修改: + + case Exit(actor, reason) => + println("sorry about your " + reason) + ... +应该被替换 + + case t @ Terminated(actorRef) => + println("sorry about your " + t.reason) + ... +注意:在Scala和Akka的actor之间有另一种细微的区别:在Scala, link/watch 到已经终止的控制器不会有任何影响。在Akka中,看管已经终止的控制器会导致发送终止消息。这会在系统移植的第5 步导致不可预料的结果。 + +### 第5步 - Akka后端的移植 + +到目前为止,用户代码已经做好了移植到Akka actors的准备工作。现在我们可以把Scala actors迁移到Akka actor上。为了完成这一目标,需要配置build,去掉scala-actors.jar 和 scala-actors-migration.jar,把 akka-actor.jar 和 typesafe-config.jar加进来。AMK只能在Akka actor 2.1下正常工作,Akka actor 2.1已经包含在分发包 [Scala distribution](http://www.scala-lang.org/downloads)中, 可以用这样的方法配置。 + +经过这一步骤以后,因为包名的不同和API之间的细微差别,编译会失败。我们必须将每一个导入的actor从scala 修改为Akka。下列是部分需要修改的包名: + + scala.actors._ -> akka.actor._ + scala.actors.migration.ActWithStash -> akka.actor.ActorDSL._ + scala.actors.migration.pattern.ask -> akka.pattern.ask + scala.actors.migration.Timeout -> akka.util.Timeout + +当然,ActWithStash 中方法的声明 def receive = 必须加上前缀override。 + +在Scala actor中,stash 方法需要一个消息做为参数。例如: + + def receive = { + ... + case x => stash(x) + } + +在Akka中,只有当前处理的消息可以被隐藏(stashed)。因此,上面的例子可以替换为: + + def receive = { + ... + case x => stash() + } + +#### 添加Actor System + +Akka actor 组织在[Actor systems](http://doc.akka.io/docs/akka/2.1.0/general/actor-systems.html)系统中。每一个被实例化的actor必须属于某一个ActorSystem。因此,要添加一个ActorSystem 实例作为每个actor 实例调用的第一个参数。下面给出了例子。 + +为了完成该转换,你需要有一个actor system 实例。例如: + + val system = ActorSystem("migration-system") + +然后,做如下转换: + + ActorDSL.actor(...) -> ActorDSL.actor(system)(...) + +如果对actor 的调用都使用同一个ActorSystem ,那么它可以作为隐式参数来传递。例如: + + ActorDSL.actor(...) -> + import project.implicitActorSystem + ActorDSL.actor(...) + +当所有的主线程和actors结束后,Scala程序会终止。迁移到Akka后,当所有的主线程结束,所有的actor systems关闭后,程序才会结束。Actor systems 需要在程序退出前明确的中止。这需要通过在Actor system中调用shutdown 方法来完成。 + +#### 远程 Actors + +当代码迁移到Akka,远程actors就不再工作了。 registerActorFor 和 alive 方法需要被移除。 在Akka中,远程控制通过配置独立的完成。更多细节请参考[Akka remoting documentation](http://doc.akka.io/docs/akka/2.1.0/scala/remoting.html)。 + +#### 样例和问题 + +这篇文档中的所有程序片段可以在[Actors Migration test suite](http://github.com/scala/actors-migration/tree/master/src/test/)中找到,这些程序做为测试文件,前缀为actmig。 + +这篇文档和Actor移植组件由 [Vojin Jovanovic](http://people.epfl.ch/vojin.jovanovic)和[Philipp Haller](http://lampwww.epfl.ch/~phaller/)编写。 + +如果你发现任何问题或不完善的地方,请把它们报告给 [Scala Bugtracker](https://github.com/scala/actors-migration/issues)。 + diff --git a/cn/overviews/core/The_Architecture_of_Scala_Collections.md b/cn/overviews/core/The_Architecture_of_Scala_Collections.md new file mode 100644 index 0000000000..541da13c72 --- /dev/null +++ b/cn/overviews/core/The_Architecture_of_Scala_Collections.md @@ -0,0 +1,532 @@ +--- +layout: overview-large +title: scala容器类体系结构 + +disqus: true + +partof: core +num: 6 +languages: [cn] +--- + +Martin Odersky & Lex Spoon + +本篇详细的介绍了Scala 容器类(collections)框架。通过与 [Scala 2.8 的 Collection API](http://docs.scala-lang.org/overviews/collections/introduction.html) 的对比,你会了解到更多框架的内部运作方式,同时你也将学习到如何通过几行代码复用这个容器类框架的功能来定义自己的容器类。 + +[Scala 2.8 容器API](http://docs.scala-lang.org/overviews/collections/introduction.html) 中包含了大量的 容器(collection)操作,这些操作在不同的许多容器类上表现为一致。假设,为每种 Collection 类型都用不同的方法代码实现,那么将导致代码的异常臃肿,很多代码将会仅仅是别处代码的拷贝。随着时间的推移,这些重复的代码也会带来不一致的问题,试想,相同的代码,在某个地方被修改了,而另外的地方却被遗漏了。而新的 容器类(collections)框架的设计原则目标就是尽量的避免重复,在尽可能少的地方定义操作(理想情况下,只在一处定义,当然也会有例外的情况存在)。设计中使用的方法是,在 Collection 模板中实现大部分的操作,这样就可以灵活的从独立的基类和实现中继承。后面的部分,我们会来详细阐述框架的各组成部分:模板(templates)、类(classes)以及trait(译注:类似于java里接口的概念),也会说明他们所支持的构建原则。 + +## Builders + +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] = ... + } +几乎所有的 Collection 操作都由遍历器(traversals)和构建器 (builders)来完成。Traversal 用可遍历类的foreach方法来实现,而构建新的 容器(collections)是由构建器类的实例来完成。上面的代码就是对这个类的精简描述。 + +我们用 b += x 来表示为构建器 b 加上元素 x。也可以一次加上多个元素,例如: b += (x, y) 及 b ++= x ,这类似于缓存(buffers)的工作方式(实际上,缓存就是构建器的增强版)。构建器的 result() 方法会返回一个collection。在获取了结果之后,构建器的状态就变成未定义,调用它的 clear() 方法可以把状态重置成空状态。构建器是通用元素类型,它适用于元素,类型,及它所返回的Collection。 + +通常,一个builder可以使用其他的builder来组合一个容器的元素,但是如果想要把其他builder返回的结果进行转换,例如,转成另一种类型,就需要使用Builder类的mapResult方法。假设,你有一个数组buffer,名叫 buf。一个ArrayBuffer的builder 的 result() 返回它自身。如果想用它去创建一个新的ArrayBuffer的builder,就可以使用 mapResult : + + scala> val buf = new ArrayBuffer[Int] + buf: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer() + + scala> val bldr = buf mapResult (_.toArray) + bldr: scala.collection.mutable.Builder[Int,Array[Int]] + = ArrayBuffer() + +结果值 bldr,是使用 buf 来收集元素的builder。当调用 bldr 的result时,其实是调用的 buf 的result,结果是返回的buf本身。接着这个数组buffer用 _.toArray 映射成了一个数组,结果 bldr 也就成了一个数组的 builder. + +## 分解(factoring out)通用操作 + +### TraversableLike类概述 + + package scala.collection + + class TraversableLike[+Elem, +Repr] { + def newBuilder: Builder[Elem, Repr] // deferred + def foreach[U](f: Elem => U) // deferred + ... + def filter(p: Elem => Boolean): Repr = { + val b = newBuilder + foreach { elem => if (p(elem)) b += elem } + b.result + } + } + +Collection库重构的主要设计目标是在拥有自然类型的同时又尽可能的共享代码实现。Scala的Collection 遵从“结果类型相同”的原则:只要可能,容器上的转换方法最后都会生成相同类型的Collection。例如,过滤操作对各种Collection类型都应该产生相同类型的实例。在List上应用过滤器应该获得List,在Map上应用过滤器,应该获得Map,如此等等。在下面的章节中,会告诉大家该原则的实现方法。 + +Scala的 Collection 库通过在 trait 实现中使用通用的构建器(builders)和遍历器(traversals)来避免代码重复、实现“结果类型相同”的原则。这些Trait的名字都有Like后缀。例如:IndexedSeqLike 是 IndexedSeq 的 trait 实现,再如,TraversableLike 是 Traversable 的 trait 实现。和普通的 Collection 只有一个类型参数不同,trait实现有两个类型参数。他们不仅参数化了容器的成员类型,也参数化了 Collection 所代表的类型,就像下面的 Seq[I] 或 List[T]。下面是TraversableLike开头的描述: + + trait TraversableLike[+Elem, +Repr] { ... } + +类型参数Elem代表Traversable的元素类型,参数Repr代表它自身。Repr上没有限制,甚至,Repr可以是非Traversable子类的实例。这就意味这,非容器子类的类,例如String和Array也可以使用所有容器实现trait中包含的操作。 + +以过滤器为例,这个操作只在TraversableLike定义了一次,就使得它适用于所有的容器类(collections)。通过查看前面 TraversableLike类的概述中相关的代码描述,我们知道,该trait声明了两个抽象方法,newBuilder 和 foreach,这些抽象方法在具体的collection类中实现。过滤器也是用这两个方法,通过相同的方式来实现的。首先,它用 newBuiler 方法构造一个新的builder,类型为 Repr 的类型。然后,使用 foreach 来遍历当前 collection 中的所有元素。一旦某个元素 x 满足谓词 p (即,p(x)为真),那么就把x加入到builder中。最后,用 builder 的 result 方法返回类型同 Repr 的 collection,里面的元素就是上面收集到的满足条件的所有元素。 + +容器(collections)上的映射操作就更复杂。例如:如果 f 是一个以一个String类型为参数并返回一个Int类型的函数,xs 是一个 List[String],那么在xs上使用该映射函数 f 应该会返回 List[Int]。同样,如果 ys 是一个 Array[String],那么通过 f 映射,应该返回 Array[Int]。 这里的难点在于,如何实现这样的效果而又不用分别针对 list 和 array 写重复的代码。TraversableLike 类的 newBuilder/foreach 也完成不了这个任务,因为他们需要生成一个完全相同类型的容器(collection)类型,而映射需要的是生成一个相同类型但内部元素类型却不同的容器。 + +很多情况下,甚至像map的构造函数的结果类型都可能是不那么简单的,因而需要依靠于其他参数类型,比如下面的例子: + + scala> import collection.immutable.BitSet + import collection.immutable.BitSet + + scala> val bits = BitSet(1, 2, 3) + bits: scala.collection.immutable.BitSet = BitSet(1, 2, 3) + + scala> bits map (_ * 2) + res13: scala.collection.immutable.BitSet = BitSet(2, 4, 6) + + scala> bits map (_.toFloat) + res14: scala.collection.immutable.Set[Float] + = Set(1.0, 2.0, 3.0) + +在一个BitSet上使用倍乘映射 _*2,会得到另一个BitSet。然而,如果在相同的BitSet上使用映射函数 (_.toFloat) 结果会得到一个 Set[Float]。这样也很合理,因为 BitSet 中只能放整型,而不能存放浮点型。 + +因此,要提醒大家注意,映射(map)的结果类型是由传进来的方法的类型决定的。如果映射函数中的参数会得到Int类型的值,那么映射的结果就是 BitSet。但如果是其他类型,那么映射的结果就是 Set 类型。后面会让大家了解 Scala 这种灵活的类型适应是如何实现的。 + +类似 BitSet 的问题不是唯一的,这里还有在map类型上应用map函数的交互式例子: + + scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => (y, x) } + res3: scala.collection.immutable.Map[Int,java.lang.String] + = Map(1 -> a, 2 -> b) + + scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => y } + res4: scala.collection.immutable.Iterable[Int] + = List(1, 2) + +第一个函数用于交换两个键值对。这个函数映射的结果是一个类似的Map,键和值颠倒了。事实上,地一个表达式产生了一个键值颠倒的map类型(在原map可颠倒的情况下)。然而,第二个函数,把键值对映射成一个整型,即成员变成了具体的值。在这种情况下,我们不可能把结果转换成Map类型,因此处理成,把结果转换成Map的一个可遍历的超类,这里是List。 + +你可能会问,哪为什么不强制让映射都返回相同类型的Collection呢?例如:BitSet上的映射只能接受整型到整型的函数,而Map上的映射只能接受键值对到键值对的函数。但这种约束从面向对象的观点来看是不能接受的,它会破坏里氏替换原则(Liskov substitution principle),即:Map是可遍历类,因此所有在可遍历类上的合法的操作都必然在Map中合法。 + +Scala通过重载来解决这个问题:Scala中的重载并非简单的复制Java的实现(Java的实现不够灵活),它使用隐式参数所提供的更加系统化的重载方式。 + +TraversableLike 中映射(map)的实现: + + def map[B, That](p: Elem => B) + (implicit bf: CanBuildFrom[B, That, This]): That = { + val b = bf(this) + for (x <- this) b += f(x) + b.result + } + +上面的代码展示了TraversableLike如何实现映射的trait。看起来非常类似于TraversableLike类的过滤器的实现。主要的区别在于,过滤器使用TraversableLike类的抽象方法 newBuilder,而映射使用的是Builder工场,它作为CanBuildFrom类型的一个额外的隐式参数传入。 + +CanBuildFrom trait: + + package scala.collection.generic + + trait CanBuildFrom[-From, -Elem, +To] { + // 创建一个新的构造器(builder) + def apply(from: From): Builder[Elem, To] + } + +上面的代码是 trait CanBuildFrom 的定义,它代表着构建者工场。它有三个参数:Elem是要创建的容器(collection)的元素的类型,To是要构建的容器(collection)的类型,From是该构建器工场适用的类型。通过定义适合的隐式定义的构建器工场,你就可以构建出符合你需要的类型转换行为。以 BitSet 类为例,它的伴生对象包含一个 CanBuildFrom[BitSet, Int, BitSet] 类型的构建器工场。这就意味着,当在一个 BitSet 上执行操作的时候,你可以创建另一个元素类型为整型的 BitSet。如果你需要的类型不同,那么,你还可以使用其他的隐式构建器工场,它们在Set的伴生对象中实现。下面就是一个更通用的构建器,A是通用类型参数: + + CanBuildFrom[Set[_], A, Set[A]] + +这就意味着,当操作一个任意Set(用现有的类型 Set[] 表述),我们可以再次创建一个 Set,并且无需关心它的元素类型A是什么。给你两个 CanBuildFrom 的隐式实例,你有可以利用 Scala 的隐式解析(implicit resolution)规则去挑选出其中最契合的一个。 + +所以说,隐式解析(implicit resolution)为类似映射的比较棘手的Collection操作提供了正确的静态类型。但是动态类型又怎么办呢?特别是,假设你有一个List,作为静态类型它有遍历方法,你在它上面使用一些映射(map)方法: + + scala> val xs: Iterable[Int] = List(1, 2, 3) + xs: Iterable[Int] = List(1, 2, 3) + + scala> val ys = xs map (x => x * x) + ys: Iterable[Int] = List(1, 4, 9) + +上述ys的静态类型是可遍历的(Iterable)类型。但是它的动态类型仍然必须是List类型的!此行为是间接被实现的。在CanBuildFrom的apply方法被作为参数传递源容器中。大多数的builder工厂仿制traversables(除建造工厂意外所有的叶子类型(leaf classes))将调用转发到集合的方法genericBuilder。反过来genericBuilder方法调用属于在定义它收集的建设者。所以Scala使用静态隐式解析,以解决map类型的限制问题,以及分派挑选对应于这些约束最佳的动态类型。 + +## 集成新容器 + +如果想要集成一个新的容器(Collection)类,以便受益于在正确类型上预定义的操作,需要做些什么呢?在下面几页中,将通过两个例子来进行演示。 + +### 集成序列(Sequence) + +RNA(核糖核酸)碱基(译者注:RNA链即很多不同RNA碱基的序列,RNA参考资料:http://zh.wikipedia.org/wiki/RNA): + + abstract class Base + case object A extends Base + case object T extends Base + case object G extends Base + case object U extends Base + + object Base { + val fromInt: Int => Base = Array(A, T, G, U) + val toInt: Base => Int = Map(A -> 0, T -> 1, G -> 2, U -> 3) + } + +假设需要为RNA链建立一个新的序列类型,这些RNA链是由碱基A(腺嘌呤)、T(胸腺嘧啶)、G(鸟嘌呤)、U(尿嘧啶)组成的序列。如上述列出的RNA碱基,很容易建立碱基的定义。 + +每个碱基都定义为一个具体对象(case object),该对象继承自一个共同的抽象类Base(碱基)。这个Base类具有一个伴生对象(companion object),该伴生对象定义了描述碱基和整数(0到3)之间映射的2个函数。可以从例子中看到,有两种不同的方式来使用容器(Collection)来实现这些函数。toInt函数通过一个从Base值到整数之间的映射(map)来实现。而它的逆函数fromInt则通过数组来实现。以上这些实现方法都基于一个事实,即“映射和数组都是函数”。因为他们都继承自Function1 trait。 + +下一步任务,便是为RNA链定义一个类。从概念上来看,一个RNA链就是一个简单的Seq[Base]。然而,RNA链可以很长,所以值的去花点时间来简化RNA链的表现形式。因为只有4种碱基,所以每个碱基可以通过2个比特位来区别。因此,在一个integer中,可以保存16个由2位比特标示的碱基。即构造一个Seq[Base]的特殊子类,并使用这种压缩的表示(packed representation)方式。 + +#### RNA链类的第一个版本 + + import collection.IndexedSeqLike + import collection.mutable.{Builder, ArrayBuffer} + import collection.generic.CanBuildFrom + + final class RNA1 private (val groups: Array[Int], + val length: Int) extends IndexedSeq[Base] { + + import RNA1._ + + def apply(idx: Int): Base = { + if (idx < 0 || length <= idx) + throw new IndexOutOfBoundsException + Base.fromInt(groups(idx / N) >> (idx % N * S) & M) + } + } + + object RNA1 { + + // 表示一组所需要的比特数 + private val S = 2 + + // 一个Int能够放入的组数 + private val N = 32 / S + + // 分离组的位掩码(bitmask) + private val M = (1 << S) - 1 + + def fromSeq(buf: Seq[Base]): RNA1 = { + 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 RNA1(groups, buf.length) + } + + def apply(bases: Base*) = fromSeq(bases) + } + +上面的RNA链类呈现出这个类的第一个版本,它将在以后被细化。类RNA1有一个构造函数,这个构造函数将int数组作为第一个参数。而这个数组包含打包压缩后的RNA数据,每个数组元素都有16个碱基,而最后一个元素则只有一部分有数据。第二个参数是长度,指定了数组中(和序列中)碱基的总数。RNA1类扩展了IndexedSeq[Base]。而IndexedSeq来自scala.collection.immutable,IndexedSeq定义了两个抽象方法:length和apply。这方法些需要在具体的子类中实现。类RNA1通过定义一个相同名字的参数字段来自动实现length。同时,通过类RNA1中给出的代码实现了索引方法apply。实质上,apply方法首先从数组中提取出一个整数值,然后再对这个整数中使用右移位(>>)和掩码(&)提取出正确的两位比特。私有常数S、N来自RNA1的伴生对象,S指定了每个包的尺寸(也就是2),N指定每个整数的两位比特包的数量,而M则是一个比特掩码,分离出一个字(word)的低S位。 + +注意,RNA1类的构造函数是一个私有函数。这意味着用户端无法通过调用new函数来创建RNA1序列的实例。这是有意义的,因为这能对用户隐藏RNA1序列包装数组的实现。如果用户端无法看到RNA序列的具体实现,以后任何时候,就可以做到改变RNA序列具体实现的同时,不影响到用户端代码。换句话说,这种设计实现了RNA序列的接口和实现之间解藕。然而,如果无法通过new来创建一个RNA序列,那就必须存在其他方法来创建它,否则整个类就变得毫无用处。事实上,有两种建立RNA序列的替代途径,两者都由RNA1的伴生对象(companion object)提供。第一个途径是fromSeq方法,这个方法将一个给定的碱基序列(也就是一个Seq[Base]类型的值)转换成RNA1类的实例。fromSeq方法将所有其序列参数内的碱基打包进一个数组。然后,将这个数组以及原序列的长度作为参数,调用RNA1的私有构造函数。这利用了一个事实:一个类的私有构造函数对于其伴生对象(companion object)是可见的。 + +创建RNA1实例的第二种途径由RNA1对象中的apply方法提供。它使用一个可变数量的Base类参数,并简单地将其作为序列指向fromSeq方法。这里是两个创建RNA实例的实际方案。 + + scala> val xs = List(A, G, T, A) + xs: List[Product with Base] = List(A, G, T, A) + + scala> RNA1.fromSeq(xs) + res1: RNA1 = RNA1(A, G, T, A) + + scala> val rna1 = RNA1(A, U, G, G, T) + rna1: RNA1 = RNA1(A, U, G, G, T) + +## 控制RNA类型中方法的返回值 + +这里有一些和RNA1抽象之间更多的交互操作 + + scala> rna1.length + res2: Int = 5 + + scala> rna1.last + res3: Base = T + + scala> rna1.take(3) + res4: IndexedSeq[Base] = Vector(A, U, G) +前两个返回值正如预期,但最后一个——从rna1中获得前3个元素——的返回值则未必如预期。实际上,我们知道一个IndexedSeq[Base]作为返回值的静态类型而一个Vector作为返回值的动态类型,但我们更想看到一个RNA1的值。但这是无法做到的,因为之前在RNA1类中所做的一切仅仅是让RNA1扩展IndexedSeq。换句话说,IndexedSeq类具有一个take方法,其返回一个IndexedSeq。并且,这个方法是根据 IndexedSeq 的默认是用Vector来实现的。所以,这就是上一个交互中最后一行上所能看到的。 + +#### RNA链类的第二个版本 + + final class RNA2 private ( + val groups: Array[Int], + val length: Int + ) extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA2] { + + import RNA2._ + + override def newBuilder: Builder[Base, RNA2] = + new ArrayBuffer[Base] mapResult fromSeq + + def apply(idx: Int): Base = // as before + } +现在,明白了本质之后,下一个问题便是如何去改变它们。一种途径便是覆写(override)RNA1类中的take方法,可能如下所示: + + def take(count: Int): RNA1 = RNA1.fromSeq(super.take(count)) + +这对take函数有效,但drop、filter或者init又如何呢?事实上,序列(Sequence)中有超过50个方法同样返回序列。为了保持一致,所有这些方法都必须被覆写。这看起来越来越不像一个有吸引力的选择。幸运的是,有一种更简单的途径来达到同样的效果。RNA类不仅需要继承自IndexedSeq类,同时继承自它的实现trait(特性)IndexedSeqLike。如上面的RNA2所示。新的实现在两个方面与之前不同。第一个,RNA2类现在同样扩展自IndexedSeqLike[Base, RNA2]。这个IndexedSeqLike trait(特性)以可扩展的方式实现了所有IndexedSeq的具体方法。比如,如take、drop、filer或init的返回值类型即是传给IndexedSeqLike类的第二个类型参数,也就是说,在RNA2中的是RNA2本身。 + +为了能够做,IndexedSeqLike将自身建立在newBuilder抽象上,这个抽象能够创建正确类型的builder。IndexedSeqLike trait(特性)的子类必须覆写newBuilder以返回一个它们自身类型的容器。在RNA2类中,newBuilder方法返回一个Builder[Base, RNA2]类型的builder。 + +为了构造这个builder,首先创建一个ArrayBuffer,其自身就是一个Builder[Base, ArrayBuffer]。然后通过调用其mapResult方法来将这个ArrayBuffer转换为一个RNA2 builder。mapResult方法需要一个从ArrayBuffer到RNA2的转换函数来作为其参数。转换函数仅仅提供RNA2.fromSeq,其将一个任意的碱基序列转换为RNA2值(之前提到过,数组缓冲是一种序列,所以RNA2.fromSeq可对其使用)。 + +如果忘记声明newBuilder,将会得到一个如下的错误信息: + + RNA2.scala:5: error: overriding method newBuilder in trait + TraversableLike of type => scala.collection.mutable.Builder[Base,RNA2]; + method newBuilder in trait GenericTraversableTemplate of type + => scala.collection.mutable.Builder[Base,IndexedSeq[Base]] has + incompatible type + class RNA2 private (val groups: Array[Int], val length: Int) ^ + + one error found(发现一个错误) + +错误信息非常地长,并且很复杂,体现了容器(Collection)库错综复杂的组合。所以,最好忽略有关这些方法来源的信息,因为在这种情况下,它更多得是分散人的精力。而剩下的,则说明需要声明一个具有返回类型Builder[Base, RNA2]的newBuilder方法,但无法找到一个具有返回类型Builder[Base,IndexedSeq[Base]]的newBuilder方法。后者并不覆写前者。第一个方法——返回值类型为Builder[Base, RNA2]——是一个抽象方法,其在RNA2类中通过传递RNA2的类型参数给IndexedSeqLike,来以这种类型实例化。第二个方法的返回值类型为Builder[Base,IndexedSeq[Base]]——是由继承后的IndexedSeq类提供的。换句话说,如果没有声明一个以第一个返回值类型为返回值的newBuilder,RNA2类就是非法的。 + +改善了RNA2类中的实现之后,take、drop或filter方法现在便会按照预期执行: + + scala> val rna2 = RNA2(A, U, G, G, T) + rna2: RNA2 = RNA2(A, U, G, G, T) + + scala> rna2 take 3 + res5: RNA2 = RNA2(A, U, G) + + scala> rna2 filter (U !=) + res6: RNA2 = RNA2(A, G, G, T) + +### 使用map(映射)和friends(友元) + +然而,在容器中存在没有被处理的其他类别的方法。这些方法就不总会返回容器类型。它们可能返回同一类型的容器,但包含不同类型的元素。典型的例子就是map方法。如果s是一个Int的序列(Seq[Int]),f是将Int转换为String的方法,那么,s.map(f)将返回一个String的序列(Seq[String])。这样,元素类型在接收者和结果之间发生了改变,但容器的类型还是保持一致。 + +有一些其他的方法的行为与map类似,比如说flatMap、collect等,但另一些则不同。例如:++这个追加方法,它也可能因参数返回一个不同类型的结果——向Int类型的列表拼接一个String类型的列表将会得到一个Any类型的列表。至于这些方法如何适应RNA链,理想情况下应认为,在RNA链上进行碱基到碱基的映射将产生另外一个RNA链。(译者注:碱基为RNA链的“元素”) + + scala> val rna = RNA(A, U, G, G, T) + rna: RNA = RNA(A, U, G, G, T) + + scala> rna map { case A => T case b => b } + res7: RNA = RNA(T, U, G, G, T) +同样,用 ++ 方法来拼接两个RNA链应该再次产生另外一个RNA链。 + + scala> rna ++ rna + res8: RNA = RNA(A, U, G, G, T, A, U, G, G, T) +另一方面,在RNA链上进行碱基(类型)到其他类型的映射无法产生另外一个RNA链,因为新的元素有错误的类型。它只能产生一个序列而非RNA链。同样,向RNA链追加非Base类型的元素可以产生一个普通序列,但无法产生另一个RNA链。 + + scala> rna map Base.toInt + res2: IndexedSeq[Int] = Vector(0, 3, 2, 2, 1) + + scala> rna ++ List("missing", "data") + res3: IndexedSeq[java.lang.Object] = + Vector(A, U, G, G, T, missing, data) +这就是在理想情况下应认为结果。但是,RNA2类并不提供这样的处理。事实上,如果你用RNA2类的实例来运行前两个例子,结果则是: + + scala> val rna2 = RNA2(A, U, G, G, T) + rna2: RNA2 = RNA2(A, U, G, G, T) + + scala> rna2 map { case A => T case b => b } + res0: IndexedSeq[Base] = Vector(T, U, G, G, T) + + scala> rna2 ++ rna2 + res1: IndexedSeq[Base] = Vector(A, U, G, G, T, A, U, G, G, T) + +所以,即使生成的容器元素类型是Base,map和++的结果也永远不会是RNA链。如需改善,则需要仔细查看map方法的签名(或++,它也有类似的方法签名)。map的方法最初在scala.collection.TraversableLike类中定义,具有如下签名: + + def map[B, That](f: A => B) + (隐含CBF:CanBuildFrom[修订版,B]): + +这里的A是一个容器元素的类型,而Repr是容器本身的类型,即传递给实现类(例如 TraversableLike和IndexedSeqLike)的第二个参数。map方法有两个以上的参数,B和That。参数B表示映射函数的结果类型,同时也是新容器中的元素类型。That作为map的结果类型。所以,That表示所创建的新容器的类型。 + +对于That类型如何确定,事实上,它是根据隐式参数cbf(CanBuildFrom[Repr,B,That]类型)被链接到其他类型。这些隐式CanBuildFrom由独立的容器类定义。大体上,CanBuildFrom[From,Elem,To]类型的值可以描述为:“有这么一种方法,由给定的From类型的容器,使用Elem类型,建立To的容器。” + +#### RNA链类的最终版本 + + final class RNA private (val groups: Array[Int], val length: Int) + extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA] { + + import RNA._ + + // 在IndexedSeq中必须重新实现newBuilder + override protected[this] def newBuilder: Builder[Base, RNA] = + RNA.newBuilder + + // 在IndexedSeq中必须实现apply + def apply(idx: Int): Base = { + if (idx < 0 || length <= idx) + throw new IndexOutOfBoundsException + Base.fromInt(groups(idx / N) >> (idx % N * S) & M) + } + + // (可选)重新实现foreach, + // 来提高效率 + 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 + } + } + } +#### RNA伴生对象的最终版本 + + object RNA { + + private val S = 2 // group中的比特(bit)数 + private val M = (1 << S) - 1 // 用于隔离group的比特掩码 + private val N = 32 / S // 一个Int中的group数 + + def fromSeq(buf: Seq[Base]): RNA = { + 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 RNA(groups, buf.length) + } + + def apply(bases: Base*) = fromSeq(bases) + + def newBuilder: Builder[Base, RNA] = + new ArrayBuffer mapResult fromSeq + + implicit def canBuildFrom: CanBuildFrom[RNA, Base, RNA] = + new CanBuildFrom[RNA, Base, RNA] { + def apply(): Builder[Base, RNA] = newBuilder + def apply(from: RNA): Builder[Base, RNA] = newBuilder + } + } +现在在RNA2序列链上的 map 和 ++ 的行为变得更加清晰了。由于没有能够创建RNA2序列的CanBuildFrom实例,因此在从trait InexedSeq继承的伴生对象上得到的CanBuildFrom成为了第二选择。这隐式地创建了IndexedSeqs,也是在应用map到RNA2的时候发生的情况。 + +为了解决这个缺点,你需要在RNA类的同伴对象里定义CanBuildFrom的隐式实例。该实例的类型应该是CanBuildFrom [RNA, Base, RNA] 。即,这个实例规定,给定一个RNA链和新元素类型Base,可以建立另一个RNA链容器。上述有关RNA链的两个代码以及其伴生对象展示了细节。相较于类RNA2有两个重要的区别。首先, newBuilder的实现,从RNA类移到了它的伴生对象中。RNA类中新的newBuilder方法只是转发这个定义。其次,在RNA对象现在有个隐式的CanBuildFrom值。要创建这样的对象你需要在CanBuildFrom trait中定义两个apply方法。同时,为容器RNA创建一个新的builder,但参数列表不同。在apply()方法只是简单地以正确的类型创建builder。相比之下,apply(from)方法将原来的容器作为参数。在适应动态类型的builder的返回值与接收者的动态类型一致非常有用。在RNA的情况下,这不会起作用,因为RNA是final类,所以静态类型的RNA任何接收者同时具有RNA作为其动态类型。这就是为什么apply(from)也只是简单地调用newBuilder ,忽略其参数。 + +这样,RNA类(final)以原本的类型实现了所有容器方法。它的实现需要一些协议支持。从本质上讲,你需要知道newBuilder 工厂放在哪里以及canBuildFrom的隐式实现。在有利方面,能够以相对较少的代码,得到大量自动定义的方法。另外,如果不打算在容器中扩展take,drop,map或++这样操作,可以不写额外的代码,并在类RNA1所示的实现上结束工作。 + +到目前为止,讨论集中在以定义新序列所需的最少方法来获得特定类型。但在实践中,可能需要在序列上添加新的功能,或重写现有的方法,以获得更好的效果。其中一个例子就是重写RNA类的foreach方法。foreach是RNA本身的一个重要方法,因为它实现了遍历容器。此外,容器的许多其他方法的实现依赖foreach。因此,投入一些精力来做优化方法的实现有意义。IndexedSeq的foreach方法的标准实现仅仅使用aplly来选取容器的中的第i个元素(i从0到容器长度-1)。因此,对RNA链的每一个元素,标准实现选择一个数组元素,并从中解开一个碱基(base)。而RNA类上重写的foreach要聪明得多。对于每一个选定的数组元素,它立刻对其中所包含的所有碱基应用给定的方法。因此,数组选择和位拆包的工作大大减少。 + +### 整合 sets与 map + +在第二个实例中,将介绍如何将一个新的map类型整合到容器框架中的。其方式是通过使用关键字“Patricia trie”,实现以String作为类型的可变映射(mutable map)。术语“Patricia“实际上就是"Practical Algorithm to Retrieve Information Coded in Alphanumeric."(检索字母数字编码信息的实用算法) 的缩写。思想是以树的形式存储一个set或者map,在这种树中,后续字符作为子树可以用唯一确定的关键字查找。例如,一个 Patricia trie存储了三个字符串 "abc", "abd", "al", "all", "xy" 。如下: + +patricia 树的例子: + +![20131225160411.png](/pictures/20131225160411.png) + +为了能够在trie中查找与字符串”abc“匹配的节点,只要沿着标记为”a“的子树,查找到标记为”b“的子树,最后到达标记为”c“的子树。如果 Patricia trie作为map使用,键所对应的值保存在一个可通过键定位的节点上。如果作为set,只需保存一个标记,说明set中存在这个节点。 + +使用Patricia tries的prefix map实现方式: + + import collection._ + + class PrefixMap[T] + extends mutable.Map[String, T] + with mutable.MapLike[String, T, PrefixMap[T]] { + + var suffixes: immutable.Map[Char, PrefixMap[T]] = Map.empty + var value: Option[T] = None + + def get(s: String): Option[T] = + if (s.isEmpty) value + else suffixes get (s(0)) flatMap (_.get(s substring 1)) + + def withPrefix(s: String): PrefixMap[T] = + if (s.isEmpty) this + else { + val leading = s(0) + suffixes get leading match { + case None => + suffixes = suffixes + (leading -> empty) + case _ => + } + suffixes(leading) withPrefix (s substring 1) + } + + override def update(s: String, elem: T) = + withPrefix(s).value = Some(elem) + + override def remove(s: String): Option[T] = + if (s.isEmpty) { val prev = value; value = None; prev } + else suffixes get (s(0)) flatMap (_.remove(s substring 1)) + + def iterator: Iterator[(String, T)] = + (for (v <- value.iterator) yield ("", v)) ++ + (for ((chr, m) <- suffixes.iterator; + (s, v) <- m.iterator) yield (chr +: s, v)) + + def += (kv: (String, T)): this.type = { update(kv._1, kv._2); this } + + def -= (s: String): this.type = { remove(s); this } + + override def empty = new PrefixMap[T] + } + +Patricia tries支持非常高效的查找和更新。另一个良好的特点是,支持通过前缀查找子容器。例如,在上述的patricia tree中,你可以从树根处按照“a”链接进行查找,获得所有以”a“为开头的键所组成的子容器。 + +依据这些思想,来看一下作为Patricia trie的映射实现方式。这种map称为PrefixMap。PrefixMap提供了withPrefix方法,这个方法根据给定的前缀查找子映射(submap),其包含了所有匹配该前缀的键。首先,使用键来定义一个prefix map,执行如下。 + + scala> val m = PrefixMap("abc" -> 0, "abd" -> 1, "al" -> 2, + "all" -> 3, "xy" -> 4) + m: PrefixMap[Int] = Map((abc,0), (abd,1), (al,2), (all,3), (xy,4)) + +然后,在m中调用withPrefix方法将产生另一个prefix map: + + scala> m withPrefix "a" + res14: PrefixMap[Int] = Map((bc,0), (bd,1), (l,2), (ll,3)) + +上面展示的代码表明了PrefixMap的定义方式。这个类以关联值T参数化,并扩展mutable.Map[String, T] 与 mutable.MapLike[String, T, PrefixMap[T]]。在RNA链的例子中对序列的处理也使用了这种模式,然后,为转换(如filter)继承实现类(如MapLike),用以获得正确的结果类型。 + +一个prefix map节点中含有两个可变字段:suffixes与value。value字段包含关联此节点的任意值,其初始化为None。suffixes字段包含从字符到PrefixMap值的映射(map),其初始化为空的map。 + +为什么要选择一种不可变map作为suffixes的实现方式?既然PrefixMap在整体上也是可变的,使用可变映射(map)是否更符合标准吗?这个问题的答案是,因为仅包含少量元素的不可变map,在空间和执行时间都非常高效。例如,包含有少于5个元素的map代表一个单独的对象。相比之下,就标准的可变map中的HashMap来说,即使为空时,也至少占用80bytes的空间。因此,如果普遍使用小容器,不可变的容器就优于可变的容器。在Patricia tries的例子中,预想除了树顶端节点之外,大部分节点仅包含少量的successor。所以在不可变映射(map)中存储这些successor效率很可能会更高。 + +现在看看映射(map)中第一个方法的实现:get。算法如下:为了获取prefix map里面和空字符串相关的值,简单地选取存储在树根节点上的任意值。另外,如果键字符串非空,尝试选取字符串的首字符匹配的子映射。如果产生一个map,继续去寻找map里面首个字符的之后的剩余键字符串。如果选取失败,键没有存储在map里面,则返回None。使用flatmap可以优雅地表示任意值上的联合选择。当对任意值ov,以及闭包f(其转而会返回任的值)进行应用时,如果ov和f都返回已定义的值,那么ov flatmap f将执行成功,否则ov flatmap f将返回None。 + +可变映射(map)之后的两个方法的实现是+=和-=。在prefixmap的实现中,它们按照其它两种方法定义:update和remove。 + +remove方法和get方法非常类似,除了在返回任何关联值之前,保存这个值的字段被设置为None。update方法首先会调用 withPrefix 方法找到需要被更新的树节点,然后将给定的值赋值给该节点的value字段。withPrefix 方法遍历整个树,如果在这个树中没有发现以这些前缀字符为节点的路径,它会根据需要创建子映射(sub-map)。 + +可变map最后一个需要实现的抽象方法是iterator。这个方法需要创建一个能够遍历map中所有键值对的迭代器iterator。对于任何给出的 prefix map,iterator 由如下几部分组成:首先,如果这个map中,在树根节点的value字段包含一个已定义的值Some(x),那么("", x)应为从iterator返回的第一个元素。此外,iterator需要串联存储在suffixes字段上的所有submap的iterator,并且还需要在这些返回的iterator每个键字符串的前面加上一个字符。进一步说,如果m是通过一个字符chr链接到根节点的submap ,并且(s, v)是一个从m.iterator返回的元素,那么这个根节点的iterator 将会转而返回(chr +: s, v)。在PrefixMap的iterator方法的实现中,这个逻辑可以非常简明地用两个for表达式实现。第一个for表达式在value.iterate上迭代。这表明Option值定义一个迭代器方法,如果Option值为None,则不返回任何元素。如果Option值为Some(x),则返回一个确切的元素x。 + +prefix map的伴生对象: + + import scala.collection.mutable.{Builder, MapBuilder} + import scala.collection.generic.CanBuildFrom + + object PrefixMap extends { + def empty[T] = new PrefixMap[T] + + def apply[T](kvs: (String, T)*): PrefixMap[T] = { + val m: PrefixMap[T] = empty + for (kv <- kvs) m += kv + m + } + + def newBuilder[T]: Builder[(String, T), PrefixMap[T]] = + new MapBuilder[String, T, PrefixMap[T]](empty) + + implicit def canBuildFrom[T] + : CanBuildFrom[PrefixMap[_], (String, T), PrefixMap[T]] = + new CanBuildFrom[PrefixMap[_], (String, T), PrefixMap[T]] { + def apply(from: PrefixMap[_]) = newBuilder[T] + def apply() = newBuilder[T] + } + } +请注意,在PrefixMap中没有newBuilder方法的定义。这是没有必要的,因为maps和sets有默认的构造器,即MapBuilder类的实例。对可变映射来说,其默认的构造器初始时是一个空映射,然后使用映射的+= 方法连续增加元素。可变集合也类似。非可变映射和非可变集合的默认构造器则不同,它们使用无损的元素添加方法+,而非+=方法。 + +然而,为了构建合适的集合或映射,你都需要从一个空的集合或映射开始。empty方法提供了这样的功能,它是PrefixMap中最后定义的方法,该方法简单的返回一个新的PrefixMap。 + +现在我们来看看PrefixMap的伴生对象。事实上,并不是非要定义这种伴生对象,类PrefixMap自己就可以很好的完成它的功能。PrefixMap 对象的主要作用,是定义一些方便的工厂方法。它也定义了一个 CanBuildFrom,该方法可以让输入工作完成的更好。 + +其中有两个方法值得一提,它们是 empty 和 apply。同样的方法,在Scala的容器框架中的其他容器中都存在,因此在PrefixMap中定义它们也很合理。用这两种方法,你可以像写其他容器一样的编写PrefixMap: + + scala> PrefixMap("hello" -> 5, "hi" -> 2) + res0: PrefixMap[Int] = Map((hello,5), (hi,2)) + + scala> PrefixMap.empty[String] + res2: PrefixMap[String] = Map() +另一个PrefixMap对象的成员是内置CanBuildFrom实例。它和上一节定义的CanBuildFrom目的相同:使得类似map等方法能返回最合适的类型。以PrefixMap的键值对映射函数为例,只要该函数生成串型和另一种类型组成的键值对,那么结果又会是一个PrefixMap。这里有一个例子: + + scala> res0 map { case (k, v) => (k + "!", "x" * v) } + res8: PrefixMap[String] = Map((hello!,xxxxx), (hi!,xx)) +给出的函数参数是一个PrefixMap res0的键值对绑定,最终生成串型值对。map的结果是一个PrefixMap,只是String类型替代了int类型。如果在PrefixMap中没有内置的canBuildFrom ,那么结果将是一个普通的可变映射,而不是一个PrefixMap。 + +### 小结 + +总而言之,如果你想要将一个新的collection类完全的融入到框架中,需要注意以下几点: + +1. 决定容器应该是可变的,还是非可变的。 +2. 为容器选择正确的基类trait +3. 确保容器继承自适合的trait实现,这样它就能具有大多数的容器操作。 +4. 如果你想要map及类似的操作去返回你的容器类型的实例,那么就需要在类的伴生对象中提供一个隐式CanBuildFrom。 + +你现在已经了解Scala容器如何构建和如何构建新的容器类型。由于Scala丰富的抽象支持,新容器类型无需写代码就可以拥有大量的方法实现。 + +### 致谢 + +这些页面的素材改编自,由Odersky,Spoon和Venners编写的[Scala编程](http://www.artima.com/shop/programming_in_scala)第2版 。感谢Artima 对于出版的大力支持。 diff --git a/cn/overviews/core/The_Scala_Actors_API.md b/cn/overviews/core/The_Scala_Actors_API.md new file mode 100644 index 0000000000..af9c132ddb --- /dev/null +++ b/cn/overviews/core/The_Scala_Actors_API.md @@ -0,0 +1,291 @@ +--- +layout: overview-large +title: The Scala Actors API + +disqus: true + +partof: core +num: 7 +languages: [cn] +--- + +Philipp Haller及Stephen Tu + +## 简介 + +本指南介绍了Scala 2.8和2.9中`scala.actors`包的API。这个包的内容因为逻辑上相通,所以放到了同一个类型的包内。这个trait在每个章节里面都会有所涉及。这章的重点在于这些traits所定义的各种方法在运行状态时的行为,由此来补充现有的Scala基础API。 + +注意:在Scala 2.10版本中这个Actors库将是过时的,并且在未来Scala发布的版本中将会被移除。开发者应该使用在`akka.actor`包中[Akka](http://akka.io/) actors来替代它。想了解如何将代码从Scala actors迁移到Akka请参考[Actors 迁移指南](http://docs.scala-lang.org/overviews/core/actors-migration-guide.html)章节。 + +## Actor trait:Reactor, ReplyReactor和Actor + +### Reactor trait + +Reactor 是所有`actor trait`的父级trait。扩展这个trait可以定义actor,其具有发送和接收消息的基本功能。 + +Reactor的行为通过实现其act方法来定义。一旦调用start方法启动Reactor,这个act方法便会执行,并返回这个Reactor对象本身。start方法是具有等幂性的,也就是说,在一个已经启动了的actor对象上调用它(start方法)是没有作用的。 + +Reactor trait 有一个Msg 的类型参数,这个参数指明这个actor所能接收的消息类型。 + +调用Reactor的!方法来向接收者发送消息。用!发送消息是异步的,这样意味着不会等待消息被接收——它在发送消息后便立刻往下执行。例如:`a ! msg`表示向`a`发送`msg`。每个actor都有各自的信箱(mailbox)作为缓冲来存放接收到的消息,直至这些消息得到处理。 + +Reactor trait中也定义了一个forward方法,这个方法继承于OutputChannel。它和!(感叹号,发送方法)有同样的作用。Reactor的SubTrait(子特性)——特别是`ReplyReactor trait`——覆写了此方法,使得它能够隐式地回复目标。(详细地看下面的介绍) + +一个Reactor用react方法来接收消息。react方法需要一个PartialFunction[Msg, Unit]类型的参数,当消息到达actor的邮箱之后,react方法根据这个参数来确定如何处理消息。在下面例子中,当前的actor等待接收一个“Hello”字符串,然后打印一句问候。 + + react { + case "Hello" => println("Hi there") + } +调用react没有返回值。因此,在接收到一条消息后,任何要执行的代码必须被包含在传递给react方法的偏函数(partial function)中。举个例子,通过嵌套两个react方法调用可以按顺序接收到两条消息: + + react { + case Get(from) => + react { + case Put(x) => from ! x + } + } +Reactor trait 也提供了控制结构,简化了react方法的代码。 + +### 终止和执行状态 + +当Reactor的act方法完整执行后, Reactor则随即终止执行。Reactor也可以显式地使用exit方法来终止自身。exit方法的返回值类型为Nothing,因为它总是会抛出异常。这个异常仅在内部使用,并且不应该去捕捉这个异常。 + +一个已终止的Reactor可以通过它的restart方法使它重新启动。对一个未终止的Reactor调用restart方法则会抛出`IllegalStateException`异常。重新启动一个已终止的actor则会使它的act方法重新运行。 + +Reactor定义了一个getState方法,这个方法可以将actor当前的运行状态作为Actor.State枚举的一个成员返回。一个尚未运行的actor处于`Actor.State.New`状态。一个能够运行并且不在等待消息的actor处于`Actor.State.Runnable`状态。一个已挂起,并正在等待消息的actor处于`Actor.State.Suspended`状态。一个已终止的actor处于`Actor.State.Terminated`状态。 + +### 异常处理 + +exceptionHandler成员允许定义一个异常处理程序,其在Reactor的整个生命周期均可用。 + + def exceptionHandler: PartialFunction[Exception, Unit] +exceptionHandler返回一个偏函数,它用来处理其他没有被处理的异常。每当一个异常被传递到Reactor的act方法体之外时,这个成员函数就被应用到该异常,以允许这个actor在它结束前执行清理代码。注意:`exceptionHandler`的可见性为protected。 + +用exceptionHandler来处理异常并使用控制结构对与react的编程是非常有效的。每当exceptionHandler返回的偏函数处理完一个异常后,程序会以当前的后续闭包(continuation closure)继续执行。 + + loop { + react { + case Msg(data) => + if (cond) // 数据处理代码 + else throw new Exception("cannot process data") + } + } +假设Reactor覆写了exceptionHandler,在处理完一个在react方法体内抛出的异常后,程序将会执行下一个循环迭代。 + +### ReplyReactor trait + +`ReplyReactor trait`扩展了`Reactor[Any]`并且增加或覆写了以下方法: + +!方法被覆写以获得一个当前actor对象(发送方)的引用,并且,这个发送方引用和实际的消息一起被传递到接收actor的信箱(mail box)中。接收方通过其sender方法访问消息的发送方(见下文)。 + +forward方法被覆写以获得一个引用,这个引用指向正在被处理的消息的发送方。引用和实际的消息一起作为当前消息的发送方传递。结果,forward方法允许代表不同于当前actor对象的actor对象转发消息。 + +增加的sender方法返回正被处理的消息的发送方。考虑到一个消息可能已经被转发,发送方可能不会返回实际发送消息的actor对象。 + +增加的reply方法向最后一个消息的发送方回复消息。reply方法也被用作回复一个同步消息发送或者一个使用future的消息发送(见下文)。 + +增加的!?方法提供同步消息发送。调用!?方法会引起发送方actor对象等待,直到收到一个响应,然后返回这个响应。重载的变量有两个。这个双参数变量需要额外的超时参数(以毫秒计),并且,它的返回类型是Option[Any]而不是Any。如果发送方在指定的超时期间没有收到一个响应,!?方法返回None,否则它会返回由Some包裹的响应。 + +增加的!!方法与同步消息发送的相似点在于,它们都允许从接收方传递一个响应。然而,它们返回Future实例,而不是阻塞发送中的actor对象直到接收响应。一旦Future对象可用,它可以被用来重新获得接收方的响应,还可以在不阻塞发送方的情况下,用于获知响应是否可用。重载的变量有两个。双参数变量需要额外的PartialFunction[Any,A]类型的参数。这个偏函数用于对接收方响应进行后处理。本质上,!!方法返回一个future对象,一旦响应被接收,这个future对象把偏函数应用于响应。future对象的结果就是后处理的结果。 + +增加的reactWithin方法允许在一段给定的时间段内接收消息。相对于react方法,这个方法需要一个额外的msec参数,用来指示在这个时间段(以毫秒计)直到匹配指定的TIMEOUT模式为止(TIMEOUT是包scala.actors中的用例对象(case object))。例如: + +reactWithin(2000) { case Answer(text) => // process text case TIMEOUT => println("no answer within 2 seconds") } + +reactWithin方法也允许以非阻塞方式访问信箱。当指定一个0毫秒的时间段时,首先会扫描信箱以找到一个匹配消息。如果在第一次扫描后没有匹配的消息,这个TIMEOUT模式将会匹配。例如,这使得接收某些消息会比其他消息有较高的优先级: + +reactWithin(0) { case HighPriorityMsg => // ... case TIMEOUT => react { case LowPriorityMsg => // ... } } + +在上述例子中,即使在信箱里有一个先到达的低优先级的消息,actor对象也会首先处理下一个高优先级的消息。actor对象只有在信箱里没有高优先级消息时才会首先处理一个低优先级的消息。 + +另外,ReplyReactor 增加了`Actor.State.TimedSuspended`执行状态。一个使用`reactWithin`方法等待接收消息而挂起的actor对象,处在` Actor.State.TimedSuspended `状态。 + +### Actor trait + +Actor trait扩展了`ReplyReactor`并增加或覆写了以下成员: + +增加的receive方法的行为类似react方法,但它可以返回一个结果。这可以在它的类型上反映——它的结果是多态的:def receive[R](f: PartialFunction[Any, R]): R。然而,因为actor对象挂起并等待消息时,receive方法会阻塞底层线程(underlying thread),使用receive方法使actor对象变得更加重量级。直到receive方法的调用返回,阻塞的线程将不能够执行其他actor对象。 + +增加的link和unlink方法允许一个actor对象将自身链接到另一个actor对象,或将自身从另一个actor对象断开链接。链接可以用来监控或对另一个actor对象的终止做出反应。特别要注意的是,正如在Actor trait的API文档中的解释,链接影响调用exit方法的行为。 + +trapExit成员允许对链接的actor对象的终止做出反应而无关其退出的原因(即,无关是否正常退出)。如果一个actor对象的trapExit成员被设置为true,则这个actor对象会因链接的actor对象而永远不会终止。相反,每当其中一个链接的actor对象个终止了,它将会收到类型为Exit的消息。这个Exit case class 有两个成员:from指终止的actor对象;reason指退出原因。 + +### 终止和执行状态 + +当终止一个actor对象的执行时,可以通过调用以下exit方法的变体,显式地设置退出原因: + + def exit(reason: AnyRef): Nothing +当一个actor对象以符号'normal以外的原因退出,会向所有链接到它的atocr对象传递其退出原因。如果一个actor对象由于一个未捕获异常终止,它的退出原因则为一个UncaughtException case class的实例。 + +Actor trait增加了两个新的执行状态。使用receive方法并正在等待接收消息的actor处在`Actor.State.Blocked`状态。使用receiveWithin方法并正在等待接收消息的actor处在`Actor.State.TimedBlocked`状态。 + +## 控制结构 + +Reactor trait定义了控制结构,它简化了无返回的react操作的编程。一般来说,一个react方法调用并不返回。如果actor对象随后应当执行代码,那么,或者显式传递actor对象的后续代码给react方法,或者可以使用下述控制结构,达到隐藏这些延续代码的目的。 + +最基础的控制结构是andThen,它允许注册一个闭包。一旦actor对象的所有其他代码执行完毕,闭包就会被执行。 + + actor { + { + react { + case "hello" => // 处理 "hello" + }: Unit + } andThen { + println("hi there") + } + } +例如,上述actor实例在它处理了“hello”消息之后,打印一句问候。虽然调用react方法不会返回,我们仍然可以使用andThen来注册这段输出问候的代码(作为actor的延续)。 + +注意:在react方法的调用(: Unit)中存在一种类型归属。总而言之,既然表达式的结果经常可以被忽略,react方法的结果就可以合法地作为Unit类型来处理。andThen方法无法成为Nothing类型(react方法的结果类型)的一个成员,所以在这里有必要这样做。把react方法的结果类型当作Unit,允许实现一个隐式转换的应用,这个隐式转换使得andThen成员可用。 + +API还提供一些额外的控制结构: + +loop { ... }。无限循环,在每一次迭代中,执行括号中的代码。调用循环体内的react方法,actor对象同样会对消息做出反应。而后,继续执行这个循环的下次迭代。 + +loopWhile (c) { ... }。当条件c返回true,执行括号中的代码。调用循环体中的react方法和使用loop时的效果一样。 + +continue。继续执行当前的接下来的后续闭包(continuation closure)。在loop或loopWhile循环体内调用continue方法将会使actor对象结束当前的迭代并继续执行下次迭代。如果使用andThen注册了当前的后续代码,这个闭包会作为第二个参数传给andThen,并以此继续执行。 + +控制结构可以在Reactor对象的act方法中,以及在act方法(传递地)调用的方法中任意处使用。对于用actor{...}这样的缩略形式创建的actor,控制结构可以从Actor对象导入。 + +### Future + +ReplyReactor和Actor trait支持发送带有结果的消息(!!方法),其立即返回一个future实例。一个future即Future trait的一个实例,即可以用来重新获取一个send-with-future消息的响应的句柄。 + +一个send-with-future消息的发送方可以通过应用future来等待future的响应。例如,使用val fut = a !! msg 语句发送消息,允许发送方等待future的结果。如:val res = fut()。 + +另外,一个Future可以在不阻塞的情况下,通过isSet方法来查询并获知其结果是否可用。 + +send-with-future的消息并不是获得future的唯一的方法。future也可以通过future方法计算而得。下述例子中,计算体会被并行地启动运行,并返回一个future实例作为其结果: + + val fut = future { body } + // ... + fut() // 等待future +能够通过基于actor的标准接收操作(例如receive方法等)来取回future的结果,使得future实例在actor上下文中变得特殊。此外,也能够通过使用基于事件的操作(react方法和ractWithin方法)。这使得一个actor实例在等待一个future实例结果时不用阻塞它的底层线程。 + +通过future的inputChannel,使得基于actor的接收操作方法可用。对于一个类型为`Future[T]`的future对象而言,它的类型是`InputChannel[T]`。例如: + val fut = a !! msg + // ... + fut.inputChannel.react { + case Response => // ... + } +## Channel(通道) + +channnel可以用来对发送到同一actor的不同类型消息的处理进行简化。channel的层级被分为OutputChannel和InputChannel。 + +OutputChannel可用于发送消息。OutputChannel的out方法支持以下操作。 + +out ! msg。异步地向out方法发送msg。当msg直接发送给一个actor,一个发送中的actor的引用会被传递。 + +out forward msg。异步地转发msg给out方法。当msg被直接转发给一个actor,发送中的actor会被确定。 + +out.receiver。返回唯一的actor,其接收发送到out channel(通道)的消息。 + +out.send(msg, from)。异步地发送msg到out,并提供from作为消息的发送方。 + +注意:OutputChannel trait有一个类型参数,其指定了可以被发送到channel(通道)的消息类型(使用!、forward和send)。这个类型参数是逆变的: + + trait OutputChannel[-Msg] +Actor能够从InputChannel接收消息。就像OutputChannel,InputChannel trait也有一个类型参数,用于指定可以从channel(通道)接收的消息类型。这个类型参数是协变的: + + trait InputChannel[+Msg] +An` InputChannel[Msg] `in支持下列操作。 + +in.receive { case Pat1 => ... ; case Patn => ... }(以及类似的 in.receiveWithin)。从in接收一个消息。在一个输入channel(通道)上调用receive方法和actor的标准receive操作具有相同的语义。唯一的区别是,作为参数被传递的偏函数具有PartialFunction[Msg, R]类型,此处R是receive方法的返回类型。 + +in.react { case Pat1 => ... ; case Patn => ... }(以及类似的in.reactWithin)通过基于事件的react操作,从in方法接收一个消息。就像actor的react方法,返回类型是Nothing。这意味着此方法的调用不会返回。就像之前的receive操作,作为参数传递的偏函数有一个更具体的类型:PartialFunction[Msg, Unit] + +### 创建和共享channel + +channel通过使用具体的Channel类创建。它同时扩展了InputChannel和OutputChannel。使channel在多个actor的作用域(Scope)中可见,或者将其在消息中发送,都可以实现channel的共享。 + +下面的例子阐述了基于作用域(scope)的共享。 + + actor { + var out: OutputChannel[String] = null + val child = actor { + react { + case "go" => out ! "hello" + } + } + val channel = new Channel[String] + out = channel + child ! "go" + channel.receive { + case msg => println(msg.length) + } + } +运行这个例子将输出字符串“5”到控制台。注意:子actor对out(一个OutputChannel[String])具有唯一的访问权。而用于接收消息的channel的引用则被隐藏了。然而,必须要注意的是,在子actor向输出channel发送消息之前,确保输出channel被初始化到一个具体的channel。通过使用“go”消息来完成消息发送。当使用channel.receive来从channel接收消息时,因为消息是String类型的,可以使用它提供的length成员。 + +另一种共享channel的可行的方法是在消息中发送它们。下面的例子对此作了阐述。 + + case class ReplyTo(out: OutputChannel[String]) + + val child = actor { + react { + case ReplyTo(out) => out ! "hello" + } + } + + actor { + val channel = new Channel[String] + child ! ReplyTo(channel) + channel.receive { + case msg => println(msg.length) + } + } +ReplyTo case class是一个消息类型,用于分派一个引用到OutputChannel[String]。当子actor接收一个ReplyTo消息时,它向它的输出channel发送一个字符串。第二个actor则像以前一样接收那个channel上的消息。 + +## Scheduler + +scheduler用于执行一个Reactor实例(或子类型的一个实例)。Reactor trait引入了scheduler成员,其返回常用于执行Reactor实例的scheduler。 + + def scheduler: IScheduler +运行时系统通过使用在IScheduler trait中定义的execute方法之一,向scheduler提交任务来执行actor。只有在完整实现一个新的scheduler时(但没有必要),此trait的大多数其他方法才是相关的。 + +默认的scheduler常用于执行Reactor实例,而当所有的actor完成其执行时,Actor则会检测环境。当这发生时,scheduler把它自己关闭(终止scheduler使用的任何线程)。然而,一些scheduler,比如SingleThreadedScheduler(位于scheduler包)必须要通过调用它们的shutdown方法显式地关闭。 + +创建自定义scheduler的最简单方法是通过扩展SchedulerAdapter,实现下面的抽象成员: + + def execute(fun: => Unit): Unit +典型情况下,一个具体的实现将会使用线程池来执行它的按名参数fun。 + +## 远程Actor + +这一段描述了远程actor的API。它的主要接口是scala.actors.remote包中的RemoteActor对象。这个对象提供各种方法来创建和连接到远程actor实例。在下面的代码段中我们假设所有的RemoteActor成员都已被导入,所使用的完整导入列表如下: + + import scala.actors._ + import scala.actors.Actor._ + import scala.actors.remote._ + import scala.actors.remote.RemoteActor._ +### 启动远程Actor + +远程actor由一个Symbol唯一标记。在这个远程Actor所执行JVM上,这个符号对于JVM实例是唯一的。由名称'myActor标记的远程actor可按如下方法创建。 + + class MyActor extends Actor { + def act() { + alive(9000) + register('myActor, self) + // ... + } + } +记住:一个名字一次只能标记到一个单独的(存活的)actor。例如,想要标记一个actorA为'myActor,然后标记另一个actorB为'myActor。这种情况下,必须等待A终止。这个要求适用于所有的端口,因此简单地将B标记到不同的端口来作为A是不能满足要求的。 + +### 连接到远程Actor + +连接到一个远程actor也同样简单。为了获得一个远程Actor的远程引用(运行于机器名为myMachine,端口为8000,名称为'anActor),可按下述方式使用select方法: + + val myRemoteActor = select(Node("myMachine", 8000), 'anActor) +从select函数返回的actor具有类型AbstractActor,这个类型本质上提供了和通常actor相同的接口,因此支持通常的消息发送操作: + + myRemoteActor ! "Hello!" + receive { + case response => println("Response: " + response) + } + myRemoteActor !? "What is the meaning of life?" match { + case 42 => println("Success") + case oops => println("Failed: " + oops) + } + val future = myRemoteActor !! "What is the last digit of PI?" +记住:select方法是惰性的,它不实际初始化任何网络连接。仅当必要时(例如,调用!时),它会单纯地创建一个新的,准备好初始化新网络连接的AbstractActor实例。 + diff --git a/cn/overviews/core/Value-Classes-and-Universal-Traits.md b/cn/overviews/core/Value-Classes-and-Universal-Traits.md new file mode 100644 index 0000000000..312c8aa87c --- /dev/null +++ b/cn/overviews/core/Value-Classes-and-Universal-Traits.md @@ -0,0 +1,253 @@ +--- +layout: overview-large +title: Value Classes and Universal + +disqus: true + +partof: core +num: 2 +languages: [cn] +--- + +Mark Harrah + +## 引言 + +Value classes是在[SIP-15](http://docs.scala-lang.org/sips/pending/value-classes.html)中提出的一种通过继承AnyVal类来避免运行时对象分配的新机制。以下是一个最简的value class。 + + class Wrapper(val underlying: Int) extends AnyVal + +它仅有一个被用作运行时底层表示的公有val参数。在编译期,其类型为Wrapper,但在运行时,它被表示为一个Int。Value class可以带有def定义,但不能再定义额外的val、var,以及内嵌的trait、class或object: + + class Wrapper(val underlying: Int) extends AnyVal { + def foo: Wrapper = new Wrapper(underlying * 19) + } + +Value class只能继承universal traits,但其自身不能再被继承。所谓universal trait就是继承自Any的、只有def成员,且不作任何初始化工作的trait。继承自某个universal trait的value class同时继承了该trait的方法,但是(调用这些方法)会带来一定的对象分配开销。例如: + + trait Printable extends Any { + def print(): Unit = println(this) + } + class Wrapper(val underlying: Int) extends AnyVal with Printable + + val w = new Wrapper(3) + w.print() // 这里实际上会生成一个Wrapper类的实例 + +本文后续篇幅将介绍相关用例和与对象分配时机相关的细节,并给出一些有关value class自身限制的具体实例。 + +## 扩展方法 + +关于value类的一个用例,是将它们和隐含类联合([SIP-13](http://docs.scala-lang.org/sips/pending/implicit-classes.html))以获得免分配扩展方法。使用隐含类可以提供便捷的语法来定义扩展方法,同时 value 类移除运行时开销。一个好的例子是在标准库里的RichInt类。RichInt 继承自Int类型并附带一些方法。由于它是一个 value类,使用RichInt 方法时不需要创建一个RichInt 的实例。 + +下面有关RichInt的代码片段示范了RichInt是如何继承Int来允许3.toHexString的表达式: + + class RichInt(val self: Int) extends AnyVal { + def toHexString: String = java.lang.Integer.toHexString(self) + } + +在运行时,表达式3.toHexString 被优化并等价于静态对象的方法调用 (RichInt$.MODULE$.extension$toHexString(3)),而不是创建一个新实例对象,再调用其方法。 + +## 正确性 + +关于value类的另一个用例是:不增加运行时开销的同时,获得数据类型的类型安全。例如,一个数据类型片断代表一个距离 ,如: + + class Meter(val value: Double) extends AnyVal { + def +(m: Meter): Meter = new Meter(value + m.value) + } + +代码:对两个距离进行相加,例如: + + val x = new Meter(3.4) + val y = new Meter(4.3) + val z = x + y + +实际上不会分配任何Meter实例,而是在运行时仅使用原始双精浮点数(double) 。 + +注意:在实践中,可以使用条件类(case)and/or 扩展方法来让语句更清晰。 + +## 必须进行分配的情况 + +由于JVM不支持value类,Scala 有时需要真正实例化value类。详细细节见[SIP-15]。 + +### 分配概要 + +value类在以下情况下,需要真正实例化: + +value类作为另一种类型使用时。 +value类被赋值给数组。 +执行运行时类型测试,例如模式匹配。 + +### 分配细节 + +无论何时,将value类作为另一种类型进行处理时(包括universal trait),此value类实例必须被实例化。例如,value类Meter : + + trait Distance extends Any + case class Meter(val value: Double) extends AnyVal with Distance + +接收Distance类型值的方法需要一个正真的Meter实例。下面的例子中,Meter类真正被实例化。 + + def add(a: Distance, b: Distance): Distance = ... + add(Meter(3.4), Meter(4.3)) + +如果替换add方法的签名: + + def add(a: Meter, b: Meter): Meter = ... + +那么就不必进行分配了。此规则的另一个例子是value类作为类型参数使用。例如:即使是调用identity方法,也必须创建真正的Meter实例。 + + def identity[T](t: T): T = t + identity(Meter(5.0)) + +必须进行分配的另一种情况是:将它赋值给数组。即使这个数组就是value类数组,例如: + + val m = Meter(5.0) + val array = Array[Meter](m) + +数组中包含了真正的Meter 实例,并不只是底层基本类型double。 + +最后是类型测试。例如,模式匹配中的处理以及asInstanceOf方法都要求一个真正的value类实例: + + case class P(val i: Int) extends AnyVal + + val p = new P(3) + p match { // 在这里,新的P实例被创建 + case P(3) => println("Matched 3") + case P(x) => println("Not 3") + } + +## 限制 + +目前Value类有一些限制,部分原因是JVM不提供value类概念的原生支持。value类的完整实现细节及其限制见[SIP-15]。 + +### 限制概要 + +一个value类 ... + +1. ...必须只有一个public的构造函数。并有且只有一个public的,类型不为value类的val参数。 +2. ...不能有特殊的类型参数. +3. ...不能有嵌套或本地类、trait或对象。 +4. ...不能定义equals或hashCode方法。 +5. ...必须是一个顶级类,或静态访问对象的一个成员 +6. ...仅能有def为成员。尤其是,成员不能有惰性val、val或者var 。 +7. ...不能被其它类继承。 + +### 限制示例 + +本章节列出了许多限制下具体影响, 而在“必要分配”章节已提及的部分则不再敖述。 + +构造函数不允许有多个参数: + + class Complex(val real: Double, val imag: Double) extends AnyVal + +则Scala编译器将生成以下的错误信息: + + Complex.scala:1: error: value class needs to have exactly one public val parameter +(Complex.scala:1: 错误:value类只能有一个public的val参数。) +(译者注:鉴于实际中编译器输出的可能是英文信息,在此提供双语。) + class Complex(val real: Double, val imag: Double) extends AnyVal + ^ +由于构造函数参数必须是val,而不能是一个按名(by-name)参数: + + NoByName.scala:1: error: `val' parameters may not be call-by-name + (NoByName.scala:1: 错误: `val' 不能为 call-by-name) + class NoByName(val x: => Int) extends AnyVal + ^ +Scala不允许惰性val作为构造函数参数, 所以value类也不允许。并且不允许多个构造函数。 + + class Secondary(val x: Int) extends AnyVal { + def this(y: Double) = this(y.toInt) + } + + Secondary.scala:2: error: value class may not have secondary constructors + (Secondary.scala:2: 错误:value类不能有第二个构造函数。) + def this(y: Double) = this(y.toInt) + ^ +value class不能将惰性val或val作为成员,也不能有嵌套类、trait或对象。 + + class NoLazyMember(val evaluate: () => Double) extends AnyVal { + val member: Int = 3 + lazy val x: Double = evaluate() + object NestedObject + class NestedClass + } + + Invalid.scala:2: error: this statement is not allowed in value class: private[this] val member: Int = 3 + (Invalid.scala:2: 错误: value类中不允许此表达式:private [this] val member: Int = 3) + val member: Int = 3 + ^ + Invalid.scala:3: error: this statement is not allowed in value class: lazy private[this] var x: Double = NoLazyMember.this.evaluate.apply() + (Invalid.scala:3: 错误:value类中不允许此表达式: lazy private[this] var x: Double = NoLazyMember.this.evaluate.apply()) + lazy val x: Double = evaluate() + ^ + Invalid.scala:4: error: value class may not have nested module definitions + (Invalid.scala:4: 错误: value类中不能定义嵌套模块) + object NestedObject + ^ + Invalid.scala:5: error: value class may not have nested class definitions + (Invalid.scala:5: 错误:value类中不能定义嵌套类) + class NestedClass + ^ +注意:value类中也不允许出现本地类、trait或对象,如下: + + class NoLocalTemplates(val x: Int) extends AnyVal { + def aMethod = { + class Local + ... + } + } +在目前value类实现的限制下,value类不能嵌套: + + class Outer(val inner: Inner) extends AnyVal + class Inner(val value: Int) extends AnyVal + + Nested.scala:1: error: value class may not wrap another user-defined value class + (Nested.scala:1:错误:vlaue类不能包含另一个用户定义的value类) + class Outer(val inner: Inner) extends AnyVal + ^ +此外,结构类型不能使用value类作为方法的参数或返回值类型。 + + class Value(val x: Int) extends AnyVal + object Usage { + def anyValue(v: { def value: Value }): Value = + v.value + } + + Struct.scala:3: error: Result type in structural refinement may not refer to a user-defined value class + (Struct.scala:3: 错误: 结构细化中的结果类型不适用于用户定义的value类) + def anyValue(v: { def value: Value }): Value = + ^ + value类不能继承non-universal trait,并且其本身不能被继承: + + trait NotUniversal + class Value(val x: Int) extends AnyVal with notUniversal + class Extend(x: Int) extends Value(x) + + Extend.scala:2: error: illegal inheritance; superclass AnyVal + is not a subclass of the superclass Object + of the mixin trait NotUniversal + (Extend.scala:2: 错误:非法继承:父类AnyVal不是一个父类对象(混入trait NotUniversal)的子类) + class Value(val x: Int) extends AnyVal with NotUniversal + ^ + Extend.scala:3: error: illegal inheritance from final class Value + (Extend.scala:3: 错误: 从Value类(final类)非法继承) + class Extend(x: Int) extends Value(x) + ^ +第二条错误信息显示:虽然value类没有显式地用final关键字修饰,但依然认为value类是final类。 + +另一个限制是:一个类仅支持单个参数的话,则value类必须是顶级类,或静态访问对象的成员。这是由于嵌套value类需要第二个参数来引用封闭类。所以不允许下述代码: + + class Outer + { + class Inner(val x: Int) extends AnyVal + } + + Outer.scala:2: error: value class may not be a member of another class + (Outer.scala:2: 错误:value类不能作为其它类的成员) + class Inner(val x: Int) extends AnyVal + ^ +但允许下述代码,因为封闭对象是顶级类: + + object Outer { + class Inner(val x: Int) extends AnyVal + } + diff --git a/cn/overviews/index.md b/cn/overviews/index.md new file mode 100644 index 0000000000..fffc09c591 --- /dev/null +++ b/cn/overviews/index.md @@ -0,0 +1,44 @@ +--- +layout: guides-index +language: cn +title: 目录 +--- +##[Thanks](Thanks.md) +##[The Scala Actors Migration Guide NEW IN 2.10](core/The-Scala-Actors-Migration-Guide.md) +##[Value Classes and Universal Traits NEW IN 2.10](core/Value-Classes-and-Universal-Traits.md) +##[String Interpolation NEW IN 2.10](core/String_Interpolation.md) +##[Implicit Classes AVAILABLE](core/Implicit-Classes.md) +##[Futures and Promises NEW IN 2.10](core/Futures-and-Promises.md) +## Scala’s Parallel Collections Library +- [Overview](parallel-collections/Overview.md) +- [Concrete Parallel Collection Classes](parallel-collections/Concrete_Parallel_Collection_Classes.md) +- [Parallel Collection Conversions](parallel-collections/Parallel_Collection_Conversions.md) +- [Concurrent Tries](parallel-collections/Concurrent_Tries.md) +- [Architecture of the Parallel Collections Library](parallel-collections/Architecture_of_the_Parallel_Collections_Library.md) +- [Creating Custom Parallel Collections](parallel-collections/Creating_Custom_Parallel_Collections.md) +- [Configuring Parallel Collections](parallel-collections/Configuring_Parallel_Collections.md) +- [Measuring Performance](parallel-collections/Measuring_Performance.md) + +##[The Architecture of Scala Collections](core/The_Architecture_of_Scala_Collections.md) +##[The Scala Actors API](core/The_Scala_Actors_API.md) + +## Scala’s Collections Library + +- [Introduction](collections/Introduction.md) +- [Mutable and Immutable Collections](collections/Mutable_and_Immutable_Collections.md) +- [Trait Traversable](collections/Trait_Traversable.md) +- [Trait Iterable](collections/Trait_Iterable.md) +- [The sequence traits Seq, IndexedSeq, and LinearSeq](collections/The_sequence_traits.md) +- [Sets](collections/Sets.md) +- [Maps](collections/Maps.md) +- [Concrete Immutable Collection Classes](collections/Concrete_Immutable_Collection_Classes.md) +- [Concrete Mutable Collection Classes](collections/Concrete_Mutable_Collection_Classes.md) +- [Arrays](collections/Arrays.md) +- [Strings](collections/Strings.md) +- [Performance Characteristics](collections/Performance_Characteristics.md) +- [Equality](collections/Equality.md) +- [Views](collections/Views.md) +- [Iterators](collections/Iterators.md) +- [Creating Collections From Scratch](collections/Creating_Collections_From_Scratch.md) +- [Conversions Between Java and Scala Collections](collections/Conversions_Between_Java_and_Scala_Collections.md) +- [Migrating from Scala 2.7](collections/Migrating_from_Scala_2_7.md) diff --git a/cn/overviews/parallel-collections/Architecture_of_the_Parallel_Collections_Library.md b/cn/overviews/parallel-collections/Architecture_of_the_Parallel_Collections_Library.md new file mode 100644 index 0000000000..bd9ae56a79 --- /dev/null +++ b/cn/overviews/parallel-collections/Architecture_of_the_Parallel_Collections_Library.md @@ -0,0 +1,62 @@ +--- +layout: overview-large +title: 并行集合库的架构 + +disqus: true + +partof: parallel-collections +num: 5 +language: cn +--- + +像正常的顺序集合库那样,Scala的并行集合库包含了大量的由不同并行集合实现的一致的集合操作。并且像顺序集合库那样,scala的并行集合库通过并行集合“模板”实现了大部分操作,从而防止了代码重复。“模板”只需要定义一次就可以通过不同的并行集合被灵活地继承。 + +这种方法的好处是大大缓解了维护和可扩展性。对于维护--所有的并行集合通过继承一个有单一实现的并行集合,维护变得更容易和更健壮;bug修复传播到类层次结构,而不需要复制实现。出于同样的原因,整个库变得更易于扩展--新的集合类可以简单地继承大部分的操作。 + +### 核心抽象 + +上述的”模板“特性实现的多数并行操作都是根据两个核心抽象--分割器和组合器。 + +#### 分割器 + +Spliter的工作,正如其名,它把一个并行集合分割到了它的元素的非重要分区里面。基本的想法是将集合分割成更小的部分直到他们小到足够在序列上操作。 + + trait Splitter[T] extends Iterator[T] { + def split: Seq[Splitter[T]] + } + +有趣的是,分割器是作为迭代器实现的,这意味着除了分割,他们也被框架用来遍历并行集合(也就是说,他们继承了迭代器的标准方法,如next()和hasNext())。这种“分割迭代器”的独特之处是它的分割方法把自身(迭代器类型的分割器)进一步分割成额外的分割器,这些新的分割器能遍历到整个并行集合的不相交的元素子集。类似于正常的迭代器,分割器在调用分割方法后失效。 + +一般来说,集合是使用分割器(Splitters)分成大小大致相同的子集。在某些情况下,任意大小的分区是必须的,特别是在并行序列上,PreciseSplitter(精确的分割器)是很有用的,它是继承于Splitter和另外一个实现了精确分割的方法--psplit. + +#### 组合器 + +组合器被认为是一个来自于Scala序列集合库的广义构造器。每一个并行集合都提供一个单独的组合器,同样,每一个序列集合也提供一个构造器。 + +而对于序列集合,元素可以被增加到一个构造器中去,并且集合可以通过调用结果方法生成,对于并行集合,组合器有一个叫做combine的方法,它调用其他的组合器进行组合并产生包含两个元素的并集的新组合器。当调用combine方法后,这两个组合器都会变成无效的。 + +trait Combiner[Elem, To] extends Builder[Elem, To] { + def combine(other: Combiner[Elem, To]): Combiner[Elem, To] +} +这两个类型参数Elem,根据上下文分别表示元素类型和结果集合的类型。 + +注意:鉴于两个组合器,c1和c2,在c1=c2为真(意味它们是同一个组合器),调用c1.combine(c2)方法总是什么都不做并且简单的返回接收的组合器c1。 + +### 层级 + +Scala的并行集合吸收了很多来自于Scala的(序列)集合库的设计灵感--事实上,它反映了规则地集合框架的相应特征,如下所示。 + +![parallel-collections-hierarchy.png](/pictures/parallel-collections-hierarchy.png) + +Scala集合的层次和并行集合库 + +当然我们的目标是尽可能紧密集成并行集合和序列集合,以允许序列集合和并行集合之间的简单替代。 + +为了能够获得一个序列集合或并行集合的引用(这样可以通过par和seq在并行集合和序列集合之间切换),两种集合类型存在一个共同的超型。这是上面所示的“通用”特征的起源,GenTraversable, GenIterable, GenSeq, GenMap and GenSet,不保证按次序或挨个的遍历。相应的序列或并行特征继承于这些。例如,一个ParSeq和Seq都是一个通用GenSeq的子类型,但是他们之间没有相互公认的继承关系。 + +更详细的讨论序列集合和并行集合器之间的层次共享,请参见技术报告。[[1](http://infoscience.epfl.ch/record/165523/files/techrep.pdf)] + +引用 + +1. [On a Generic Parallel Collection Framework, Aleksandar Prokopec, Phil Bawgell, Tiark Rompf, Martin Odersky, June 2011](http://infoscience.epfl.ch/record/165523/files/techrep.pdf) + diff --git a/cn/overviews/parallel-collections/Concrete_Parallel_Collection_Classes.md b/cn/overviews/parallel-collections/Concrete_Parallel_Collection_Classes.md new file mode 100644 index 0000000000..659927d2c1 --- /dev/null +++ b/cn/overviews/parallel-collections/Concrete_Parallel_Collection_Classes.md @@ -0,0 +1,147 @@ +--- +layout: overview-large +title: 具体并行集合类 + +disqus: true + +partof: parallel-collections +num: 2 +language: cn +--- + +### 并行数组(Parallel Range) + +一个并行数组由线性、连续的数组元素序列。这意味着这些元素可以高效的被访问和修改。正因为如此,遍历元素也是非常高效的。在此意义上并行数组就是一个大小固定的数组。 + + scala> val pa = scala.collection.parallel.mutable.ParArray.tabulate(1000)(x => 2 * x + 1) + pa: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 3, 5, 7, 9, 11, 13,... + + scala> pa reduce (_ + _) + res0: Int = 1000000 + + scala> pa map (x => (x - 1) / 2) + res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(0, 1, 2, 3, 4, 5, 6, 7,... + +在内部,分离一个并行数组[分离器](http://docs.scala-lang.org/overviews/parallel-collections/architecture.html#core_abstractions)相当于使用它们的下标迭代器更新来创建两个分离器。[组合](http://docs.scala-lang.org/overviews/parallel-collections/architecture.html#core_abstractions)稍微负责一点。因为大多数的分离方法(如:flatmap, filter, takeWhile等)我们不能预先知道元素的个数(或者数组的大小),每一次组合本质上来说是一个数组缓冲区的一种变量根据分摊时间来进行加减的操作。不同的处理器进行元素相加操作,对每个独立并列数组进行组合,然后根据其内部连结在再进行组合。在并行数组中的基础数组只有在知道元素的总数之后才能被分配和填充。基于此,变换方法比存取方法要稍微复杂一些。另外,请注意,最终数组分配在JVM上的顺序进行,如果映射操作本身是很便宜,这可以被证明是一个序列瓶颈。 + +通过调用seq方法,并行数组(parallel arrays)被转换为对应的顺序容器(sequential collections) ArraySeq。这种转换是非常高效的,因为新创建的ArraySeq 底层是通过并行数组(parallel arrays)获得的。 + +### 并行向量(Parallel Vector) + +这个并行向量是一个不可变数列,低常数因子数的访问和更新的时间。 + + scala> val pv = scala.collection.parallel.immutable.ParVector.tabulate(1000)(x => x) + pv: scala.collection.parallel.immutable.ParVector[Int] = ParVector(0, 1, 2, 3, 4, 5, 6, 7, 8, 9,... + + scala> pv filter (_ % 2 == 0) + res0: scala.collection.parallel.immutable.ParVector[Int] = ParVector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18,... +不可变向量表现为32叉树,因此[分离器]通过将子树分配到每个分离器(spliter)来分离。[组合(combiners)]存储元素的向量并通过懒惰(lazily)拷贝来组合元素。因此,转换方法相对于并行数组来说可伸缩性较差。一旦串联操作在将来scala的发布版本中成为可变的,组合器将会使得串联和变量器方法更加有效率。 + +并行向量是一个连续[向量]的并行副本,因此两者之间的转换需要一定的时间。 + +### 并行范围(Parallel Range) + +一个ParRange表示的是一个有序的等差整数数列。一个并行范围(parallel range)的创建方法和一个顺序范围的创建类似。 + + scala> 1 to 3 par + res0: scala.collection.parallel.immutable.ParRange = ParRange(1, 2, 3) + + scala> 15 to 5 by -2 par + res1: scala.collection.parallel.immutable.ParRange = ParRange(15, 13, 11, 9, 7, 5) + +正如顺序范围有没有创建者(builders),平行的范围(parallel ranges)有没有组合者(combiners)。映射一个并行范围的元素来产生一个并行向量。顺序范围(sequential ranges)和并行范围(parallel ranges)能够被高效的通过seq和par方法进行转换。 + +### 并行哈希表(Parallel Hash Tables) + +并行哈希表存储在底层数组的元素,并将它们放置在由各自元素的哈希码的位置。并行不变的哈希集(set)([mutable.ParHashSet](http://www.scala-lang.org/api/2.10.0/scala/collection/parallel/mutable/ParHashSet.html))和并行不变的哈希映射([mutable.ParHashMap](http://www.scala-lang.org/api/2.10.0/scala/collection/parallel/mutable/ParHashMap.html)) 是基于哈希表的。 + + scala> val phs = scala.collection.parallel.mutable.ParHashSet(1 until 2000: _*) + phs: scala.collection.parallel.mutable.ParHashSet[Int] = ParHashSet(18, 327, 736, 1045, 773, 1082,... + + scala> phs map (x => x * x) + res0: scala.collection.parallel.mutable.ParHashSet[Int] = ParHashSet(2181529, 2446096, 99225, 2585664,... + +并行哈希表组合器元素排序是依据他们的哈希码前缀在桶(buckets)中进行的。它们通过简单地连接这些桶在一起。一旦最后的哈希表被构造出来(如:组合结果的方法被调用),基本数组分配和从不同的桶元素复制在平行于哈希表的数组不同的相邻节段。 + +连续的哈希映射和散列集合可以被转换成并行的变量使用par方法。并行哈希表内在上要求一个映射的大小在不同块的哈希表元素的数目。这意味着,一个连续的哈希表转换为并行哈希表的第一时间,表被遍历并且size map被创建,因此,第一次调用par方法的时间是和元素个数成线性关系的。进一步修改的哈希表的映射大小保持状态,所以以后的转换使用PAR和序列具有常数的复杂性。使用哈希表的usesizemap方法,映射大小的维护可以开启和关闭。重要的是,在连续的哈希表的修改是在并行哈希表可见,反之亦然。 + +### 并行散列Tries(Parallel Hash Tries) + +并行hash tries是不可变(immutable)hash tries的并行版本,这种结果可以用来高效的维护不可变集合(immutable set)和不可变关联数组(immutable map)。他们都支持类[immutable.ParHashSet](http://www.scala-lang.org/api/2.10.0/scala/collection/parallel/immutable/ParHashSet.html)和[immutable.ParHashMap](http://www.scala-lang.org/api/2.10.0/scala/collection/parallel/immutable/ParHashMap.html)。 + + scala> val phs = scala.collection.parallel.immutable.ParHashSet(1 until 1000: _*) + phs: scala.collection.parallel.immutable.ParHashSet[Int] = ParSet(645, 892, 69, 809, 629, 365, 138, 760, 101, 479,... + + scala> phs map { x => x * x } sum + res0: Int = 332833500 + +类似于平行散列哈希表,parallel hash trie在桶(buckets)里预排序这些元素和根据不同的处理器分配不同的桶(buckets) parallel hash trie的结果,这些构建subtrie是独立的。 + +并行散列试图可以来回转换的,顺序散列试图利用序列和时间常数的方法。 + +### 并行并发tries(Parallel Concurrent Tries) + +[ concurrent.triemap ](http://www.scala-lang.org/api/2.10.0/scala/collection/concurrent/TrieMap.html)是竞争对手的线程安全的地图,而[ mutable.partriemap ](http://www.scala-lang.org/api/2.10.0/scala/collection/parallel/mutable/ParTrieMap.html) 是他的并行副本。如果这个数据结构在遍历的过程中被修改了,大多数竞争对手的数据结构不能确保一致遍历,尝试确保在下一次迭代中更新是可见的。这意味着,你可以在尝试遍历的时候改变这些一致性,如下例子所示输出1到99的平方根。 + + scala> val numbers = scala.collection.parallel.mutable.ParTrieMap((1 until 100) zip (1 until 100): _*) map { case (k, v) => (k.toDouble, v.toDouble) } + numbers: scala.collection.parallel.mutable.ParTrieMap[Double,Double] = ParTrieMap(0.0 -> 0.0, 42.0 -> 42.0, 70.0 -> 70.0, 2.0 -> 2.0,... + + scala> while (numbers.nonEmpty) { + | numbers foreach { case (num, sqrt) => + | val nsqrt = 0.5 * (sqrt + num / sqrt) + | numbers(num) = nsqrt + | if (math.abs(nsqrt - sqrt) < 0.01) { + | println(num, nsqrt) + | numbers.remove(num) + | } + | } + | } + (1.0,1.0) + (2.0,1.4142156862745097) + (7.0,2.64576704419029) + (4.0,2.0000000929222947) + ... + +合成器是引擎盖下triemaps实施——因为这是一个并行数据结构,只有一个组合构建整个变压器的方法调用和所有处理器共享。 + +与所有的并行可变容器(collections),Triemaps和并行partriemaps通过调用序列或PAR方法得到了相同的存储支持,所以修改在一个在其他可见。转换发生在固定的时间。 + +### 性能特征 + +顺序类型(sequence types)的性能特点: + +![20131225152436.png](/pictures/20131225152436.png) + +性能特征集(set)和映射类型: + +![20131225152515.png](/pictures/20131225152515.png) + +####Key + +上述两个表的条目,说明如下: + +|C |该操作需要的时间常数(快)| +|-----|------------------------| +|eC|该操作需要有效的常数时间,但这可能依赖于一些假设,如一个向量或分配哈希键的最大长度。| +|aC|该操作需要分期常量时间。一些调用的操作可能需要更长的时间,但是如果很多操作都是在固定的时间就可以使用每个操作的平均了。| +|Log|该操作需要collection大小的对数时间比例。| +|L|这个操作是线性的,需要collection大小的时间比例。| +|-|这个操作是不被支持的。| +第一个表处理序列类型--可变和不可变--使用以下操作:| + +| head | 选择序列的第一个元素。| +|-----|------------------------| +|tail|产生一个由除了第一个元素的所有元素组成的新的序列。| +|apply|标引,索引| +|update|对于不可变(immutable sequence)执行函数式更新(functional update),对于可变数据执行带有副作用(side effect)的更新。| +|prepend|在序列的前面添加一个元素。 针对不可变的序列,这将产生一个新的序列,针对可变序列这将修改已经存在的序列。| +|append|在序列结尾添加一个元素。针对不可变的序列,这将产生一个新的序列,针对可变序列这将修改已经存在的序列。| +|insert|在序列中的任意位置插入一个元素。这是可变序列(mutable sequence)唯一支持的操作。| + +第二个表处理可变和不可变集合(set)与关联数组(map)使用以下操作: + +|lookup| 测试一个元素是否包含在集合,或选择一个键所关联的值。| +|-----|------------------------| +|add | 新增一个元素到集合(set)或者键/值匹配映射。| +|remove|从集合(set)或者关键映射中移除元素。| +|min|集合(set)中最小的元素,或者关联数组(map)中的最小的键(key)。| diff --git a/cn/overviews/parallel-collections/Concurrent_Tries.md b/cn/overviews/parallel-collections/Concurrent_Tries.md new file mode 100644 index 0000000000..ec7f9bf179 --- /dev/null +++ b/cn/overviews/parallel-collections/Concurrent_Tries.md @@ -0,0 +1,111 @@ +--- +layout: overview-large +title: 并发字典树 + +disqus: true + +partof: parallel-collections +num: 4 +language: cn +--- + + +对于大多数并发数据结构,如果在遍历中途数据结构发生改变,都不保证遍历的一致性。实际上,大多数可变容器也都是这样。并发字典树允许在遍历时修改Trie自身,从这个意义上来讲是特例。修改只影响后续遍历。顺序并发字典树及其对应的并行字典树都是这样处理。它们之间唯一的区别是前者是顺序遍历,而后者是并行遍历。 + +这是一个很好的性质,可以让一些算法实现起来更加的容易。常见的是迭代处理数据集元素的一些算法。在这样种场景下,不同的元素需要不同次数的迭代以进行处理。 + +下面的例子用于计算一组数据的平方根。每次循环反复更新平方根值。数据的平方根收敛,就把它从映射中删除。 + + case class Entry(num: Double) { + var sqrt = num + } + + val length = 50000 + + // 准备链表 + val entries = (1 until length) map { num => Entry(num.toDouble) } + val results = ParTrieMap() + for (e <- entries) results += ((e.num, e)) + + // 计算平方根 + while (results.nonEmpty) { + for ((num, e) <- results) { + val nsqrt = 0.5 * (e.sqrt + e.num / e.sqrt) + if (math.abs(nsqrt - e.sqrt) < 0.01) { + results.remove(num) + } else e.sqrt = nsqrt + } + } + +注意,在上面的计算平方根的巴比伦算法([3](http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method))中,某些数据会比别的数据收敛的更快。基于这个因素,我们希望能够尽快把他们从结果中剔除,只遍历那些真正需要耗时处理的元素。 + +另一个例子是广度优先搜索算法,该算法迭代地在末端节点遍历,直到找到通往目标的路径,或遍历完所有周围节点。一个二维地图上的节点定义为Int的元组。map定义为二维布尔值数组,用来表示各个位置是否已经到达。然后,定义2个并行字典树映射,open和closed。其中,映射open保存接着需要被遍历的末端节点。映射closed保存所有已经被遍历过的节点。映射open使用恰当节点来初始化,用以从地图的一角开始搜索,并找到通往地图中心的路径。随后,并行地对映射open中的所有节点迭代遍历,直到没有节点可以遍历。每次一个节点被遍历时,将它从映射open中移除,并放置在映射closed中。一旦执行完成,输出从目标节点到初始节点的路径。 +(译者注:如扫雷,不断判断当前位置(末端节点)上下左右是否为地雷(二维布尔数组),从起始位置逐渐向外扩张。) + + val length = 1000 + + //定义节点类型 + type Node = (Int, Int); + type Parent = (Int, Int); + + //定义节点类型上的操作 + def up(n: Node) = (n._1, n._2 - 1); + def down(n: Node) = (n._1, n._2 + 1); + def left(n: Node) = (n._1 - 1, n._2); + def right(n: Node) = (n._1 + 1, n._2); + + // 创建一个map及一个target + val target = (length / 2, length / 2); + val map = Array.tabulate(length, length)((x, y) => (x % 3) != 0 || (y % 3) != 0 || (x, y) == target) + def onMap(n: Node) = n._1 >= 0 && n._1 < length && n._2 >= 0 && n._2 < length + + //open列表 - 前节点 + // closed 列表 - 已处理的节点 + val open = ParTrieMap[Node, Parent]() + val closed = ParTrieMap[Node, Parent]() + + // 加入一对起始位置 + open((0, 0)) = null + open((length - 1, length - 1)) = null + open((0, length - 1)) = null + open((length - 1, 0)) = null + + // 贪婪广度优先算法路径搜索 + while (open.nonEmpty && !open.contains(target)) { + for ((node, parent) <- open) { + def expand(next: Node) { + if (onMap(next) && map(next._1)(next._2) && !closed.contains(next) && !open.contains(next)) { + open(next) = node + } + } + expand(up(node)) + expand(down(node)) + expand(left(node)) + expand(right(node)) + closed(node) = parent + open.remove(node) + } + } + + // 打印路径 + var pathnode = open(target) + while (closed.contains(pathnode)) { + print(pathnode + "->") + pathnode = closed(pathnode) + } + println() +例如,GitHub上个人生游戏的示例,就是使用Ctries去选择性地模拟人生游戏中当前活跃的机器人([4](https://github.com/axel22/ScalaDays2012-TrieMap))。它还基于Swing实现了模拟的人生游戏的视觉化,以便很直观地观察到调整参数是如何影响执行。 + +并发字典树也支持线性化、无锁、及定时快照操作。这些操作会利用特定时间点上的所有元素来创建新并发 字典树。因此,实际上捕获了特定时间点上的字典树状态。快照操作仅仅为并发字典树生成一个新的根。子序列采用惰性更新的策略,只重建与更新相关的部分,其余部分保持原样。首先,这意味着,由于不需要拷贝元素,自动快照操作资源消耗较少。其次,写时拷贝优化策略只拷贝并发字典树的部分,后续的修改可以横向展开。readOnlySnapshot方法比Snapshot方法效率略高,但它返回的是无法修改的只读的映射。并发字典树也支持线性化,定时清除操作基于快照机制。了解更多关于并发字典树及快照的工作方式,请参阅 ([1](http://infoscience.epfl.ch/record/166908/files/ctries-techreport.pdf)) 和 ([2](http://lampwww.epfl.ch/~prokopec/ctries-snapshot.pdf)). + +并发字典树的迭代器基于快照实现。在迭代器对象被创建之前,会创建一个并发字典树的快照,所以迭代器只在字典树的快照创建时的元素中进行遍历。当然,迭代器使用只读快照。 + +size操作也基于快照。一种直接的实现方式是,size调用仅仅生成一个迭代器(也就是快照),通过遍历来计数。这种方式的效率是和元素数量线性相关的。然而,并发字典树通过缓存其不同部分优化了这个过程,由此size方法的时间复杂度降低到了对数时间。实际上这意味着,调用过一次size方法后,以后对call的调用将只需要最少量的工作。典型例子就是重新计算上次调用size之后被修改了的字典数分支。此外,并行的并发字典树的size计算也是并行的。 + +**引用** + +[缓存感知无锁并发哈希字典树][1](http://infoscience.epfl.ch/record/166908/files/ctries-techreport.pdf) +[具有高效非阻塞快照的并发字典树][2](http://lampwww.epfl.ch/~prokopec/ctries-snapshot.pdf) +[计算平方根的方法][3](http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) +[人生游戏模拟程序][4](https://github.com/axel22/ScalaDays2012-TrieMap)(译注:类似大富翁的棋盘游戏) + diff --git a/cn/overviews/parallel-collections/Configuring_Parallel_Collections.md b/cn/overviews/parallel-collections/Configuring_Parallel_Collections.md new file mode 100644 index 0000000000..c8f4e7fad9 --- /dev/null +++ b/cn/overviews/parallel-collections/Configuring_Parallel_Collections.md @@ -0,0 +1,58 @@ +--- +layout: overview-large +title: 配置并行集合 + +disqus: true + +partof: parallel-collections +num: 7 +language: cn +--- + + +### 任务支持 + +并行集合是以操作调度的方式建模的。每一个并行集合都有一个任务支持对象作为参数,该对象负责处理器任务的调度和负载均衡。 + +任务支持对象内部有一个线程池实例的引用,并且决定着任务细分成更小任务的方式和时机。更多关于这方面的实现细节,请参考技术手册 [[1](http://infoscience.epfl.ch/record/165523/files/techrep.pdf)]。 + +并行集合的任务支持现在已经有一些可用的实现。ForkJoinTaskSupport内部使用fork-join池,并被默认用与JVM1.6以及更高的版本。ThreadPoolTaskSupport 效率更低,是JVM1.5和不支持fork-join池的JVM的回退。ExecutionContextTaskSupport 使用在scala.concurrent中默认的执行上下文实现,并且它重用在scala.concurrent使用的线程池(根据JVM版本,可能是fork join 池或线程池执行器)。执行上下文的任务支持被默认地设置到每个并行集合中,所以并行集合重用相同的fork-join池。 + +以下是一种改变并行集合的任务支持的方法: + + scala> import scala.collection.parallel._ + import scala.collection.parallel._ + + scala> val pc = mutable.ParArray(1, 2, 3) + pc: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3) + + scala> pc.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(2)) + pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ForkJoinTaskSupport@4a5d484a + + scala> pc map { _ + 1 } + res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) + +以上代码配置并行集合使用parallelism 级别为2的fork-join池。配置并行集合使用线程池执行器: + + scala> pc.tasksupport = new ThreadPoolTaskSupport() + pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ThreadPoolTaskSupport@1d914a39 + + scala> pc map { _ + 1 } + res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) + +当一个并行集合被序列化,它的任务支持域免于序列化。当对一个并行集合反序列化时,任务支持域被设置为默认值——执行上下文的任务支持。 + +通过继承TaskSupport 特征并实现下列方法,可实现一个典型的任务支持: + + def execute[R, Tp](task: Task[R, Tp]): () => R + + def executeAndWaitResult[R, Tp](task: Task[R, Tp]): R + + def parallelismLevel: Int + +execute方法异步调度任务并且返回等待计算结果的未来状态。executeAndWait 方法功能一样,但只当任务完成时才返回。parallelismLevel 简单地返回任务支持用于调度任务的处理器目标数量。 + +**引用** + +1. [On a Generic Parallel Collection Framework, June 2011](http://infoscience.epfl.ch/record/165523/files/techrep.pdf) + diff --git a/cn/overviews/parallel-collections/Creating_Custom_Parallel_Collections.md b/cn/overviews/parallel-collections/Creating_Custom_Parallel_Collections.md new file mode 100644 index 0000000000..ed27314196 --- /dev/null +++ b/cn/overviews/parallel-collections/Creating_Custom_Parallel_Collections.md @@ -0,0 +1,192 @@ +--- +layout: overview-large +title: 创建自定义并行容器 + +disqus: true + +partof: parallel-collections +num: 6 +language: cn +--- + +### 没有组合器的并行容器 + +正如可以定义没有构造器的个性化序列容器一样,我们也可以定义没有组合器的并行容器。没有组合器的结果就是容器的transformer方法(比如,map,flatMap,collect,filter,……)将会默认返回一个标准的容器类型,该类型是在继承树中与并行容器最接近的一个。举个例子,range类没有构造器,因此一个range类的元素映射会生成一个vector。 + +在接下来的例子中,我们定义了一个并行字符串容器。因为字符串在逻辑上是非可变序列,我们让并行字符串继承 immutable.ParSeq[Char]: + + class ParString(val str: String) + extends immutable.ParSeq[Char] { + +接着,我们定义非可变序列必须实现的方法: + + def apply(i: Int) = str.charAt(i) + + def length = str.length + +我们还得定义这个并行容器的序列化副本。这里,我们返回WrappedString类: + + def seq = new collection.immutable.WrappedString(str) + +最后,我们必须为并行字符串容器定义一个splitter。我们给这个splitter起名ParStringSplitter,让它继承一个序列splitter,即,SeqSplitter[Char]: + + def splitter = new ParStringSplitter(str, 0, str.length) + + class ParStringSplitter(private var s: String, private var i: Int, private val ntl: Int) + extends SeqSplitter[Char] { + + final def hasNext = i < ntl + + final def next = { + val r = s.charAt(i) + i += 1 + r + } + +上面的代码中,ntl为字符串的总长,i是当前位置,s是字符串本身。 + +除了next和hasNext方法,并行容器迭代器,或者称它为splitter,还需要序列容器迭代器中的一些其他的方法。首先,他们有一个方法叫做 remaining,它返回这个分割器尚未遍历的元素数量。其次,需要dup方法用于复制当前的分割器。 + + def remaining = ntl - i + + def dup = new ParStringSplitter(s, i, ntl) + +最后,split和psplit方法用于创建splitter,这些splitter 用来遍历当前分割器的元素的子集。split方法,它返回一个分割器的序列,它用来遍历互不相交,互不重叠的分隔器元素子集,其中没有一个是空的。如果当前分割器有1个或更少的元素,然后就返回一个序列的分隔器。psplit方法必须返回和指定sizes参数个数一致的分割器序列。如果sizes参数指定元素数量小于当前分配器,然后一个带有额外的分配器就会附加在分配器的尾部。如果sizes参数比在当前分配器的剩余元素大很多,需要更多的元素,它将为每个分配器添加一个空的分配器。最后,调用split或psplit方法使得当前分配器无效。 + + def split = { + val rem = remaining + if (rem >= 2) psplit(rem / 2, rem - rem / 2) + else Seq(this) + } + + def psplit(sizes: Int*): Seq[ParStringSplitter] = { + val splitted = new ArrayBuffer[ParStringSplitter] + for (sz <- sizes) { + val next = (i + sz) min ntl + splitted += new ParStringSplitter(s, i, next) + i = next + } + if (remaining > 0) splitted += new ParStringSplitter(s, i, ntl) + splitted + } + } + } + +综上所述,split方法是通过psplit来实现的,它常用于并行序列计算中。由于不需要psplit,并行映射、集合、迭代器的实现,通常就更容易些。 + +因此,我们得到了一个并行字符串类。它唯一的缺点是,调用类似filter等转换方法不是生成并行串,而是生成并行向量,这可能是个折中的选择 - filter方法如果生成串而非向量,代价也许是昂贵的。 + +### 带组合子的并行容器 + +假设我们想要从并行字符串中过滤掉某些字符,例如,去除其中的逗号。如上所述,调用filter方法会生成一个并行向量,但是我们需要得到的是一个并行串(因为API中的某些接口可能需要一个连续的字符串来作为参数)。 + +为了避免这种情况的发生,我们需要为并行串容器写一个组合子。同时,我们也将继承ParSeqLike trait,以确保filter的返回类型是更具体的类型 - - ParString而不是ParSeq[Char]。ParSeqLike的第三个参数,用于指定并行容器对应的序列的类型(这点和序列化的 *Like trait 不同,它们只有两个类型参数)。 + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] + +所有的方法仍然和以前一样,只是我们会增加一个额外的protected方法newCombiner,它在内部被filter方法调用。 + + protected[this] override def newCombiner: Combiner[Char, ParString] = new ParStringCombiner + +接下来我们定义ParStringCombiner类。组合子是builders的子类型,它们引进了名叫combine的方法,该方法接收另一个组合子作为参数,并返回一个新的组合子,该新的组合子包含了当前组合子和参数中的组合子中的所有元素。当前组合子和参数中的组合子在调用combine方法之后将会失效。如果参数中的组合子和当前的组合子是同一个对象,那么combine方法仅仅返回当前的组合子。该方法通常情况下是高效的,最坏情况下时间复杂度为元素个数的对数,因为它在一次并行计算中会被多次调用。 + +我们的ParStringCombiner会在内部维护一个字符串生成器的序列。它通过在序列的最后一个字符串builder中增加一个元素的方式,来实现+=方法。并且通过串联当前和参数中的组合子的串builder列表来实现combine方法。result方法,在并行计算结束后被调用,它会通过将所有字符串生成器添加在一起来产生一个并行串。这样一来,元素只在末端被复制一次,避免了每调一次combine方法就被复制一次。理想情况下,我们想并行化这一进程,并在它们并行时候进行复制(并行数组正在被这样做),但没有办法检测到的字符串的内部表现,这是我们能做的最好的 - 我们不得不忍受这种顺序化的瓶颈。 + + private class ParStringCombiner extends Combiner[Char, ParString] { + var sz = 0 + val chunks = new ArrayBuffer[StringBuilder] += new StringBuilder + var lastc = chunks.last + + def size: Int = sz + + def +=(elem: Char): this.type = { + lastc += elem + sz += 1 + this + } + + def clear = { + chunks.clear + chunks += new StringBuilder + lastc = chunks.last + sz = 0 + } + + def result: ParString = { + val rsb = new StringBuilder + for (sb <- chunks) rsb.append(sb) + new ParString(rsb.toString) + } + + def combine[U <: Char, NewTo >: ParString](other: Combiner[U, NewTo]) = if (other eq this) this else { + val that = other.asInstanceOf[ParStringCombiner] + sz += that.sz + chunks ++= that.chunks + lastc = chunks.last + this + } + } + +### 大体上我如何来实现一个组合子? + +没有现成的秘诀——它的实现依赖于手头上的数据结构,通常在实现上也需要一些创造性。但是,有几种方法经常被采用: + +1. 连接和合并。一些数据结构在这些操作上有高效的实现(经常是对数级的)。如果手头的容器可以由这样的一些数据结构支撑,那么它们的组合子就可以是容器本身。 Finger trees,ropes和各种堆尤其适合使用这种方法。 + +2. 两阶段赋值,是在并行数组和并行哈希表中采用的方法,它假设元素子集可以被高效的划分到连续的排序桶中,这样最终的数据结构就可以并行的构建。第一阶段,不同的处理器独立的占据这些桶,并把这些桶连接在一起。第二阶段,数据结构被分配,不同的处理器使用不相交的桶中的元素并行地占据部分数据结构。必须注意的是,各处理器修改的部分不能有交集,否则,可能会产生微妙的并发错误。正如在前面的篇幅中介绍的,这种方法很容易应用到随机存取序列。 + +3. 一个并发的数据结构。尽管后两种方法实际上不需要数据结构本身有同步原语,它们假定数据结构能够被不修改相同内存的不同处理器,以并发的方式建立。存在大量的并发数据结构,它们可以被多个处理器安全的修改——例如,并发skip list,并发哈希表,split-ordered list,并发 avl树等等。需要注意的是,并发的数据结构应该提供水平扩展的插入方法。对于并发并行容器,组合器可以是容器本身,并且,完成一个并行操作的所有的处理器会共享一个单独的组合器实例。 + +### 使用容器框架整合 + +ParString类还没有完成。虽然我们已经实现了一个自定义的组合子,它将会被类似filter,partition,takeWhile,或者span等方式使用,但是大部分transformer方法都需要一个隐式的CanBuildFrom出现(Scala collections guide有详细的解释)。为了让ParString可能,并且完全的整合到容器框架中,我们需要为其掺入额外的一个叫做GenericParTemplate的trait,并且定义ParString的伴生对象。 + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with GenericParTemplate[Char, ParString] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] { + + def companion = ParString +在这个伴生对象内部,我们隐式定义了CanBuildFrom。 + + object ParString { + implicit def canBuildFrom: CanCombineFrom[ParString, Char, ParString] = + new CanCombinerFrom[ParString, Char, ParString] { + def apply(from: ParString) = newCombiner + def apply() = newCombiner + } + + def newBuilder: Combiner[Char, ParString] = newCombiner + + def newCombiner: Combiner[Char, ParString] = new ParStringCombiner + + def apply(elems: Char*): ParString = { + val cb = newCombiner + cb ++= elems + cb.result + } + } + +### 进一步定制——并发和其他容器 + +实现一个并发容器(与并行容器不同,并发容器是像collection.concurrent.TrieMap一样可以被并发修改的)并不总是简单明了的。尤其是组合器,经常需要仔细想想。到目前为止,在大多数描述的并行容器中,组合器都使用两步评估。第一步元素被不同的处理器加入到组合器中,组合器被合并在一起。第二步,在所有元素完成处理后,结果容器就被创建。 + +组合器的另一种方式是把结果容器作为元素来构建。前提是:容器是线程安全的——组合器必须允许并发元素插入。这样的话,一个组合器就可以被所有处理器共享。 + +为了使一个并发容器并行化,它的组合器必须重写canBeShared方法以返回真。这会保证当一个并行操作被调用,只有一个组合器被创建。然后,+=方法必须是线程安全的。最后,如果当前的组合器和参数组合器是相同的,combine方法仍然返回当前的组合器,要不然会自动抛出异常。 + +为了获得更好的负载均衡,Splitter被分割成更小的splitter。默认情况下,remaining方法返回的信息被用来决定何时停止分割splitter。对于一些容器而言,调用remaining方法是有花销的,一些其他的方法应该被使用来决定何时分割splitter。在这种情况下,需要重写splitter的shouldSplitFurther方法。 + +如果剩余元素的数量比容器大小除以8倍并行级别更大,默认的实现将拆分splitter。 + + def shouldSplitFurther[S](coll: ParIterable[S], parallelismLevel: Int) = + remaining > thresholdFromSize(coll.size, parallelismLevel) + +同样的,一个splitter可以持有一个计数器,来计算splitter被分割的次数。并且,如果split次数超过3+log(并行级别),shouldSplitFurther将直接返回true。这避免了必须去调用remaining方法。 + +此外,对于一个指定的容器如果调用remaining方法开销不低(比如,他需要评估容器中元素的数量),那么在splitter中的方法isRemainingCheap就应该被重写并返回false。 + +最后,若果在splitter中的remaining方法实现起来极其麻烦,你可以重写容器中的isStrictSplitterCollection方法,并返回false。虽然这些容器将不能够执行一些严格依赖splitter的方法,比如,在remaining方法中返回一个正确的值。重点是,这并不影响 for-comprehension 中使用的方法。 + diff --git a/cn/overviews/parallel-collections/Measuring_Performance.md b/cn/overviews/parallel-collections/Measuring_Performance.md new file mode 100644 index 0000000000..ee2d5a7bc3 --- /dev/null +++ b/cn/overviews/parallel-collections/Measuring_Performance.md @@ -0,0 +1,181 @@ +--- +layout: overview-large +title: 测量性能 + +disqus: true + +partof: parallel-collections +num: 8 +language: cn +--- + +### 在JVM上的性能 + +对JVM性能模型的评论常常令人费解,其结论也往往不易理解。由于种种原因,代码也可能不像预期的那样高性能、可扩展。在这里,我们提供了一些示例。 + +其中一个原因是JVM应用程序的编译过程不同于静态编译语言(见[[2](http://www.ibm.com/developerworks/library/j-jtp12214/)])。Java和Scala的编译器将源代码转换为JVM的字节码,做了非常少的优化。大多数现代JVM,运行时,会把字节码转化成相应机器架构的机器代码。这个过程被称为即时编译。由于追求运行速度,所以实时编译的代码优化程度较低。为了避免重新编译,所谓的HotSpot编译器只优化了部分经常被运行的代码。这对于基准程序作者来说,这意味着程序每次运行时的性能都可能不同。在同一个JVM实例中多次执行一段相同的代码(比如一个方法)可能会得到非常不同的性能结果,这取决于这段代码在运行过程中是否被优化。另外,在测量某些代码的执行时间时其中可能包含JIT编译器对代码进行优化的时间,因此可能得到不一致的结果。 + +另一个在JVM上隐藏执行的是内存自动管理。每隔一段时间,程序的运行就被阻塞并且启动垃圾收集器。如果被进行基准测试的程序分配了任何堆内存(大部分JVM程序都会分配),垃圾收集器将会工作,因此可能会影响测量结果。为了缓冲垃圾收集的影响,被测量的程序应该运行多次以便触发多次垃圾回收。 + +性能恶化的常见原因之一是将原始类型作为参数传递给泛型方法时发生的隐式装箱和拆箱。在运行时,原始类型被转换为封装对象,这样它们就可以作为参数传给有泛型类型参数的方法。这会导致额外的空间分配并且运行速度会更慢,也会在堆中产生额外的垃圾。 + +就并行性能而言,一个常见的问题是存储冲突,因为程序员针对对象的内存分配没有做明确的控制。事实上,由于GC的影响,冲突可以发生在应用程序生命期的最后,在对象被移出内存后。在编写基准测试时这种影响需要被考虑到。 + +### 微基准测试的例子 + +有几种方法可以在测试中避免上述影响。首先,目标微基准测试必须被执行足够多次来确保实时编译器将程序编译为机器码并被优化过。这就是所谓的预热阶段。 + +微基准测试本身需要被运行在单独的JVM实例中,以便减少在程序不同部分或不相关的实时编译过程中针对对象分配的垃圾收集所带来的干扰。 + +微基准测试应该跑在会做更多积极优化的服务器版本的HotSpot JVM上。 + +最后,为了减少在基准测试中间发生垃圾回收的可能性,理想的垃圾回收周期应该发生在基准测试之前,并尽可能的推迟下一个垃圾回收周期。 + +scala.testing.Benchmark trait 是在Scala标准库中被预先定义的,并按前面提到的方式设计。下面是一个用于测试并行算法中映射操作的例子: + + import collection.parallel.mutable.ParTrieMap + import collection.parallel.ForkJoinTaskSupport + + object Map extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val partrie = ParTrieMap((0 until length) zip (0 until length): _*) + + partrie.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + partrie map { + kv => kv + } + } + } + +run方法包含了基准测试代码,重复运行时测量执行时间。上面的Map对象扩展了scala.testing.Benchmark trait,同时,参数par为系统的并行度,length为trie中元素数量的长度。 + +在编译上面的程序之后,可以这样运行: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10 + +server参数指定需要使用server类型的虚拟机。cp参数指定了类文件的路径,包含当前文件夹的类文件以及以及scala类库的jar包。参数-Dpar和-Dlength分别对应并行度和元素数量。最后,10意味着基准测试需要在同一个JVM中运行的次数。 + +在i7四核超线程处理器上将par的值设置为1、2、4、8并获得对应的执行时间。 + + Map$ 126 57 56 57 54 54 54 53 53 53 + Map$ 90 99 28 28 26 26 26 26 26 26 + Map$ 201 17 17 16 15 15 16 14 18 15 + Map$ 182 12 13 17 16 14 14 12 12 12 + +我们从上面的结果可以看到运行时间在最初的几次运行中是较高的,但是在代码被优化后时间就缩短了。另外,我们可以看到在这个例子中超线程带来的好处并不明显,从4线程到8线程的结果说明性能只有小幅提升。 + +### 多大的容器才应该使用并发? + +这是一个经常被问到的问题。答案是有些复杂的。 + +collection的大小所对应的实际并发消耗取决于很多因素。部分原因如下: + +- 硬件架构。不同的CPU类型具有不同的性能和可扩展能力。取决于硬件是否为多核或者是否有多个通过主板通信的处理器。 +- JVM的供应商及版本。在运行时不同的虚拟机应用不同的代码优化。它们的内存管理实现不同,同步技术也不同。有些不支持ForkJoinPool,转而使用ThreadPoolExecutor,这会导致更多的开销。 +- 元素负载。用于并行操作的函数或断言决定了每个元素的负载有多大。负载越小,并发运行时用来提高运行速度的元素数量就越高。 +- 特定的容器。例如:ParArray和ParTrieMap的分离器在遍历容器时有不同的速度,这意味着在遍历过程中要有更多的per-element work。 +- 特定的操作。例如:ParVector在转换方法中(比如filter)要比在存取方法中(比如foreach)慢得多。 +- 副作用。当同时修改内存区域或者在foreach、map等语句中使用同步时,就会发生竞争。 +- 内存管理。当分配大量对象时垃圾回收机制就会被触发。GC循环会消耗多长时间取决于新对象的引用如何进行传递。 + +即使单独的来看,对上面的问题进行推断并给出关于容器应有大小的明确答案也是不容易的。为了粗略的说明容器的应有大小,我们给出了一个无副作用的在i7四核处理器(没有使用超线程)和JDK7上运行的并行矢量减(在这个例子中进行的是求和)处理性能的例子: + + import collection.parallel.immutable.ParVector + + object Reduce extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val parvector = ParVector((0 until length): _*) + + parvector.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + parvector reduce { + (a, b) => a + b + } + } + } + + object ReduceSeq extends testing.Benchmark { + val length = sys.props("length").toInt + val vector = collection.immutable.Vector((0 until length): _*) + + def run = { + vector reduce { + (a, b) => a + b + } + } + + } +首先我们设定在元素数量为250000的情况下运行基准测试,在线程数设置为1、2、4的情况下得到了如下结果: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=250000 Reduce 10 10 + Reduce$ 54 24 18 18 18 19 19 18 19 19 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=250000 Reduce 10 10 + Reduce$ 60 19 17 13 13 13 13 14 12 13 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=250000 Reduce 10 10 + Reduce$ 62 17 15 14 13 11 11 11 11 9 +然后我们将元素数量降低到120000,使用4个线程来比较序列矢量减运行的时间: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Reduce 10 10 + Reduce$ 54 10 8 8 8 7 8 7 6 5 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=120000 ReduceSeq 10 10 + ReduceSeq$ 31 7 8 8 7 7 7 8 7 8 +在这个例子中,元素数量为120000时看起来正处于阈值附近。 + +在另一个例子中,我们使用mutable.ParHashMap和map方法(一个转换方法),并在同样的环境中运行下面的测试程序: + + import collection.parallel.mutable.ParHashMap + + object Map extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val phm = ParHashMap((0 until length) zip (0 until length): _*) + + phm.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + phm map { + kv => kv + } + } + } + + object MapSeq extends testing.Benchmark { + val length = sys.props("length").toInt + val hm = collection.mutable.HashMap((0 until length) zip (0 until length): _*) + + def run = { + hm map { + kv => kv + } + } + } +在元素数量为120000、线程数量从1增加至4的时候,我们得到了如下结果: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=120000 Map 10 10 + Map$ 187 108 97 96 96 95 95 95 96 95 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=120000 Map 10 10 + Map$ 138 68 57 56 57 56 56 55 54 55 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Map 10 10 + Map$ 124 54 42 40 38 41 40 40 39 39 + +现在,如果我们将元素数量降低到15000来跟序列化哈希映射做比较: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=15000 Map 10 10 + Map$ 41 13 10 10 10 9 9 9 10 9 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=15000 Map 10 10 + Map$ 48 15 9 8 7 7 6 7 8 6 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=15000 MapSeq 10 10 + MapSeq$ 39 9 9 9 8 9 9 9 9 9 + +对这个容器和操作来说,当元素数量大于15000的时候采用并发是有意义的(通常情况下,对于数组和向量来说使用更少的元素来并行处理hashmap和hashset是可行的但不是必须的)。 + +**引用** + +1. [Anatomy of a flawed microbenchmark,Brian Goetz](http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html) +2. [Dynamic compilation and performance measurement, Brian Goetz](http://www.ibm.com/developerworks/library/j-jtp12214/) + diff --git a/cn/overviews/parallel-collections/Overview.md b/cn/overviews/parallel-collections/Overview.md new file mode 100644 index 0000000000..262b99879a --- /dev/null +++ b/cn/overviews/parallel-collections/Overview.md @@ -0,0 +1,165 @@ +## 概述(Overview) + +Aleksandar Prokopec, Heather Miller + +### 动机 : + +近年来,处理器厂家在单核向多核架构迁移的过程中,学术界和工业界都认为当红的并行编程仍是一个艰巨的挑战。 + +在Scala标准库中包含的并行容器通过免去并行化的底层细节,以方便用户并行编程,同时为他们提供一个熟悉而简单的高层抽象。希望隐藏在抽象容器之后的隐式并行性将带来可靠的并行执行,并进一步靠近主流开发者的工作流程。 + +原理其实很简单-容器是抽象编程中被广泛熟识和经常使用的类,并且考虑到他们的规则性,容器能够使程序高效且透明的并行化。通过使用户能够在并行操作有序容器的同时改变容器序列,Scala的并行容器在使代码能够更容易的并行化方面做了很大改进。 + +下面是个序列的例子,这里我们在某个大的容器上执行一个一元运算: + + val list = (1 to 10000).toList + list.map(_ + 42) +为了并行的执行同样的操作,我们只需要简单的调用序列容器(列表)的par方法。这样,我们就可以像通常使用序列容器的方式那样来使用并行容器。上面的例子可以通过执行下面的来并行化: + + list.par.map(_ + 42) +Scala的并行容器库设计创意般的同Scala的(序列)容器库(从2.8引入)密切的整合在一起。在Scala(序列)容器库中,它提供了大量重要的数据结构对应的东西,包括: + +- ParArray +- ParVector +- mutable.ParHashMap +- mutable.ParHashSet +- immutable.ParHashMap +- immutable.ParHashSet +- ParRange +- ParTrieMap (collection.concurrent.TrieMaps are new in 2.10) + +在通常的架构之外,Scala的并行容器库也同序列容器库(sequential collections)一样具有可扩展性。这就是说,像通常的序列容器那样,用户可以整合他们自己的容器类型,并且自动继承所有的可用在别的并行容器(在标准库里的)的预定义(并行)操作。 + +### 下边是一些例子 + +为了说明并行容器的通用性和实用性,我们提供几个简单的示例用法,所有这些都被透明的并行执行。 + +提示:随后某些例子操作小的容器,实际中不推荐这样做。他们提供的示例仅为演示之用。一般来说,当容器的尺寸比较巨大(通常为成千上万个元素时)时,加速才会比较明显。(关于并行容器的尺寸和性能更多的信息,请参见) + +#### map + +使用parallel map来把一个字符串容器变为全大写字母: + + scala> val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + astNames: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> lastNames.map(_.toUpperCase) + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) + +#### fold + +通过fold计算一个ParArray中所有数的累加值: + + scala> val parArray = (1 to 1000000).toArray.par + parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... + + scala> parArray.fold(0)(_ + _) + res0: Int = 1784293664 + +#### filter + +使用并行过滤器来选择按字母顺序排在“K”之后的姓名。(译者注:这个例子有点问题,应该是排在“J”之后的) + + scala> val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + astNames: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> lastNames.filter(_.head >= 'J') + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) + +## 创建一个并行容器 + +并行容器(parallel collections)同顺序容器(sequential collections)完全一样的被使用,唯一的不同是要怎样去获得一个并行容器。 + +通常,我们有两种方法来创建一个并行容器: + +第一种,通过使用new关键字和一个适当的import语句: + + import scala.collection.parallel.immutable.ParVector + val pv = new ParVector[Int] + +第二种,通过从一个顺序容器转换得来: + + val pv = Vector(1,2,3,4,5,6,7,8,9).par + +这里需要着重强调的是这些转换方法:通过调用顺序容器(sequential collections)的par方法,顺序容器(sequential collections)可以被转换为并行容器;通过调用并行容器的seq方法,并行容器可以被转换为顺序容器。 + +注意:那些天生就有序的容器(意思是元素必须一个接一个的访问),像lists,queues和streams,通过拷贝元素到类似的并行容器中被转换为它们的并行对应物。例如List--被转换为一个标准的不可变的并行序列中,就是ParVector。当然,其他容器类型不需要这些拷贝的开销,比如:Array,Vector,HashMap等等。 + +关于并行容器的转换的更多信息请参见 [conversions](http://docs.scala-lang.org/overviews/parallel-collections/conversions.html) 和 [concrete parallel collections classes](http://docs.scala-lang.org/overviews/parallel-collections/concrete-parallel-collections.html)章节 + +## 语义(semantic) + +尽管并行容器的抽象概念很像通常的顺序容器,重要的是要注意它的语义的不同,特别是关于副作用(side-effects)和无关操作(non-associative operations)。 + +为了看看这是怎样的情况,首先,我们设想操作是如何被并行的执行。从概念上讲,Scala的并行容器框架在并行容器上通过递归的“分解"给定的容器来并行化一个操作,在并行中,容器的每个部分应用一个操作,然后“重组”所有这些并行执行的结果。 + +这些并发和并行容器的“乱序”语义导致以下两个影响: + +1. 副作用操作可能导致结果的不确定性 +2. 非关联(non-associative)操作导致不确定性 + +### 副作用操作 + +为了保持确定性,考虑到并行容器框架的并发执行的语义,一般应该避免执行那些在容器上引起副作用的操作。一个简单的例子就是使用访问器方法,像在 foreach 之外来增加一个 var 定义然后传递给foreach。 + + scala> var sum = 0 + sum: Int = 0 + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.foreach(sum += _); sum + res01: Int = 467766 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res02: Int = 457073 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res03: Int = 468520 + +从上述例子我们可以看到虽然每次 sum 都被初始化为0,在list的foreach每次调用之后,sum都得到不同的值。这个不确定的源头就是数据竞争 -- 同时读/写同一个可变变量(mutable variable)。 + +在上面这个例子中,可能同时有两个线程在读取同一个sum的值,某些操作花了些时间后,它们又试图写一个新的值到sum中,可能的结果就是某个有用的值被覆盖了(因此丢失了),如下表所示: + + 线程A: 读取sum的值, sum = 0 sum的值: 0 + 线程B: 读取sum的值, sum = 0 sum的值: 0 + 线程A: sum 加上760, 写 sum = 760 sum的值: 760 + 线程B: sum 加上12, 写 sum = 12 sum的值: 12 + +上面的示例演示了一个场景:两个线程读相同的值:0。在这种情况下,线程A读0并且累计它的元素:0+760,线程B,累计0和它的元素:0+12。在各自计算了和之后,它们各自把计算结果写入到sum中。从线程A到线程B,线程A写入后,马上被线程B写入的值覆盖了,值760就完全被覆盖了(因此丢失了)。 + +### 非关联(non-associative)操作 + +对于”乱序“语义,为了避免不确定性,也必须注意只执行相关的操作。这就是说,给定一个并行容器:pcoll,我们应该确保什么时候调用一个pcoll的高阶函数,例如:pcoll.reduce(func),被应用到pcoll元素的函数顺序是任意的。一个简单但明显不可结合(non-associative)例子是减法运算: + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.reduce(_-_) + res01: Int = -228888 + + scala> list.reduce(_-_) + res02: Int = -61000 + + scala> list.reduce(_-_) + res03: Int = -331818 + +在上面这个例子中,我们对 ParVector[Int]调用 reduce 函数,并给他 _-_ 参数(简单的两个非命名元素),从第二个减去第一个。因为并行容器(parallel collections)框架创建线程来在容器的不同部分执行reduce(-),而由于执行顺序的不确定性,两次应用reduce(-)在并行容器上很可能会得到不同的结果。 + +注意:通常人们认为,像不可结合(non-associative)作,不可交换(non-commutative)操作传递给并行容器的高阶函数同样导致非确定的行为。但和不可结合是不一样的,一个简单的例子是字符串联合(concatenation),就是一个可结合但不可交换的操作: + + scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par + strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) + + scala> val alphabet = strings.reduce(_++_) + alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz + +并行容器的“乱序”语义仅仅意味着操作被执行是没有顺序的(从时间意义上说,就是非顺序的),并不意味着结果的重“组合”也是乱序的(从空间意义上)。恰恰相反,结果一般总是按序组合的 -- 一个并行容器被分成A,B,C三部分,按照这个顺序,将重新再次按照A,B,C的顺序组合。而不是某种其他随意的顺序如B,C,A。 + +关于并行容器在不同的并行容器类型上怎样进行分解和组合操作的更多信息,请参见。 diff --git a/cn/overviews/parallel-collections/Parallel_Collection_Conversions.md b/cn/overviews/parallel-collections/Parallel_Collection_Conversions.md new file mode 100644 index 0000000000..9f94de3193 --- /dev/null +++ b/cn/overviews/parallel-collections/Parallel_Collection_Conversions.md @@ -0,0 +1,52 @@ +--- +layout: overview-large +title: 并行容器的转换 + +disqus: true + +partof: parallel-collections +num: 3 +language: cn +--- + +### 顺序容器和并行容器之间的转换 + +每个顺序容器都可以使用par方法转换成它的并行形式。某些顺序容器具有直接的并行副本。对于这些容器,转换是非常高效的(它所需的时间是个常量),因为顺序容器和并行容器具有相同的数据结构上的表现形式(其中一个例外是可变hash map和hash set,在第一次调用par进行转换时,消耗略高,但以后对par的调用将只消耗常数时间)。要注意的是,对于可变容器,如果顺序容器和并行容器共享底层数据结构,那么对顺序容器的修改也会在他的并行副本中可见。 + +| 顺序 |并行 | +|------|-----| +|可变性(mutable)| | +|Array|ParArray| +|HashMap| ParHashMap| +|HashSet| ParHashSet| +|TrieMap| ParTrieMap| +|不可变性(immutable)| | +|Vector | ParVector| +|Range | ParRange| +|HashMap | ParHashMap| +|HashSet | ParHashSet| + +其他容器,如列表(list),队列(queue)及流(stream),从“元素必须逐个访问”这个意义上来讲,天生就是顺序容器。它们可以通过将元素拷贝到类似的并行容器的方式转换成其并行形式。例如,函数式列表可以转换成标准的非可变并行序列,即并行向量。 + +所有的并行容器都可以用 seq 方法转换成其顺序形式。从并行容器转换成顺序容器的操作总是很高效(耗费常数时间)。在可变并行容器上调用seq方法,会生成一个顺序容器,并使用相同的存储空间。对其中一个容器的更新会同时反映到另一个容器上。 + +### 不同类型容器之间的转换 + +通过顺序容器和并行容器之间的正交变换,容器可以在不同容器类型之间相互转换。例如,调用toSeq会将顺序集合转变成顺序序列,而在并行集合上调用toSeq,会将它转换成一个并行序列。基本规律是:如果存在一个X的并行版本,那么toX方法会将容器转换成ParX容器。 + +下面是对所有转换方法的总结: + +|方法 | 返回值类型 | +|----------|-----------| +|toArray | Array | +|toList | List | +|toIndexedSeq | IndexedSeq | +|toStream | Stream | +|toIterator | Iterator | +|toBuffer | Buffer | +|toTraversable | GenTraverable | +|toIterable | ParIterable | +|toSeq | ParSeq | +|toSet | ParSet | +|toMap | ParMap | +