设计模式在软件编程的各个方面都有应用。MVC与分布式一般算成是架构模式,而写文档注释也有一定的模式可以遵守。另外,我们也可以设计出一些使用的模式,以便在开发软件之后,可以让用户按照指定的模式使用软件。
Scala的for语句
在scala当中,for并不是经典的控制流语句而已。在for里面可以加一些过滤器,同时,也可使用yield来生成一个集合。这种控制语句应该是与Map-Reduce相互配合的。而且为了支持新的范式,也需要修改for语句的行为。
对于非递归的函数,scala不需要指定返回的类型。这是因为,虽然hindley-Milner算法能够推断出递归函数的返回的类型,但是在面向对象的语言中却并不是总是可以行得通的。所以,在处理递归函数的返回类型上,Scala并没有使用Hindley-Milner类型推断算法。
另外,Scala的函数参数也可以使用默认参数,或者使用变长的参数(变长的参数的处理与Java是类似的)。
不返回值的函数,一般都是使用Unit作为返回类型。
Scala中捕获异常可以采用模式匹配的方法。这比Java精简了不少。
另外,在数据流编程当中,一种方法是使用if守卫加上yield和for产生序列,比如for (elem <- a if elem % 2 == 0) yield 2 * elem。但是对于函数式语言实践而言,最经常使用的是filter-map-reduce的方法,比如a.filter( _ % 2 == 0).map(2*_)或者甚至是a.filter { _ % 2 == 0 } map { 2 * _ }这样的序列化的形式。另外,很大一部分时间编程语言都是在做查找和排序的操作。这种情况下,Scala对数组采用了很多的方法。比如整型数组的Array(1,2,3,4,5).sum方法,以及ArrayBuffer(1,7,2,9).sorted(_<_)方法。
在类型系统比较丰富的语言中,不熟悉类型系统的编程者可能觉得语法很奇怪。不过,其实也没有什么好奇怪的。学完类型系统就会觉得是自然而然的。
Scala中的映射
Scala中,其实又被称为字典,在Scala中称为哈希表。哈希表是应用非常广泛的数据结构。Scala用Map来记其名子。比如
val scores = Map("Alice" -> 10, "Bob" -> 3, "Cindy" -> 8)在Scala中,哈希表具有Map(T1, T2)这样的数据类型,而且这里的哈希表是不可变元素。要使用可变的映射,数据结构应该变成scala.collection.mutable.Map。或者要从头开始建立哈希表的时候,使用scala.collection.mutable.HashMap[String, Int]这样的形式。
在Scala中,函数与映射在语法上类似。比如scores("Bob")可以取到相应键的值。如果相应的键不存在,则抛出异常就可以了。另外,可以使用score.contains之类的方法检查映射中是否包含指定的键。另外,使用getOrElse()方法,可以执行,如果包含相应键就返回值,否则就返回一个指定的值的方法。
如果映射是可变的,可以使用score("Bob") = 10或者scores("Fred") = 7这样的语法来更新已有的键的值,或者创建一个新的键值对。或者使用+=方法来把新的关系加上去,如
scores += ("Bob" -> 10, "Fred" -> 7)
socres -= "Alice"对于不可变映射而言,不能重新赋值,但是可以通过+与-方法生成与原来映射相关的一个新的映射。假如不可变映射与一个var相关联,还可以重新给var赋值。虽然表面上看起来,创建新的映射效率比较低,但是实际上,函数式语言中正好相反。在函数式语言中,递归与不可变元素的使用反而是效率比较高的。取遍映射中的所有的键值对可以使用for方法,比如
for ((k,v) <- 映射) 处理k与v之所以能够在for语句中实现这一点,主要是因为for语句里面能够模式匹配。另外,也可以使用score.keys与score.values之类的返回相应的键与值。总而言之,这是映射这种数据结构的通用的方法。
另外,要想混用Java与Scala中的映射,必须要经过一个类型转换。比如
import scala.collection.JavaConversions.mapAsScalaMap
val scores : scala.collection.mutable.Map[String, Int] =
new java.util.TreeMap[String, Int]
import scala.collection.JavaConversions.propertiesAsScalaMap
val props : scala.collection.Map[String, String] = System.getProperties()反过来的类型转换,提供的是相反的隐式类型转换的值。这时候导入的是
import scala.collection.JavaConversions.mapAsJavaMap另外一种常见的数据结构是元组。Scala的元组是一个多类型的参数类。另外,可以使用模式匹配来将元组中相应位置的值赋给相应的变量。元组的一个作用是通过具体的元组的部长乘积,构造出对偶的数组。比如
val symbols = Array("<", "-", ">")
val counts = Array(2, 10 ,2)
val pairs = symbols.zip(counts)
for ((s, n) <- pairs) Console.print(s*n)在上面的式子中,pairs是如Array(("<", 2), ("-",10), (">",2))之类的有序组。对于二元序组来说,可以通过toMap方法将其转换成相应的映射。
注,在scala中,无参函数调用也不必带(),其实也许是因为本来函数与变量以及类的名子空间都是在同一个当中,所以,实际上,根据名子就知道它是函数,是变量还是方法,根本没有必要区分。而且,一个函数中实际上也可以带有相关的参数(惰性求值)。编译器如有可能,总是应该完成大部分的工作,应该具有常识,应该具有智能。
Scala中的类的使用
Scala中的类的风格大部分是与Java相通的。但是Scala作为一种更高级的语言,应该使用那些支持的高级的特性来完成类的声明与定义。
对于getter/setter(取值器与改值器),有时候需要特别的处理,比如不允许改值。这种方式下,还是要使用private关键字来修饰变量,并且自己手动写getter/setter方法。注意,可以使用this来引用类的成员。在默认情况下,Scala为其每一个字段都提供getter/setter方法,除非成员被声明为private。要想查看Scalac到底生成了哪些代码,可以使用javap来查看生成的class文件。另外,其实getter/setter方法默认是与变量同名的一个无参函数。因此可以手工地使用def age来定义取到成员变量的一个值。
如果不允许类中的名子被修改,那么更好的办法是使用val代替var。
按照JavaBeans中的要求,所有的属性都要有getFoo与setFoo这样的方法。为此,可以使用scala中的scala.reflect.BeanProperty包提供的@BeanProperty修饰器,来完成相应的操作。具体用法是:
class person {
@BeanProperty var name : String = _
}这样除了生成一般的方法之外,还生成getName与setName方法。此外,注解也可以以
class Person (@BeanProperty var name : String)这样的方式被使用。
与Java一样,Scala中的类的构造器也可以有多个。不过,在Scala中,构造器的名子为this(在Java中就是类的同名的函数)。构造器有主构造器与辅构造器之分。我们可以通过某些方法指定。默认Scala会生成一个无参的主构造器。
Scala的设计者认为每敲一个键都是珍贵的,所以可以把主构造器与类定义结合在一起。也就是说,在定义类的时候,就可以定义传入类的相关的参数。如
class Person (val name : String){}这样的紧凑形式。
另外,Scala也支持嵌套类的使用,使用的方式与java不太一样。
注:java中,#运算符代表的是一个类型投影,比如说,Network#Member,会得到Network中的嵌套类Member。
Scala中的对象
scala中的对象直接实现了单件的模式。因为既然被定义为一个对象,那么所有的该对象的变量都是共同的操作。与类相比,对象只是没有有参构造函数,以及对象的构造器在该对象第一次被使用的时候调用。为了使类具有类似于静态方法的效果,可以给类定义一个伴生的对象。这个时候,类可以调用伴生对象的方法。不过,访问的时候必须显式地指明伴生对象。而且Scala中强制要求伴生对象的定义必须与类的定义放在一起。
对象的常用的方法是继承一个抽象类,并且实例化方法。比如,绿色是一种取特殊的值的颜色,那么绿色就成为一个对象。扩展了抽象类的方法。
Scala应用程序
与Java一样,scala程序的运行是从一个对象的main方法开始。main方法的类型是Array[String] => Unit。除了直接写main方法之外,我们也可以自己扩展App特质,然后把程序代码放在构造器方法体内。
object Hello extends App {
println("hello, World!")
}还可以在App构造器内使用args.length得到参数的长度,以及得到相关的参数。应当注意,程序的体是放在App特定的构造器里面,所以println函数不用放在方法中,就好像是直接使用类的成员传递一样。
包管理是构造大型应用程序的一部分,属于封装。在Java中,包总是以绝对路径的方式被访问,但是在Scala中可以是相对路径。此外,包名可以放在不同的文件中。由于Java虚拟机的局限,包可以包含类、对象与特质,但是不能直接包含函数或者变量定义。为了解决这种困难,可以使用“包对象”的概念,也就是使用
package com.horstmann.impatient
pacakge object people {
val defaultName = "John Q. Public"
}
package people {
class Person {
var name = defaultName
}
}这样的方式来定义一个包对象。由于Person类既在com.horstmann.impatient.people包的作用域中,而该作用域下定义了defaultName成员,所以可以在Person类中访问defaultName。
包以象是Scala的一个小把戏。在内部,包对象被编译成带有静态方法和字段的JVM类,名为package.class,位于相应的包下面。
另外,引入包的时候可以引入包中的所有的名称(使用下划线),也可以引入单独的名称(比如引用包中的一个类)。在Scala中,包引入可以放在任何作用域里面,不必放在文件的顶部。这样可以只在运行的时候才引用相应的包。Java程序员的实践比较依赖于IDE。因为他们不喜欢把包中的所有的对象都引入文件,而IDE正好可以让它们选择引用哪些包,并且为相应的引入生成长长的import语句。
如果要引入包中的几个对象,可以使用选取器,如
import java.awt.{Color, Font}或者重命名要导入的成员以解决可能的冲突
import java.util.{HashMap => JavaHashMap}
import scala.collection.mutable._总而言之,在scala程序员中,把包中的所有的对象都导入进来是很普遍的。
Scala的面向对象的机制
首先,scala的类的机制与java类似,另外,scala中重写一个非抽象的方法的时候必须使用override修饰符。
与java中一样,调用超类的方法也使用super关键字。
如果需要类型检查,可以按照如下的方式:
if (p.isInstanceOf[Employee]) {
val s = p.asInstanceOf[Employee]
...
}注意scala是静态的语言,所以运行期中的type(a) == int这样的代码显然是不容许的(即使容许,求值也会发生在编译期)。因此,使用了isInstanceOf作为一个有参数的构造器。与其直接使用类型运算符,在scala中,不如使用模式匹配这种轻量级的类型运算符。比如在检查变量的类型的时候,使用
p match {
case s : Employee => ...
case _ => //otherwise
}我们或许可以把模式匹配看成轻量级的类型运算。另外,可能令人感到困惑的是,因为是静态类型,难道程序员不是知道所有的变量的类型了么?为什么还需要做检查变量的类型这样看起来无意义的工作呢?
剩下的编程实践
一种编程语言有理论的部分和实践的部分两个大类。在理论的部分,几乎只能是执行某些计算。然而一个有实际意义的语言,通常的编程主题有文件Io、进程控制等。访问操作系统接口往往也是必要的。
文件的本质,在Haskell中显示的比较紧。但是在通常的编程语言中,理解的层次往往只是文件当成是一个可以随时读取的对象。随着读取的进行,每次都往前移动。现在我们知道,我们完全可以把文件当成是一个惰性数据结构。
随着软件产业的发展,有一些主题在编程语言中也变得流行起来。除了文件IO,访问操作系统函数接口之外,还有的普遍的操作有XML、序列化,异常处理等。
Scala的序列化来自于java.io.Serializable。但是在scala中,要声明一个类是可以序列化的,使用的方法是注解。比如
@SerialVersionUID(42L) class Person extends Serializable注意,由于scala包直接包含了Serializable,所以不需要引入额外的序列化的包。另外,scala的所有的集合类都是可以序列化的。
注:自己在Python中曾看到“注解”或者装饰器其实就是一个高阶函数。并且,使用了注解后,每次调用这个类,这个类都与这个高阶函数相绑定。也许序列化函数也应该这样。虽然注解可能在语言习惯上不是太好,但是确实是非常有效。