读《快学Scala 》一书的摘要
Scala 运行于JVM之上,拥有海量类库和工具,兼顾函数式编程和面向对象。
在Scala中, 解释器就是我们喜欢的REPL,变量或者函数的类型总是写在变量或函数的后面(与java相反),数值类型的转换通过方法而不是强制类型转换,仅当同一行代码存在多条语句时才需要用分号隔开。
scala 允许自定义操作符,注意有分寸地使用,在使用scala.开头的包时,可以省去scala前缀。scala没有静态方法,类似的特性可以用单例对象,一个类对应的companion object就跟Java中的静态方法一样,使用companion object的apply方法是scala中构建对象的常用方法。
控制
在scala中,判断语句跟其他语言类似,但{}块包含一系列表达式,其结果也是一个表达式,块中最后一个表达式的值就是块的值。
在for 循环的变量之前并没有val或var的指定。该变量的类型是集合的元素类型,循环变量的作用域一直持续到循环结束。scala并没有提供break或continue语句来退出循环,替代方法如下:
- 使用Boolean的控制变量
- 使用嵌套函数——在函数中return
- 使用Breaks对象中的break方法,控制权的转移是通过抛出和捕获异常完成的,出于性能考虑,尽量避免这一机制。
for有丰富的形态,可以提供多个生成器,并带有if开头的表达式,还可以使用任意多的循环变量定义。
函数
scala中的方法对对象进行操作,而函数不是,C++中也有函数,不过在Java中智能用静态方法模拟,scala的函数不需要return,对应递归函数必须指明返回类型,可以混用未命名参数和带名参数,只要那些未命名的参数是排在前面的即可。
同样,变长参数列表很方便,通过 :_* 将值序列转换成参数序列。当变长参数类型为Object的Java方法时,需要手工对基本类型进行转换。
scala对不返回值的函数有特殊的表示法,如果函数体包含在花括号当中但没有前面的=号,那么返回类型就是Unit,这样的函数也叫过程函数。
当val 被声明为lazy时,它的初始化将被延迟加载,直到对它初次取值。每次访问延迟加载的变量,都会有一个方法被调用,以线程安全的方式检查该值是否已被初始化。
异常
scala异常的工作机制和Java或C++一样,但没有"受检"异常,捕获异常采用模式匹配的语法,不需要使用捕获的异常对象,可以用_来替代变量名。try/catch和try/finally是互补的。
数组和映射
Scala中的Array是定长数组,ArrayBuffer是变长数组,对应于Java中的ArrayList,C++中的Vector,可以用相同的代码处理这两种数据结构,用 for (i<-区间 )来遍历,
用for(...) yield
创建一个类型与原始集合相同的新集合,还可以通过if 在进行条件过滤。Scala中的内建函数sum,sorted,max,min,quicksork提供了常用算法。由于Scala数组是用java数组实现的,可以在java和scala之间传递,只需引入scala.collection.JavaConversions里的隐式转换方法。
scala中,映射是对偶的集合,可以看做将键映射到值的函数,区别在于函数一般用于计算,而映射只做查询。用=可以直接增加映射,也可用+=添加多个关系,用for((k,v)<-映射) 来遍历映射,使用scala.collection.JavaConversions.mapAsScalaMap将Java中的map转换为scala中的映射。
scala中,元组是不同类型的值的聚集,()构成元组,用方法1,2...访问其组元,而通常使用模式匹配来获取元组的组元。使用元组的原因之一是把多个值绑在一起,以便它们能够被一起处理,通常用zip方法开完成,使用toMap方法将对偶的集合转换成映射。
类与对象
在scala中,类并不声明为public,源文件可以包含多个类,所有这些类都具有共有可见性。对每个字段都提供了getter和setter方法,分别叫做 字段名 和 字段名_,可重新自定义。
注意:
1)如果字段私有,则getter和setter也是私有的
2)如果字段val,则只有getter方法
3)如果不需任何getter和setter,可将字段声明为private[this]
将scala字段标注为@BeanProperty时,会产生Java属性的定义方法getxxx和setxxx。
scala中的类有一个主构造器primary constructor,可有任意多个辅助构造器 auxiliary constructor。辅助构造器与其他语言的区别在于: 1)名称为this 2)必须以一个先前已定义的其他辅助构造器或主构造器的调用开始。而主构造器的参数直接放置在类名之后,会执行类定义中的所有语句,无参主构造器仅仅执行类体中的所有语句而已。如果将主构造器定义为私有的,则必须通过辅助构造器来构造对象了。在内嵌类中,可以通过外部类.this来访问外部类的this 引用。
对象本质上拥有类的所有特质,只不能提供构造器参数。所有使用单例对象的地方,scala中都可以用对象来实现。
1)作为存放工具函数或常量的地方
2)高效共享单个不可变实例
3)需要用单个实例来协调某个服务时
对Java中既有实例方法又有静态方法的类,scala通过类及与类同名的伴生对象来实现。类和它的伴生对象可以相互访问私有特性,必须存在于同一源文件中。
每个scala程序都必须从一个对象的main方法开始,也可以扩展App特质。scala中没有枚举类型,但标准类库提供了一个Enumeration助手类,用于产生枚举。
包
Scala中的包与java包或c++命名空间的目的相同,但可以在同一文件中为多个包贡献内容。
尽量使用完整包名,避免使用scala,java,com,org等来命名嵌套的包。串联式包语句可以限定可见的包。
包可以包含类,对象和属性,但不能包含函数和变量的定义,在实现上,包对象被编译成带有静态方法和字段的JvM类。通过修饰符同样可以达到public,private或protected的效果。
在scala中, 任何地方都可以声明引入包,这一点和python很相似。通过选取器可以引入包中的指定成员,还可以对指定成员重命名或者隐藏java.lang,scala,predef 总是被默认引入的。
继承
scala扩展类的方式同样是使用extends关键字,重写一个非抽象方法必须使用override修饰符,用isInstanceOf方法判断某个对象是否属于某个特定的类,只有主构造器可以调用超类的构造器。
字段重写时的限制:
- def 只能重写另一个def
- 只能重写另一个val或不带参数的def
- var只能重写另一个抽象的var
构造顺序问题的根本原因——java允许在超类的构造方法中调用子类的方法。因为在子类中正确的扩展相等性判断非常困难,所以将equals方法定义成final。除非万不得已,不要使用wait,notify和synchronized。
和java的接口不同,scala特质可以给出这些特质的缺省实现。让特质拥有具体行为存在一个弊端,当特质改变时,所有混入了该特质的类必须要重新编译。scala不支持多继承,可以用with关键字来添加额外的特质。当做富接口使用的特质将具体方法和抽象方法结合在了一起,特质中的字段同样既可以是具体的,又可以是抽象的。
混入特质的对象在构造时的执行顺序:
- 首先调用超类的构造器
- 特质构造器在超类构造器之后,类构造器之前执行
- 特质由左到右构造
- 每个特质中,父特质先被构造,
- 如果多个特质有一个父特质,若已被构造则不会再次构造
- 所有特质构造完毕,子类被构造。
缺少构造器参数是特质与类之间唯一的技术差别。
文件访问
scala.io.source对象的getlines方法可以读取文件的所有行,可以把source对象当成迭代器读取文件中的每个字符,java.util.Scanner来处理同事包含文本和数字的文件。
从URL中读取时,需要事先知道编码格式,scala中没有提供读取二进制文件的方法,需要使用Java类库,同样没有内建的对写入文件的支持,可使用java.io.PrintWriter,访问目录也要用java的方法,例如java.nio.file.Files类中的walkFileTree。
scala集合类都是可序列化的。
scala.sys.process包提供了用于与shell程序交互的工具,包含了一个从字符串到processbuilder对象的隐式转换,!操作符就是执行这个processbuidler的对象。scala.util.matching.Regex 利用正则表达式对字符串进行分析。
操作符与解析器
变量、函数、类等的名称统称为标识符,反引号中可以包含几乎任何字符序列。
在scala中,除了
- 以冒号:结尾的操作符
- 赋值操作符
所有操作符都是左结合的。
unapply方法接受一个对象,然后从中取值,通常是当初用来构造该对象的值。要取任意长度的值的序列,一般用unapplySeq命名方法。
Scala解析器库是scala语言总内嵌领域特定语言(DSL)的高级示例。为了使用Scala解析库,需提供一个扩展自Parsers特质的类并定义那些由基本操作组合起来的解析操作,包括:
- 匹配一个词法单元
- 在两个操作之间做选择(|)
- 依次执行两个操作(~)
- 重复一个操作(rep)
- 可选择地执行一个操作(opt)
组合子返回的是样例类的实例而不是对偶,这样更方便模式匹配。要生成解析树,需用^^操作符,给出产生树节点的函数,避免左递归和回溯。
StandardTokenParsers类提供了一个产出这些词法单元的解析器。
集合
Scala中所有集合都是iterable的,seq是有先后次序的序列(如数组和列表),Set是没有先后次序的序列,map是一种键值对偶。Scala优先采用不可变集合,::操作符从给定的头和尾创建一个新的列表。如果要把列表中的某个节点变成列表中的最后一个节点,不能将next引用设为nil,而应该设为LinkedList.empty.
已排序的集使用红黑树实现的,scala2.9没有可变的已排序集,要用到java.util.TreeSet
Scala 关于添加和移除的操作符:
- 向后
(:+)
或向前(+:)
追加元素到序列中 - 添加
(+)
元素到无先后次序的集合中 - 用
-
移除元素 - 用
++
和--
批量添加和移除元素 - 对于列表,优先使用
::
和:::
- 改值操作有
+=
,++=
,-=
和--=
对于集合,推荐++,&和--,尽量不用++:,+=:和++=: 操作方式。
初始值和操作符是两个分开定义的柯里化参数,这样scala就能用初始值类型来推断操作符的类型定义。任何while循环都可以用折叠来替代,对于那些完整构造需要很大开销的集合而言,迭代器作用大,而流将缓存访问过的行,允许你重新访问他们。
对于数组,缓存,哈希表,平衡树而言,基于par方法的并行实现很高效。
模式匹配
与switch语句不同,scala模式匹配没有break的问题。如果case中的判断不能匹配,则捕获所有的模式来尝试匹配。变量模式可能与常量表达式冲突,变量必须以小写字母开头。如果有一个小写字母开头的常量,则需要把它抱在反引号中。
在类型匹配的时候,必须给出一个变量名,否则会拿对象本身来进行匹配。由于匹配发生在运行时,Jvm中泛型的类型信息是被擦掉的,所有不能用类型来匹配特定的Map类型。正则表达式是适合使用提取器的场景。
样例类是一种特殊的类,经过优化以被用于模式匹配,其实例使用(),样例对象不使用圆括号。中置表示法可用于任何返回对偶的unapply方法。样例类的特点:
- 模式匹配的代码更精简
- 构造时不需new
- 可以免费得到toString,equals,hashCode 和copy方法
让所有样例类都扩展某个密封的类或特质是个好做法。被包在花括号内的一组case语句是一个偏函数,偏函数表达式必须位于编译器可以推断返回类型的上下文中。
注解
注解可以在程序的各个条目中添加信息,是插入到代码中以便有工具可以对他们进行处理的标签。可以对是scala类使用java注解,也可以使用scala特有的注解。
在scala中,可为类,方法,字段,局部变量和参数添加注解。Java注解的参数类型只能是:
- 数值型变量
- 字符串
- 类变量
- java枚举
- 其他注解
- 上述类型的数组。
如果要实现一个新的Java注解,则需要用Java来编写该注解类。scala用@clonable和@remote来标记可被克隆的和远程的对象。@varargs注解可以从Java调用Scala的带有变长参数的方法。
Scala类库中的有些注解可以控制编译器的优化,@tailrec 用于消除递归,@switch 注解可以检查scala的match语句是否真的被编译成了跳转表,用@inline来建议编译器做内联,@editable给那些可以在生产代码中移除的方法打上标记,对被省略的方法的调用,编译器会替换成Unit对象,@uncheckVariance会取消与型变相关的错误提示。
xml处理
Scala提供了对xml(当然也就支持html了)的内建支持,可以用scala.xml.Elem的值表示一个XML元素。Node类是所有xml节点类型的父类,Elem类描述xml元素。要处理某个元素的属性键和值,可以用attributes属性,然后用()来访问定键的值 ,使用循环或asAttrMap方法遍历所有属性。
内嵌的字符串会被转成Atom[String]节点,所以可在xml中包含scala代码,被内嵌的scala代码还可以继续包含XML片段,被引用的字符串当中的花括号不会被解析和求值。
NodeSeq提供了类似xpath中/,//的操作符,scala中用,\ 替代,可以在模式匹配中使用xml的关键字。由于scala中xml节点和节点序列是不可变的,若要修改一个节点,需创建拷贝,给出修改,在拷贝未修改的部分。RuleTransformer类的transform方法遍历给定节点的所有后代,应用所有规则,最后返回经过变换的树。
Scala中的ContructingParser是个解析器,用于加载xml,可以保留注释,CDATA和空白,用doc.dtd可以访问到DTD。
保存XML时,没有内容的元素不会被写成自结束的标签。Scala中每个元素都有一个scope属性,类型为NamespaceBinding,该类的Uri属性输出命名空间的URI。
高级函数和高级类型
在scala中,函数是头等公民,可以用变量存储函数,可以使用匿名函数,和带参数的函数。如果需要一个序列的值,一般从一个简单序列转化得出。函数可以在变量不再作用域内时被调用,这样的函数叫闭包。
柯里化是指将原来接受两个参数变成接受一个参数的函数的过程。不需要用return语句来返回函数值,函数的返回值就是函数体的值。
scala中,用方括号来定义类型参数,从调用该方法的实际参数来推断出类型。视图界定 T<%V要求必须存在一个从T到V的隐式转换,Manifest对象是构造器的隐式参数,可用于上下文界定,类型变化的方向和子类型方向是相反的。
函数在参数上是逆变的,在返回值上的协变的,对象是不能泛型化的。
在内部,编译器将所有嵌套的类型表达式a.b.c.T都翻译成类型投影a.b.c.type#T。对应复杂类型,可用type关键字创建一个简单的别名,type同样被用于那些在子类中被具体化的抽象类型。
结构类型指的是一组关于抽象方法,字段和类型的规格说明,可用安全而方便的反射调用。
在scala中,通过特质和自身类型达到一个简单的依赖注入效果。如果类型是在类实例化时给出,则使用泛型,如果类型是在子类中给出,则使用抽象类型。
List这样的泛型类型有时称为类型构造器。Container特质是scala集合类库中使用的构建器机制的的简化版。
Actor
actor提供了并发程序中与传统的基于锁的结构不同的另一种选择,通过尽可能避免锁和共享状态,actor更容易地设计出正确、没有死锁或争用状况的程序。Scala提供了actor的简单实现,akka(http://akka.io)提供了高级actor类库。
每个actor都要扩展Actor类并重写Act方法,actor是处理异步消息的对象,消息可以是任何对象,通过!操作符发送消息,例如:
actorX !“happy new year”
一个好的方式是使用样例类作为消息,这样,actor可以使用模式匹配了。发送的消息存放在mailbox,receive方法从mailbox中取下一条消息并处理,如果在receive方法被调用时并没消息,则该调用会阻塞,直到有消息抵达。actor可以安全地修改它自己的数据。
向其他actor发送消息的方法:
- 使用全局的actor
- actor可以构造成带有指向一个或更多actor的引用
- actor可接收带有指向另一个actor的引用的消息
- actor可以返回消息给发送方
actor可以发送一个消息并等待回复,用!?操作符即可,尽量避免同步消息。
actor的act方法在actor的start方法被调用时开始执行。接下来进入某个循环,终止条件如下:
- act方法返回
- act方法由于异常被终止
- actor调用exit方法
通过link方法可以将不同的actor链接在一起。
actor的设计原则如下:
- 避免使用共享状态
- 不要调用actor的方法
- 保持每个actor简单
- 上下文数据包含在消息中
- 最小化给发送方回复
- 最少阻塞调用
- 使用react
- 建立失败区