diff --git a/_zh-cn/overviews/index.md b/_zh-cn/overviews/index.md new file mode 100644 index 0000000000..a363ab3b38 --- /dev/null +++ b/_zh-cn/overviews/index.md @@ -0,0 +1,53 @@ +--- +layout: guides-index +language: zh-cn +title: 目录 +--- + +
+

核心库

+
+ * Scala的容器类 + * [简介](/zh-cn/overviews/collections/introduction.html) + * [Mutable和Immutable集合](/zh-cn/overviews/collections/overview.html) + * [Trait Traversable](/zh-cn/overviews/collections/trait-traversable.html) + * [Trait Iterable](/zh-cn/overviews/collections/trait-iterable.html) + * [序列trait:Seq、IndexedSeq 及 LinearSeq](/zh-cn/overviews/collections/seqs.html) + * [集合](/zh-cn/overviews/collections/sets.html) + * [映射](/zh-cn/overviews/collections/maps.html) + * [具体的不可变集实体类](/zh-cn/overviews/collections/concrete-immutable-collection-classes.html) + * [具体的可变容器类](/zh-cn/overviews/collections/concrete-mutable-collection-classes.html) + * [数组](/zh-cn/overviews/collections/arrays.html) + * [字符串](/zh-cn/overviews/collections/strings.html) + * [性能特点](/zh-cn/overviews/collections/performance-characteristics.html) + * [等价性](/zh-cn/overviews/collections/equality.html) + * [视图](/zh-cn/overviews/collections/views.html) + * [Iterators](/zh-cn/overviews/collections/iterators.html) + * [从头定义新容器](/zh-cn/overviews/collections/creating-collections-from-scratch.html) + * [Java和Scala容器的转换](/zh-cn/overviews/collections/conversions-between-java-and-scala-collections.html) + * [Scala 2.7迁移指南](/zh-cn/overviews/collections/migrating-from-scala-27.html) + * [Scala容器类体系结构](/zh-cn/overviews/core/architecture-of-scala-collections.html) + * [字符串插值](/zh-cn/overviews/core/string-interpolation.html) New in 2.10 + * [Implicit Classes](/zh-cn/overviews/core/implicit-classes.html) New in 2.10 + * [Value Classes and Universal Traits](/zh-cn/overviews/core/value-classes.html) New in 2.10 + +
+

Parallel and Concurrent Programming

+
+ * [Future和Promise](/zh-cn/overviews/core/futures.html) New in 2.10 + * Scala的并行容器类 + * [概述](/zh-cn/overviews/parallel-collections/overview.html) + * [具体并行集合类](/zh-cn/overviews/parallel-collections/concrete-parallel-collections.html) + * [并行容器的转换](/zh-cn/overviews/parallel-collections/conversions.html) + * [并发字典树](/zh-cn/overviews/parallel-collections/ctries.html) + * [并行集合库的架构](/zh-cn/overviews/parallel-collections/architecture.html) + * [创建自定义并行容器](/zh-cn/overviews/parallel-collections/custom-parallel-collections.html) + * [配置并行集合](/zh-cn/overviews/parallel-collections/configuration.html) + * [测量性能](/zh-cn/overviews/parallel-collections/performance.html) + * [Scala Actors迁移指南](/zh-cn/overviews/core/actors-migration-guide.html) New in 2.10 + * [The Scala Actors API](/zh-cn/overviews/core/actors.html) Deprecated + +
+

Acknowledgements

+
+* [致谢名单](/zh-cn/overviews/thanks.html) diff --git a/_zh-cn/overviews/parallel-collections/performance.md b/_zh-cn/overviews/parallel-collections/performance.md index dabbdd2d57..3e41445962 100644 --- a/_zh-cn/overviews/parallel-collections/performance.md +++ b/_zh-cn/overviews/parallel-collections/performance.md @@ -1,21 +1,19 @@ --- -layout: multipage-overview +layout: overview-large title: 测量性能 -discourse: false +disqus: true partof: parallel-collections -overview-name: Parallel Collections - num: 8 language: zh-cn --- -## 在JVM上的性能 +### 在JVM上的性能 对JVM性能模型的评论常常令人费解,其结论也往往不易理解。由于种种原因,代码也可能不像预期的那样高性能、可扩展。在这里,我们提供了一些示例。 -其中一个原因是JVM应用程序的编译过程不同于静态编译语言(见[[2]])。Java和Scala的编译器将源代码转换为JVM的字节码,做了非常少的优化。大多数现代JVM,运行时,会把字节码转化成相应机器架构的机器代码。这个过程被称为即时编译。由于追求运行速度,所以实时编译的代码优化程度较低。为了避免重新编译,所谓的HotSpot编译器只优化了部分经常被运行的代码。这对于基准程序作者来说,这意味着程序每次运行时的性能都可能不同。在同一个JVM实例中多次执行一段相同的代码(比如一个方法)可能会得到非常不同的性能结果,这取决于这段代码在运行过程中是否被优化。另外,在测量某些代码的执行时间时其中可能包含JIT编译器对代码进行优化的时间,因此可能得到不一致的结果。 +其中一个原因是JVM应用程序的编译过程不同于静态编译语言(见[[2](http://www.ibm.com/developerworks/library/j-jtp12214/)])。Java和Scala的编译器将源代码转换为JVM的字节码,做了非常少的优化。大多数现代JVM,运行时,会把字节码转化成相应机器架构的机器代码。这个过程被称为即时编译。由于追求运行速度,所以实时编译的代码优化程度较低。为了避免重新编译,所谓的HotSpot编译器只优化了部分经常被运行的代码。这对于基准程序作者来说,这意味着程序每次运行时的性能都可能不同。在同一个JVM实例中多次执行一段相同的代码(比如一个方法)可能会得到非常不同的性能结果,这取决于这段代码在运行过程中是否被优化。另外,在测量某些代码的执行时间时其中可能包含JIT编译器对代码进行优化的时间,因此可能得到不一致的结果。 另一个在JVM上隐藏执行的是内存自动管理。每隔一段时间,程序的运行就被阻塞并且启动垃圾收集器。如果被进行基准测试的程序分配了任何堆内存(大部分JVM程序都会分配),垃圾收集器将会工作,因此可能会影响测量结果。为了缓冲垃圾收集的影响,被测量的程序应该运行多次以便触发多次垃圾回收。 @@ -23,7 +21,7 @@ language: zh-cn 就并行性能而言,一个常见的问题是存储冲突,因为程序员针对对象的内存分配没有做明确的控制。事实上,由于GC的影响,冲突可以发生在应用程序生命期的最后,在对象被移出内存后。在编写基准测试时这种影响需要被考虑到。 -## 微基准测试的例子 +### 微基准测试的例子 有几种方法可以在测试中避免上述影响。首先,目标微基准测试必须被执行足够多次来确保实时编译器将程序编译为机器码并被优化过。这就是所谓的预热阶段。 @@ -33,9 +31,43 @@ language: zh-cn 最后,为了减少在基准测试中间发生垃圾回收的可能性,理想的垃圾回收周期应该发生在基准测试之前,并尽可能的推迟下一个垃圾回收周期。 -正确基准的范例,你可以参考在[Scala library benchmarks][3]的源代码。 +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线程的结果说明性能只有小幅提升。 + +### 多大的容器才应该使用并发? 这是一个经常被问到的问题。答案是有些复杂的。 @@ -49,13 +81,101 @@ collection的大小所对应的实际并发消耗取决于很多因素。部分 - 副作用。当同时修改内存区域或者在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/) -## 引用 - -1. [Anatomy of a flawed microbenchmark, Brian Goetz][1] -2. [Dynamic compilation and performance measurement, Brian Goetz][2] -3. [Scala library benchmarks][3] - - [1]: http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html "flawed-benchmark" - [2]: http://www.ibm.com/developerworks/library/j-jtp12214/ "dynamic-compilation" - [3]: https://github.com/scala/scala/tree/2.12.x/test/benchmarks diff --git a/_zh-cn/overviews/thanks.md b/_zh-cn/overviews/thanks.md new file mode 100644 index 0000000000..a214af96dc --- /dev/null +++ b/_zh-cn/overviews/thanks.md @@ -0,0 +1,47 @@ +--- +layout: guides-thanks +language: zh-cn +title: 致谢名单 +--- + +2013年10月份起,CSDN CODE开始组织志愿者翻译Scala官方文档。计划翻译的文档主要为Scala官网上overview部分的内容,包含以下部分: + +- The Scala Actors Migration Guide +- Value Classes and Universal Traits +- String Interpolation +- Implicit Classes +- Futures and Promises +- 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/_zh-cn/tutorials/scala-for-java-programmers.md b/_zh-cn/tutorials/scala-for-java-programmers.md new file mode 100644 index 0000000000..fd792844b3 --- /dev/null +++ b/_zh-cn/tutorials/scala-for-java-programmers.md @@ -0,0 +1,387 @@ +--- +layout: overview +title: 给 Java 工程师的 Scala 入门教学 +overview: scala-for-java-programmers + +discourse: false +multilingual-overview: true +language: zh-cn +--- + +Michel Schinz 与 Philipp Haller 著 +Lightsing 译 + +## 介绍 + +此教学将对 Scala 语言以及编译器做一个简易介绍。面向的读者为具有编程经验,并且想简单了解 Scala 的人。本文假设读者有着基本的、最好是 Java 上的面向对象知识。 + +## 第一个例子 + +这里用标准的 *Hello world* 程序作为第一个例子。虽然它很无趣,但让我们可以用少量语言特质来演示 Scala 工具。程序如下: + + object HelloWorld { + def main(args: Array[String]) { + println("Hello, world!") + } + } + +Java 程序员应该对这个程序结构感到熟悉:这有一个`main` 函数,该函数接受一个字符串数组作为参数,即命令行参数;函数内容为调用已定义好的函数`println ` 并用Hello world 字符串当参数。 `main` 函数没有返回值 (它是一个过程方法)。因此并不需要声明返回值类型。 + +Java 程序员不太熟悉的是包着 `main` 函数的 `object` 声明。这种声明引入我们一般称之 *Singleton* 的东西,也就是只有一个实例的类。所以上面的代码同时声明了一个 `HelloWorld` 类和一个该类的实例,也叫做 `HelloWorld`。该实例会在第一次被使用到的时候即时产生。 + +眼尖的读者可能已经注意到这边 `main` 函数的声明没有带着 `static`。这是因为 Scala 没有静态成员 (函数或属性)。 Scala 程序员将这成员声明在单实例对象中,而不是定义静态成员。 + +### 编译这个例子 + +我们用 Scala 编译器 `scalac`来编译这个例子。 `scalac` 就像大多数编译器一样,它接受源代码文件当对象,并接受额外的选项,然后产生一个或多个对象文件。它产出的对象文件为标准 Java class 文件。 + +如果我们将上面的程序存为文件 `HelloWorld.scala`,编译指令为( `>` 是提示字符,不用打): + + > scalac HelloWorld.scala + +这会在当前目录产生一些 class 文件。其中一个会叫做 `HelloWorld.class`,里面包含着可被 `scala` 直接执行的类。 + +### 执行示例 + +一旦编译过后,Scala 程序可以用 `scala` 指令执行。其使用方式非常像执行 Java 程序的 `java` 指令,并且接受同样选项。上面的示例可以用以下指令来执行并得到我们预期的输出: + + > scala -classpath . HelloWorld + + Hello, world! + +## 与 Java 互动 + +Scala 的优点之一是它非常容易跟 Java 代码沟通。Scala 会默认 import `java.lang` 底下之类,其他类则需要明确导入。 + +让我们看个展示这点的示例。取得当下日期并根据某个特定国家调整成该国格式,如法国。 + +Java 的标准函数库定义了一些有用的工具类,如 `Date` 跟 `DateFormat`。因为 Scala 可以无缝的跟 Java 互动,这边不需要以 Scala 实作同样类-我们只需要导入对应的 Java 包: + + import java.util.{Date, Locale} + import java.text.DateFormat + import java.text.DateFormat._ + + object FrenchDate { + def main(args: Array[String]) { + val now = new Date + val df = getDateInstance(LONG, Locale.FRANCE) + println(df format now) + } + } + +Scala 的导入表达式跟 Java 非常像,但更为强大。如第一行,同一个 package 下的多个类可以用大括号括起来一起导入。另外一个差别是,当要导入套件或类下所有名称时,用下标 (`_`) 而不是星号 (`*`)。这是因为星号在 Scala 是一个合法的标识符 (如函数名称)。 + +所以第三行的表达式导入所有 `DateFormat` 类的成员。这让静态方法 `getDateInstance` 跟静态属性 `LONG` 可直接被使用。 + +在 `main` 函数中我们先创造一个 Java 的 `Date` 类实例,该实例默认拥有现在的日期。接下来用 `getDateInstance` 函数定义日期格式。最后根据地区化的 `DateFormat` 实例对现在日期设定格式并印出。最后一行展现了一个 Scala 有趣特点。只需要一个对象的函数可以用中缀语法调用。就是说,这个表达式 + + df format now + +是这个表达式的简略版本 + + df.format(now) + +它看起来也许只是语法上的小细节,但却有着重要的影响,其中一个影响将会在下一节做介绍。 + +最后值得一提的是,Scala 可以直接继承 Java 类或者实现 Java 接口。 + +## 一切都是对象 + +Scala 是一个纯粹的面向对象语言,这句话的意思是说,*所有东西*都是对象,包括数字、函数。因为 Java 将基本类型 (如 `boolean` 与 `int` ) 跟参照类型分开,而且没有办法像操作变量一样操作函数,从这角度来看 Scala 跟 Java 是不同的。 + +### 数字是对象 + +因为数字是对象,他们也有函数。事实上,一个像底下的算数表达式: + + 1 + 2 * 3 / x + +只有使用函数调用,因为像前一节一样,该式等价于 + + (1).+(((2).*(3))./(x)) + +这也表示着 `+`、`*` 之类的在 Scala 里是合法的标识符。 + +因为Scala的词法分析器对于符号采用最长匹配,在第二版的表达式当中,那些括号是必要的。也就是说分析器会把这个表达式: + + 1.+(2) + +拆成 `1.`、`+`、`2` 这三个标记。会这样拆分是因为 `1.` 既是合法匹配的同时又比 `1` 长。 `1.` 会被解释成字面常数 `1.0`,使得它被视为 `Double` 而不是 `Int`。把表达式写成: + + (1).+(2) + +可以避免 `1` 被解释成 `Double`。 + +### 函数是对象 + +可能令 Java 程序员更为惊讶的会是,Scala 中函数也是对象。因此,将函数当做对象传递、把它们存入变量、从其他函数返回函数都是可能的。能够像操作变量一样的操作函数这点是*函数式编程*这一非常有趣的程序设计思想的基石之一。 + +为何把函数当做变量一样的操作会很有用呢,让我们考虑一个定时函数,功能是每秒执行一些动作。我们要怎么将这动作传给它?最直接的便是将这动作视为函数传入。应该有不少程序员对这种简单传递函数的行为很熟悉:通常在用户界面相关的程序上,用来注册一些当事件发生时被调用的回调函数。 + +在接下来的程序中,定时函数叫做 `oncePerSecond` ,它接受一个回调函数做参数。该函数的类型被写作 `() => Unit` ,这个类型便是所有无对象且无返回值函数的类型( `Unit` 这个类型就像是 C/C++ 的 `void` )。此程序的主函数只是调用定时函数并带入回呼函数,回呼函数输出一句话到终端上。也就是说这个程序会不断的每秒输出一次 "time flies like an arrow"。 + + object Timer { + def oncePerSecond(callback: () => Unit) { + while (true) { callback(); Thread sleep 1000 } + } + def timeFlies() { + println("time flies like an arrow...") + } + def main(args: Array[String]) { + oncePerSecond(timeFlies) + } + } + +值得注意的是,在打印字符串时,我们使用的是 Scala 预定义的方法 `println`,而不是 `System.out` 中的。 + +#### 匿名函数 + +这程序还有改进空间。第一点,函数 `timeFlies` 只是为了能够被传递进 `oncePerSecond` 而定义的。赋予一个只被使用一次的函数名字似乎是没有必要的,最好能够在传入 `oncePerSecond` 时构造出这个函数。Scala 可以借由*匿名函数*来达到这点。利用匿名函数的改进版本程序如下: + + object TimerAnonymous { + def oncePerSecond(callback: () => Unit) { + while (true) { callback(); Thread sleep 1000 } + } + def main(args: Array[String]) { + oncePerSecond(() => + println("time flies like an arrow...")) + } + } + +这例子中的右箭头 `=>` 告诉我们有一个匿名函数,右箭头将函数对象跟函数内容分开。这个例子中,在箭头左边那组空的括号告诉我们对象列是空的。函数内容则是跟先前的 `timeFlies` 里一样。 + +## 类 + +之前已讲过,Scala 是一个面向对象语言,因此它有着类的概念 (更精确的说,的确有一些面向对象语言没有类的概念,但是 Scala 不是其中之一)。Scala 声明类的语法跟 Java 很接近。一个重要的差别是,Scala 的类可以有参数。如下面展示的复数的定义: + + class Complex(real: Double, imaginary: Double) { + def re() = real + def im() = imaginary + } + +这个复数类接受两个参数,分别为实跟虚部。在创造 `Complex` 的实例时,必须传入这些参数: `new Complex(1.5, 2.3)`。这个类有两个函数分别叫做 `re` 跟 `im` 让我们取得这两个部分。 + +值得注意的是,这两个函数的回传值并没有被明确给定。编译器将会自动的推断,它会查看这些函数的右侧并推导出这两个函数都会回传类型为 `Double` 的值。 + +编译器并不一定每次都能够推断出类型,而且很不幸的是我们并没有简单规则以分辨哪种情况能推断,哪种情况不能。实践上这通常不是问题,因为当编译器无法推断未明确给定的类型时,它会报错。Scala 初学者在遇到那些看起来很简单就能推导出类型的情况时,应该尝试着忽略类型声明并看看编译器是不是也觉得可以推断。多尝试几次之后程序员应该能够体会到何时忽略类型、何时该明确指定。 + +### 无对象函数 + +函数 `re`、`im` 有个小问题,为了调用函数,我们必须在函数名称后面加上一对空括号,如这个例子: + + object ComplexNumbers { + def main(args: Array[String]) { + val c = new Complex(1.2, 3.4) + println("imaginary part: " + c.im()) + } + } + +最好能够在不需要加括号的情况下取得实虚部,这样便像是在取得属性。Scala完全可以做到这件事,需要的只是在定义函数的时候*不要定义参数*。这种函数跟零参数函数是不一样的,不论是定义或是调用,它们都没有括号跟在名字后面。我们的 `Complex` 可以改写成: + + class Complex(real: Double, imaginary: Double) { + def re = real + def im = imaginary + } + +### 继承与重写 + +Scala 中所有的类都继承自一个父类。像前一节的 `Complex` 这种没有指定的例子,Scala 会默认使用 `scala.AnyRef`。 + +Scala 中可以重写继承自父类的函数。但是为了避免意外重写,必须加上 `override` 修饰字来明确表示要重写函数。我们以重写 `Complex` 类中来自 `Object` 的 `toString` 作为示例。 + + class Complex(real: Double, imaginary: Double) { + def re = real + def im = imaginary + override def toString() = + "" + re + (if (im < 0) "" else "+") + im + "i" + } + + +## Case Class 跟模式匹配(pattern matching) + +树是常见的数据结构。如:解译器跟编译器内部常见的表示程序方式便是树;XML文件是树;还有一些容器基于树,如红黑树。 + +接下来我们会通过一个小型计算程序来看看 Scala 是如何表示并操作树。这个程序将足以操作仅含有整数常数、整数变量跟加法的简单算术式。`1+2` 跟 `(x+x)+(7+y)` 为两个例子。 + +我们得先决定这种表达式的表示法。最自然表示法便是树,其中节点是操作、叶节点是值。 + +Java 中我们会将这个树用一个抽象父类表示,然后每种节点跟叶节点分别有各自的实际类。在函数编程里会用代数数据类型。Scala 则是提供了介于两者之间的 *case class*。这是用其定义这样数据类型的示例: + + abstract class Tree + case class Sum(l: Tree, r: Tree) extends Tree + case class Var(n: String) extends Tree + case class Const(v: Int) extends Tree + +`Sum`、`Var`、`Const` 类定义成 case class 代表着它们跟一般类有所差别: + +- 在创建类实例时不需要用 `new` (也就是说我们可以写 `Const(5)`,而不是 `new Const(5)`)。 +- 对应所有构造参数,Scala 会自动定义对应的取值函数 (即,对于 `Const` 类的实例,我们可以直接用 `c.v` 来取得建构式中的 `v` 参数)。 +- `equals` 跟 `hashCode` 会有预设定义。该定义会根据实例的*结构*而不是个别实例的识别来运作。 +- `toString` 会有预设定义。会印出"原始型态" (即,`x+1` 的树会被印成`Sum(Var(x),Const(1))`)。 +- 这些类的实例可以借由*模式匹配*来拆解。 + +现在我们有了算术表达式的数据类型,可以开始定义各种运算。我们将从一个可以在*环境*内对运算式求值的函数起头。环境的用处是赋值给变量。举例来说,运算式 `x+1` 在一个将 `x` 赋与 `5` 的环境 (写作 `{ x -> 5 }` ) 下求值会得到 `6`。 + +因此我们需要一个表示环境的方法。当然我们可以用一些像是哈希表的关连性数据结构,但是我们也可以直接用函数!环境就只是一个将值对应到 (变量) 名称的函数。之前提到的环境 `{ x -> 5 }` 在 Scala 中可以简单的写作: + + { case "x" => 5 } + +这个标记定义了一个当输入是字符串 `"x"` 时回传整数 `5`,其他输入则是用例外表示失败的函数。 + +开始之前,让我们先给环境类型一个名字。当然,我们可以直接用 `String => Int`,但是给这类型名字可以让我们简化程序,而且在未来要改动时较为简便。在 Scala 中我们这样做: + + type Environment = String => Int + +于是类型 `Environment` 便可以当做输入 `String` 回传 `Int` 函数的类型之代名。 + +现在我们可以给出求值函数的定义。概念上非常简单:两个表达式和的值是两个表达式值的和;变量的值直接从环境取值;常数的值就是常数本身。表示这些在 Scala 里并不困难: + + def eval(t: Tree, env: Environment): Int = t match { + case Sum(l, r) => eval(l, env) + eval(r, env) + case Var(n) => env(n) + case Const(v) => v + } + +这个求值函数借由对树 `t` 做*模式匹配*来求值。上述实作的意思应该从直观上便很明确: + +1. 首先检查树 `t` 是否为 `Sum`,如果是的话将左跟右侧子树绑定到新变量 `l`跟 `r`,然后再对箭头后方的表达式求值;这一个表达式可以使用(而且这边也用到)根据箭头左侧模式所绑定的变量,也就是 `l` 跟 `r`, +2. 如果第一个检查失败,也就是说树不是 `Sum`,接下来检查 `t` 是否为 `Var`,如果是的话将 `Var` 所带的名称绑定到变量 `n` 并求值右侧表达式, +3. 如果第二个检查也失败,表示树不是 `Sum` 也不是 `Var`,那便检查是不是 `Const`,如果是的话将 `Const` 所带的名称绑定到变量 `v` 并求值右侧表达式, +4. 最后,如果全部检查都失败,会抛出异常表示匹配失败;这只会在有更多 `Tree` 的子类时发生。 + +如上,模式匹配基本上就是尝试将一个值对一系列模式做匹配,并在一个模式成功匹配时抽取并命名该值的各部分,最后对一些代码求值,而这些代码通常会利用被命名到的部分。 + +一个经验丰富的面向对象程序员也许会疑惑为何我们不将 `eval` 定义成 `Tree` 类跟子类的*方法*。由于 Scala 允许在 case class 中跟一般类一样定义函数,事实上我们可以这样做。要用模式匹配或是函数只是品味的问题,但是这会对扩充性有重要影响。 + +- 当使用函数时,增加新的节点类型是相当容易的,只要定义新的 `Tree` 子类即可。不过另一方面,为树增加新操作很麻烦,因为它需要修改 `Tree` 的所有子类。 +- 当使用模式匹配时情况则反过来:增加新节点需要修改所有对树做模式匹配的函数将新节点纳入考虑;增加新操作则很简单,定义新函数就好。 + +让我们定义新操作以更进一步的探讨模式匹配:对符号求导数。读者们可能还记得这个操作的规则: + +1. 和的导数是导数的和 +2. 如果是对变量 `v` 取导数,变量 `v` 的导数是1,不然就是0 +3. 常数的导数是0 + +这些规则几乎可以从字面上直接翻成 Scala 代码: + + def derive(t: Tree, v: String): Tree = t match { + case Sum(l, r) => Sum(derive(l, v), derive(r, v)) + case Var(n) if (v == n) => Const(1) + case _ => Const(0) + } + +这个函数引入两个关于模式匹配的新观念。首先,变量的 `case` 运算式有一个*看守*,也就是 `if` 关键字之后的表达式。除非表达式求值为真,不然这个看守会让匹配直接失败。在这边是用来确定我们只在取导数变量跟被取导数变量名称相同时才回传常数 `1`。第二个新特征是可以匹配任何值的*万用字符* `_`。 + +我们还没有探讨完模式匹配的全部功能,不过为了让这份文件保持简短,先就此打住。我们还是希望能看到这两个函数在真正的示例如何作用。因此让我们写一个简单的 `main` 函数,对表达式 `(x+x)+(7+y)` 做一些操作:先在环境 `{ x -> 5, y -> 7 }` 下计算结果,然后在对 `x` 接着对 `y` 取导数。 + + def main(args: Array[String]) { + val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y"))) + val env: Environment = { case "x" => 5 case "y" => 7 } + println("Expression: " + exp) + println("Evaluation with x=5, y=7: " + eval(exp, env)) + println("Derivative relative to x:\n " + derive(exp, "x")) + println("Derivative relative to y:\n " + derive(exp, "y")) + } + +执行这程序,得到预期的输出: + + Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y))) + Evaluation with x=5, y=7: 24 + Derivative relative to x: + Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0))) + Derivative relative to y: + Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1))) + +研究这输出我们可以发现,取导数的结果应该在输出前更进一步化简。用模式匹配实现一个基本化简函数是一个很有趣 (但是意外的棘手) 的问题,在这边留给读者当练习。 + +## 特质 (Traits) + +除了由父类继承行为以外,Scala 类还可以从一或多个*特质*导入行为。 + +对一个 Java 程序员最简单去理解特质的方式应该是视它们为带有实例的接口。在 Scala 里,当一个类继承特质时,它实现了该特质的接口并继承所有特质带有的功能。 + +为了理解特质的用处,让我们看一个经典示例:有序对象。大部分情况下,一个类所产生出来的对象之间可以互相比较大小是很有用的,如排序它们。在Java里可比较大小的对象实作 `Comparable` 介面。在Scala中借由定义等价于 `Comparable` 的特质 `Ord`,我们可以做的比Java稍微好一点。 + +当在比较对象的大小时,有六个有用且不同的谓词 (predicate):小于、小于等于、等于、不等于、大于等于、大于。但是把六个全部都实现很烦,尤其是当其中有四个可以用剩下两个表示的时候。也就是说,(举例来说) 只要有等于跟小于谓词,我们就可以表示其他四个。在 Scala 中这些观察可以很漂亮的用下面的特质声明呈现: + + trait Ord { + def < (that: Any): Boolean + def <=(that: Any): Boolean = (this < that) || (this == that) + def > (that: Any): Boolean = !(this <= that) + def >=(that: Any): Boolean = !(this < that) + } + +这份定义同时创造了一个叫做 `Ord` 的新类型,跟 Java 的 `Comparable` 接口有着同样定位,且给了一份以第一个抽象谓词表示剩下三个谓词的预设实作。因为所有对象预设都有一份等于跟不等于的谓词,这边便没有定义。 + +上面使用了一个 `Any` 类型,在 Scala 中这个类型是所有其他类型的父类型。因为它同时也是基本类型如 `Int`、`Float` 的父类型,可以将其视为更为一般化的 Java `Object` 类型。 + +因此只要定义测试相等性跟小于的谓词,并且加入 `Ord`,就可以让一个类的对象们互相比较大小。让我们实作一个表示阳历日期的 `Date` 类来做为例子。这种日期是由日、月、年组成,我们将用整数来表示这三个资料。因此我们可以定义 `Date` 类为: + + class Date(y: Int, m: Int, d: Int) extends Ord { + def year = y + def month = m + def day = d + override def toString(): String = year + "-" + month + "-" + day + +这边要注意的是声明在类名称跟参数之后的 `extends Ord`。这个语法声明了 `Date` 继承 `Ord` 特质。 + +然后我们重新定义继承自 `Object` 的 `equals` 函数好让这个类可以正确的根据每个属性来比较日期。因为在 Java 中 `equals` 直接比较实际对象本身,并不能在这边用。于是我们有下面的例子: + + override def equals(that: Any): Boolean = + that.isInstanceOf[Date] && { + val o = that.asInstanceOf[Date] + o.day == day && o.month == month && o.year == year + } + +这个函数使用了预定义函数 `isInstanceOf` 跟 `asInstanceOf`。`isInstanceOf` 对应到 Java 的 `instanceof` 运算子,只在当使用它的对象之类型跟给定类型一样时传回真。 `asInstanceOf` 对应到 Java 的转型运算子,如果对象是给定类型的实例,该对象就会被视为给定类型,不然就会丢出 `ClassCastException` 。 + +最后我们需要定义测试小于的谓词如下。 + + def <(that: Any): Boolean = { + if (!that.isInstanceOf[Date]) + error("cannot compare " + that + " and a Date") + + val o = that.asInstanceOf[Date] + (year < o.year) || + (year == o.year && (month < o.month || + (month == o.month && day < o.day))) + } + +这边使用了另外一个预定义函数 `error`,它会丢出带着给定错误信息的例外。这便完成了 `Date` 类。这个类的实例可被视为日期或是可比较对象。而且它们通通都定义了之前所提到的六个比较谓词: `equals` 跟 `<` 直接出现在类定义当中,其他则是继承自 `Ord` 特质。 + +特质在其他场合也有用,不过详细探讨它们的用途并不在本文件目标内。 + +## 泛型 + +在这份教学里,我们最后要探讨的 Scala 特性是泛型。Java 程序员应该相当清楚在 Java 1.5 之前缺乏泛型所导致的问题。 + +泛型指的是能够将类型也作为程序参数。举例来说,当程序员在为链表写函数库时,它必须决定列表的元素类型为何。由于这列表是要在许多不同场合使用,不可能决定列表的元素类型为如 `Int` 一类。这样限制太多。 + +Java 程序员采用所有对象的父类 `Object`。这个解决办法并不理想,一方面这并不能用在基础类型 (`int`、`long`、`float` 之类),再来这表示必须靠程序员手动加入大量的动态转型。 + +Scala 借由可定义泛型类 (跟函数) 来解决这问题。让我们借由最简单的类容器来观察这点:引用,它可以是空的或者指向某类型的对象。 + + class Reference[T] { + private var contents: T = _ + def set(value: T) { contents = value } + def get: T = contents + } + +类 `Reference` 带有一个类型参数 `T`,这个参数会是容器内元素的类型。此类型被用做 `contents` 变量的类型、 `set` 函数的对象类型、 `get` 函数的回传类型。 + +上面代码使用的 Scala 变量语法应该不需要过多的解释。值得注意的是赋与该变量的初始值是 `_`,该语法表示预设值。数值类型预设值是0,`Boolean` 类型是 `false`, `Unit` 类型是 `()` ,所有的对象类型是 `null`。 + +为了使用 `Reference` 类型,我们必须指定 `T`,也就是这容器所包容的元素类型。举例来说,创造并使用该容器来容纳整数,我们可以这样写: + + object IntegerReference { + def main(args: Array[String]) { + val cell = new Reference[Int] + cell.set(13) + println("Reference contains the half of " + (cell.get * 2)) + } + } + +如例子中所展现,并不需要先将 `get` 函数所回传的值转型便能当做整数使用。同时因为被声明为储存整数,也不可能存除了整数以外的东西到这一个容器中。 + +## 结语 + +本文件对Scala语言做了快速的概览并呈现一些基本的例子。对 Scala 有更多兴趣的读者可以阅读有更多进阶示例的 *Scala By Example*,并在需要的时候参阅 *Scala Language Specification* 。