Jade Dungeon

类与对象

在Scala里有些术语和Java里不太一样。为了不搞混,在这里说明:

  • Scala和Java里「类」这个概念没有差别。
  • Scala里的「对象」是指的一种对「类」起补充说明、有点像Java里单例对象的东西。
  • 「类」的实例在Java里叫「对象」,而Scala里就叫「实例」。

定义类

注意:Scala中的类和方法默认是public的。

简单定义类与创建对象:

scala> class ChecksumAccumulator { }
defined class ChecksumAccumulator

如果类没有主体,就不需要指定一对空的大括号(当然你如果想的话也可以)。

scala> class ChecksumAccumulator
defined class ChecksumAccumulator

创建实例

scala> new ChecksumAccumulator
res0: ChecksumAccumulator = ChecksumAccumulator@91f1520

scala> class ChecksumAccumulator {
     |   var sum = 0
     | }
defined class ChecksumAccumulator

scala> val acc = new ChecksumAccumulator
acc: ChecksumAccumulator = ChecksumAccumulator@501fdcfb

scala> val csa = new ChecksumAccumulator
csa: ChecksumAccumulator = ChecksumAccumulator@58f285cd

默认访问控制为public

成员方法:

class ChecksumAccumulator {
	private var sum = 0
	
	def add(b: Byte): Unit = {
		sum += b
	}

	def checksum(): Int =  {
		return ~(sum & 0xFF) + 1
	}
}

Scala中参数都是val,不可变。

	def add(b: Byte): Unit = {
		// b = 1   // error, because b is val
		sum += b
	}

只有一行的方法体可以去掉花括号并放在函数头一行,方法会自动返回最后一行语句, 所以可以不用写return语句:

class ChecksumAccumulator {
	private var sum = 0
	def add(b: Byte): Unit = sum += b
	def checksum(): Int =  ~(sum & 0xFF) + 1
}

没有返回的方法可以省略类型Unit与等号:

	def add(b: Byte): Unit = sum += b
	// 简化
	def add(b: Byte) { sum += b }

在实践中学习

我们以实现一个实数(rational number)类的过程作为例子,来说明类的实现细节。

实数由两部分组成:表示分子(numerator)和分母(denominator)。其中分母不能为零。 要模拟加,减,乘还有除运算。

  • 要加两个分数,首先要获得公分母,然后才能把两个分子相加。
  • 要乘两个分数,可以简单的两个分子相乘,然后两个分母相乘。
  • 除法是把右操作数分子分母调换,然后做乘法。
  • 所有的操作结果产生新的实例,而不会被修改原来的实例。

主构造器:primary constructor

class Rational(n: Int, d: Int)

没有花括号是因为之前说过:没有内容花括号可以省略。

Java类具有可以带参数的构造器,而Scala类可以直接带参数。在类名Rational 之后括号里的nd,被称为类参数(class parameter)。Scala编译器会收集这两个 参数并创造一个带同样的两个参数的主构造器(primary constructor)。

这里的nd前面没有用valvar修饰,所以只会生成私有的同名字段与私有的get 、set方法,外部的类是访问不到这两个字段的。如果有valvar的话,生成私有的 final或非final的,但访问方法是public的。

注意:Scala编译器将把你放在类内部的任何不是字段的部分或者方法定义的代码作为 主构造器的内容。例如:

scala> class Rational(n: Int, d: Int) { println("Created "+n+"/"+d) }

scala> new Rational(1, 2)
Created 1/2 res0: Rational = Rational@a0b0f5

你可以像这样打印输出一条消息,因为打印语句被作为主构造器的内容执行了。

先决条件(precondition)

先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。使用Predef 包中的require方法。如果传入的值为真,require将正常返回。反之,require 将通过抛出IllegalArgumentException来阻止对象被构造。

class Rational(n: Int, d: Int) { 
	require(d != 0) 
}

字段

Scala自动为成员生成getter/setter访问器,但格式与Java不同。例如对字段position 生成读取方法为position(),赋值方法为position_=(String)。注意转为Java代码后 =是关键字,所以在Java里调用的话是position_eq(java.lang.String)

如果想要生成Java风格的访问方法,可以加上scala.reflect.BeanProperty注解:

@scala.reflect.BeanProperty
var age: Int = _

要注意的是:

scala> class Rational(n: Int, d: Int)

在前面主构造器部分已经提别提到:主构造器的两个参数nd没有valvar修饰, 所以在类中生成的字段与访问器都是私有的,外部无法访问。所以下面代码是无法访问到 某个实例的nd的:

def showRational(r: Rational): Rational = println("Rational: "+n+"/"+d) 

所以又增加了两个字段,分别是numerdenom,并用类参数nd初始化它们:

class Rational(n: Int, d: Int) {
	require(d != 0) 

	val numer: Int = n 
	val denom: Int = d 
}

在对象外面访问分子和分母:

scala> val r = new Rational(1, 2) 
r: Rational = 1 / 2 

scala> r.numer 
res7: Int = 1 

scala> r.denom 
res8: Int = 2

方法

添加加法运算,得到另外一个分数后返回一个新对象为二者的和:

class Rational(n: Int, d: Int) {
	require(d != 0) 

	val numer: Int = n 
	val denom: Int = d 

	def add(that: Rational): Rational = new Rational( 
		numer * that.denom + that.numer * denom, 
		denom * that.denom 
	)
}

加法操作:

scala> val oneHalf = new Rational(1, 2) 
oneHalf: Rational = 1/2 

scala> val twoThirds = new Rational(2, 3) 
twoThirds: Rational = 2/3 

scala> oneHalf add twoThirds
res0: Rational = 7/6

自指向

关键字this指向当前执行方法被调用的对象实例,或者如果使用在构造器里的话, 就是正被构建的对象实例。

例如,我们考虑添加一个方法,lessThan,来测试给定的分数是否小于传入的参数:

def lessThan(that: Rational) = 
	this.numer * that.denom < that.numer * this.denom 

这里,this.numer指向lessThan被调用的那个对象的分子。你也可以去掉this 前缀而只是写numer

举一个不能缺少this的例子,考虑在Rational类里添加max方法返回指定分数和 参数中的较大者:

def max(that: Rational) = 
	if (this.lessThan(that)) that else this

这里,第一个this是冗余的,你写成(lessThan(that))也是一样的。 但第二个this表示了当测试为假的时候的方法的结果;如果你省略它, 就什么都返回不了了。

从构造器

有些时候一个类里需要多个构造器。Scala里主构造器之外的构造器被称为从构造器 (auxiliary constructor)。格式为def this(...)

Java里,构造器的第一个动作必须要么调用同类里的另一个构造器,要么直接调用超类的 构造器。Scala的类里面,只有主构造器可以调用超类的构造器,从构造器的第一个语句 要么调用主构造器,要么调用另一个从构造器。Scala里更严格的限制实际上是权衡了更高 的简洁度和与Java构造器相比的简易性所付出的代价之后作出的设计:主构造器就像一个 守门人,即控制实例的初始化也控制着与超类的沟通。

比方说,分母为1的分数只写分子的话就更为简洁。如,对于5/1来说,可以只是写成 5。因此,如果不是写成Rational(5, 1),客户程序员简单地写成Rational(5) 或许会更好看一些。

这就需要给Rational添加一个只带一个参数分子的从构造器并预先设定分母为1

class Rational(n: Int, d: Int) { 
	require(d != 0) 

	val numer: Int = n 
	val denom: Int = d 

	def this(n: Int) = this(n, 1)

Rational的从构造器主体几乎完全是调用主构造器,直接传递了它的唯一的参数n 作为分子和1作为分母。

私有字段和方法

分数的分子分母有时可以约掉,添加一个最大公约数的私有方法:

class Rational(n: Int, d: Int) { 
	require(d != 0) 

	private val g = gcd(n.abs, d.abs) 

	val numer = n / g 
	val denom = d / g 

	private def gcd(a: Int, b: Int): Int = 
		if (b == 0) a else gcd(b, a % b) 
}

定义操作符

用通常的数学的符号替换add方法,同样实现一个*方法以实现乘法:

def +(that: Rational): Rational = new Rational( 
	numer * that.denom + that.numer * denom, 
	denom * that.denom 

def *(that: Rational): Rational = 
	new Rational(numer * that.numer, denom * that.denom)

使用

scala> val x = new Rational(1, 2)
x: Rational = 1/2

scala> val y = new Rational(2, 3) 
y: Rational = 2/3 

scala> x.+(y) 
res33: Rational = 7/6

scala> x + y 
res32: Rational = 7/6

而且实现的加法和乘法都带有优先级(与Scala的操作符优先级相同):

scala> x + x * y 
res34: Rational = 5/6 

scala> (x + x) * y 
res35: Rational = 2/3 

scala> x + (x * y) 
res36: Rational = 5/6

方法覆盖(override)

override修饰符表示覆盖之前的方法定义。Rational类里覆盖了toString方法的 默认实现。如:

class Rational(n: Int, d: Int) { override def toString = n +"/"+ d } 

方法定义前的override修饰符标示了之前的方法定义被重载;第10章会更进一步说明。 现在分数显示得很漂亮了,所以我们去掉了前一个版本的Rational类里面的println 除错语句。你可以在解释器里测试Rational的新行为:

scala> val x = new Rational(1, 3) 
x: Rational = 1/3 

scala> val y = new Rational(5, 7) 
y: Rational = 5/7

方法重载(overload)

方法的参数表不同产生重载。

给每个数学方法都有两个版本了:一个带分数做参数,另一个带整数。

def +(that: Rational): Rational = new Rational( 
    numer * that.denom + that.numer * denom, 
    denom * that.denom 
  ) 

def +(i: Int): Rational = new Rational(numer + i * denom, denom) 

def -(that: Rational): Rational = new Rational( 
    numer * that.denom - that.numer * denom, 
    denom * that.denom 
  ) 

def -(i: Int): Rational = new Rational(numer - i* denom, denom) 

def *(that: Rational): Rational = new Rational(
    numer * that.numer, 
    denom * that.denom
  ) 
 
def *(i: Int): Rational = new Rational(numer * i, denom) 

def /(that: Rational): Rational = new Rational(
   numer * that.denom, 
    denom * that.numer
  ) 
 
def /(i: Int): Rational = new Rational(numer, denom * i)

隐式转换

虽然现在可以写r * 2了,但是不能用2 * r这样的写法:

scala> val x = new Rational(2, 3)

scala> 2 * r
error: overloaded method value * with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (this.Rational)

出错的原因是因为Int类上没有重载乘法运算符来处理我们的Rational类。 当然我们也不可能去修改Int类的源代码。

即使是这样,Scala里我们也有解决方案:

解决的方案是告诉Scala如何把Int类转换为Rational类,再加上修饰符implicit 通知Scala编译器可以自动调用:

scala> implicit def intToRational(x: Int) = new Rational(x)

scala> 2 * r
res16: Rational = 4/3

隐式转换只能在定义的作用范围内起作用,如果隐式方法被定义在Rational类中, 就不在解释器的作用范围内,所以要把它定义在解释器内。

完整的Rational代码

class Rational(n: Int, d: Int) { 
	require(d != 0) 

	private val g = gcd(n.abs, d.abs)
	val numer = n / g 
	val denom = d / g 

	def this(n: Int) = this(n, 1)

	def +(that: Rational): Rational = new Rational( 
    numer * that.denom + that.numer * denom, 
    denom * that.denom 
  ) 

  def +(i: Int): Rational = new Rational(numer + i * denom, denom) 

  def -(that: Rational): Rational = new Rational( 
      numer * that.denom - that.numer * denom, 
      denom * that.denom 
    ) 

  def -(i: Int): Rational = new Rational(numer - i* denom, denom) 

  def *(that: Rational): Rational = new Rational(
      numer * that.numer, 
      denom * that.denom
    ) 

  def *(i: Int): Rational = new Rational(numer * i, denom) 

  def /(that: Rational): Rational = new Rational(
     numer * that.denom, 
     denom * that.numer
    ) 

  def /(i: Int): Rational = new Rational(numer, denom * i)

	def lessThan(that: Rational) = 
		this.numer * that.denom < that.numer * this.denom 

	def max(that: Rational) = 
		if (this.lessThan(that)) that else this

	override def toString = n +"/"+ d

	private def gcd(a: Int, b: Int): Int = 
		if (b == 0) a else gcd(b, a % b) 
}

val x = new Rational(2, 3)
print("    x = ");  println(x)
print("x * x = ");  println(x * x)
print("x * 2 = ");  println(x * 2)

implicit def intToRational(x: Int) = new Rational(x)
print("2 * x = ");  println(2 * x)

对象

单例对象

Scala中没有像Java那样的静态成员而是用单例对象(Singleton Object)来代替。

在定义格式基本上和类一样,除了了object关键字代替class

object ObjNam{
	// ...
}

而且Scala中的对象也可以继承类:

class Currency {}

object USD extends Currency{ }

但是注意单例对象无法初始化,所以不能给主构造器传递参数。对象的主构造器会在对象 第一次被使用时调用。如果没有使用过对象,那么它的构造函数也不会被执行。例:

object Account {
	private var lastNumber = 0
	
	def newUniqueNumber() = { lastNumber += 1; lastNumber }

如果没用用过,那么lastNumber也不会被初始化。

伴生对象

如果一个单例对象的名字和类一样,并且必须在同一个文件里。那它就是这个类的伴生对象 (Companion Object),类是它的伴生类(Companion Class)。它们可以相互访问私有 成员。

但是要注意,虽然可以相互访问私有成员,但并不是在同一作用域中。例如:Account类 必须通过Account.newAccount()而不是newAccount()方式来访问伴生对象的方法。

import scala.collection.mutable.Map

class ChecksumAccumulator {
	private var sum = 0
	def add(b: Byte) { sum += b }
	def checksum(): Int =  ~(sum & 0xFF) + 1
}

object ChecksumAccumulator {
	private val cache = Map[String, Int]()

	def caculate(s: String): Int = {
		if (cache.contains(s))
			cache(s)
		else {
			val acc = new ChecksumAccumulator
			for (c <- s)
				acc.add(c.toByte)
			val cs = acc.checksum()
			cache += (s -> cs)
			cs
		}
	}
}

直接通过调用方法:

val res1 = ChecksumAccumulator.caculate("Every value is an object")
println(res1)

val res2 = ChecksumAccumulator.caculate("So simple!")
println(res2)

私有构造函数

一个典型的应用场景是,把类的主构造器做成私有的,通过伴生对象来访问工厂方法:

class Marker private (val color:String) {
    println("Createing " + this)

    override def toString(): String = "make color " + color
}

object Marker {
  private val markers = Map(
    "red" -> new Marker("red"),
    "green" -> new Marker("green"),
    "blue" -> new Marker("blue")
  )

  def getMarker(color: String) =
    if(markers.contains(color))  markers(color) else null
}

println(Marker getMarker "blue")
println(Marker getMarker "blue")
println(Marker getMarker "red" )
println(Marker getMarker "red" )

伴生对象代替构造函数

伴生对象把对象名(参数表)的形式转为对象名.apply(参数表)的调用,看起来像是一个 不用new的构造函数:

class Marker private (val color:String) {
    println("Createing " + this)

    override def toString(): String = "make color " + color
}

object Marker {
    def apply(color: String) = new Marker(color)
}

val a = Marker("yellow")
val b = Marker("black")

这样的写法的一个优点是对嵌套表达式少写new会方便很多:

Array(Array(1,7), Array(2,9))

与Java的区别

总结一下,Scala中的类如果对应到Java的类的话,Scala的对象和Java中的对象不是同一回 事情,是另一个新的概念。

为了表达统一,我们把对应Java里对象的概念叫作「类的实例」。

Scala java
类的实例 对象
单例对象与伴生对象

这样「类」,「对象」,「类的实例」三个概念都有了明确的定义。

类与对象高级应用

结构类型

结构类型很像JavaScript或是Ruby中的鸭子类型:检查是否有指定的字段、方法。

如下面的appendLines()方法用结构类型声明参数可以是任何含有 append(str: String): Any方法的类型:

def appendLines(target: { def append(str: String): Any }, 
		lines: Iterable[String]) 
{
  for (l <- lines) { target.append(l); target.append("\n") }
}

val lines = Array("Mary", "had", "a", "little", "lamb")

val builder = new StringBuilder
appendLines(builder, lines)
println(builder)

由于在底层实现时是通过反射来调用target.append()方法,为了性能考虑只有在无法用 共同特质描述的情况下才用结构类型。

类型别名

关键字type可以给类型创建别名。别名必须放在类或对象中,不能出现在Scala顶层:

class Document {
  import scala.collection.mutable._

  type Index = HashMap[String, (Int, Int)]

}

不过在REPL环境中可以用别名,因为REPL已经在一个对象中了。

内部类

Scala中几乎可以在任何语法中嵌套任何语法:可以在函数中定义函数,也可以在类中定义 类。

类中的类

内部类的一个例子:

import scala.collection.mutable.ArrayBuffer

class Network {
  class Member(val name: String) {
    val contacts = new ArrayBuffer[Member]
  }

  private val members = new ArrayBuffer[Member]

  def join(name: String) = {
    val m = new Member(name)
    members += m
    m
  }
}

对于两个不同的网络实例:

val chatter = new Network
val myFace = new Network

内部的Member是不同的,也就是说chatter.MembermyFace.Member是两个不同的类 。这与Java不同,Java里同一个类的内部类是相同的。

在新建实例时,Scala中:

new chatter.Member

而在Java里,这个语法看起来有点别扭:

chatter.new Member()

以我们的例子来说,可以在各自的网络中添加成员,但不能跨网添加成员:

val fred = chatter.join("Fred")
val wilma = chatter.join("Wilma")
fred.contacts += wilma // OK
val barney = myFace.join("Barney") // Has type myFace.Member
fred.contacts += barney // No
  // Can’t add a myFace.Member to a buffer of chatter.Member elements

如果想要跨网加,可以有两个方案:

  • 用移到伴生对象中
  • 类型投影

伴生对象中的类

import scala.collection.mutable.ArrayBuffer

class Network {
  private val members = new ArrayBuffer[Network.Member]

  def join(name: String) = {
    val m = new Network.Member(name)
    members += m
    m
  }
  def description = "a network with members " + 
    (for (m <- members) yield m.description).mkString(", ")
}

object Network {
  class Member(val name: String) {
    val contacts = new ArrayBuffer[Member]
    def description = name + " with contacts " + 
      (for (c <- contacts) yield c.name).mkString(" ")
  }
}

object Main extends App {
  val chatter = new Network
  val myFace = new Network

  val fred = chatter.join("Fred")
  val wilma = chatter.join("Wilma")
  fred.contacts += wilma // OK
  val barney = myFace.join("Barney")
  fred.contacts += barney // Also OK
  println("chatter is " + chatter.description)
  println("myFace is " + myFace.description)
}

类型投影

上面那样把内部类放在伴生对象中,所以的内部类都可以跨实例匹配了。如果只是想在指定 的语句中跨实例,用类型投影的的方式Network#Member表示是任何NetworkMember ,如:

class Network {
  class Member(val name: String) {
    val contacts = new ArrayBuffer[Network#Member]
  }

  private val members = new ArrayBuffer[Member]

  def join(name: String) = {
    val m = new Member(name)
    members += m
    m
  }
}

val chatter = new Network
val myFace = new Network

val fred = chatter.join("Fred")
val wilma = chatter.join("Wilma")
fred.contacts += wilma // OK
val barney = myFace.join("Barney") 
fred.contacts += barney // Also OK

外部类的引用

像Java一样,在内部类中可以通过外部类.this的方式来得到外部类的this引用。

如果有必要,还可以用下面这样的语法建立一个指向该引用的别名:

class 外部类 { 别名 =>
	class 内部类 {
	}
}

例如:

import scala.collection.mutable.ArrayBuffer

class Network(val name: String) { outer =>  
  class Member(val name: String) {
    val contacts = new ArrayBuffer[Member]
    def description = name + " inside " + outer.name
  }

  private val members = new ArrayBuffer[Member]

  def join(name: String) = {
    val m = new Member(name)
    members += m
    m
  }
}

object Main extends App {
  val chatter = new Network("Chatter")
  val myFace = new Network("MyFace")

  val fred = chatter.join("Fred")
  println(fred.description);
  val barney = myFace.join("Barney")
  println(barney.description);
}

这里的outer指向的是Ntework.this,引用名可以是任何合法的标识符。当然推荐不要 用self,这个名字在内部类中容易引起误解。

这样的语法与自身类型语法相关,关于它后面有更多描述。

路径

包名加上类名就形成了典型的路径。用具体的定义来说,路径必须是不可变的,内容可以 是:

  • 对象
  • val
  • thissupersuper[S]C.thisC.superC.super[S]

不可以是:

  • 路径的组成部分不可以是类。如:内部类是按个实例区分的不同类,所以不能用类表示。
  • 也不能是var,因为var的内容是可变的,而路径是不可变的。

以内部类为例:

var personal = new Network
val fred = new personal.Member  // error person是可变引用

val wrok = new Network
val fred = new work.Member  // OK

其实对于内部类,编译器会把嵌套的表达式转为投影type#T。如:personal.Member转 为personal.type#Member,即personal.type单例对象中的Member

所以如果看到编译器报错personal.type#Member有问题,要明白所指的其实就是 personal.Member

与Java内部类的区别

路径依赖有点像Java里的内部类,但区别是:

  • 路径依赖表达了外在的对象
  • Java内部类表达了外在的类。

Java的内部类在Scala表达为两个类:

  class Outer {
    class Inner
  }

与Java的Outer.Inner不同,Scala中表达为Outer#Inner

总之,要注意。路径依赖是在对象中的类,而不是Java那样类中的类。

.语法留给对象使用:

  val o1 = new Outer
  val o2 = new Outer

虽然o1.Innero2.Inner是不同的两个路径依赖类型,但两个都能匹配更加通用的 Outer#Inner

和Java中一样,Scala的内部类实例也有对外部类实例的引用。所以不能只有内部类实例而 没有外部类实例。有两个方式实例化内部类:

  • 直接在外部类方法体中实例化,这样可以用this引用外部类对象。
  • 使用路径依赖类型。如o1.Inner。返回的内部类有对01的引用。例子如下:
  scala> new o1.Inner
  res1: o1.Inner = Outer$Inner@13727f

相对的,类型Outer#Inner是没有指向对象的引用的,所以不能创建它的实例:

  scala> new Outer#Inner
  <console>:6: error: Outer is not a legal prefix for
    a constructor
         new Outer#Inner
                   ^

枚举对象

Scala没有枚举类,而是用标准库中的工具类scala.Enumeration用来扩展实现枚举对象:

  object Color extends Enumeration {
    val Red = Value
    val Green = Value
    val Blue = Value
  }

注意:枚举是和路径依赖一样,是对象中的类,不是类中的类。

还可以简化:

  object Color extends Enumeration {
    val Red, Green, Blue = Value
  }

可以Color的全部成员,然后直接写颜色名:

  import Color._

前面定义的RedGreenBlue这些值的类型为Enumeration定义的内部类,名为 Value。同名无参数方法Value返回该类的新对象,即Color.Red类的值类型是 Color.Value。而且是依赖路径的。如:

  object Direction extends Enumeration {
    val North, East, South, West = Value
  }

上面就定义了一个完全不同的类型,因为路径不同。DirectionColorValue也是 不同的类。

枚举的类型

注意枚举的类型是Direction.Value而不是DirectionDirection是持有这些值的 单例对象。有些人推荐增加一个类型的别名:

  object Direction extends Enumeration {
		type Direction = Value
    val North, East, South, West = Value
  }

这样类型就从Direction.Value增加一个别名Direction.Direction。虽然还是又长又 啰嗦,但是在import Direction._以后可以只写Direction

import Direction._

def func01(direction: Direction) = {
	if (direction == West)
		"stop"
	else
		"keep moving"
}

枚举值

枚举的值默认从0开始增加,用成员方法id可以取出值:

  scala> Direction.East.id
  res5: Int = 1

反过来也可以通过非零整数取得id

  scala> Direction(1)
  res6: Direction.Value = East

当然想指定id也可以:

object Direction extends Enumeration {
  val North = Value(10)
  val East = Value(20)
  val South = Value(30)
  val West = Value(40)
}

还可以重载Value方法把名称与值对应起来:

object Direction extends Enumeration {
  val North = Value(10,"North")
  val East = Value(20,"East")
  val South = Value(30,"South")
  val West = Value(40,"West")
}

直接通过toString方法就可以取得枚举的名称:

  scala> for (d <- Direction) print(d +" ")
  North East South West 

也可以通过withName方法通过名称得到枚举:

Direction.withName("West")

枚举类型的缺陷

然而,这种方法有一些问题。主要有两个缺点:

  • 类型擦除
  • 编译时类型检查漏洞

擦除(erasure)后枚举具有相同的类型:

object Weekday extends Enumeration {
    val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}

object OtherEnum extends Enumeration {
	val A, B, C = Value
}

def test(enum: Weekday.Value) = {
    println(s"enum: $enum")
}

def test(enum: OtherEnum.Value) = {
    println(s"enum: $enum")
}

<console>:25: error: double definition:
def test(enum: Weekday.Value): Unit at line 21 and
def test(enum: OtherEnum.Value): Unit at line 25
have same type after erasure: (enum: Enumeration#Value)Unit
         def test(enum: OtherEnum.Value) = {
             ^

在编译期间没有详尽的匹配检查(matching check)。 下面的示例将在没有任何警告的情况下编译,但是在对周一和周日以外的工作日匹配时 会抛出scala.MatchError异常:

def nonExhaustive(weekday: Weekday.Value) {
  weekday match {
    case Monday => println("I hate Mondays")
    case Sunday => println("The weekend is already over? ")
  }
}

在Scala中,我们严重依赖于编译器强大的类型系统,使用这种方法, 编译器不能找到非穷尽模式匹配子句,也不能对不同的枚举使用重载方法。

为了避免这种问题,我们可以第三方的枚举实现:enumeratum

第三方的Enumeratum

com.beachape.enumeratum是一个类型安全且功能强大的Scala枚举实现, 它提供了详尽的模式匹配警告。

import enumeratum._

sealed trait Weekday extends EnumEntry
object Weekday extends Enum[Weekday] {
  val values = findValues // mandatory due to Enum extension

  case object Monday extends Weekday
  case object Tuesday extends Weekday
  case object Wednesday extends Weekday
  case object Thursday extends Weekday
  case object Friday extends Weekday
  case object Saturday extends Weekday
  case object Sunday extends Weekday
}

def test(weekday: Weekday) = {
    weekday match {
      case Weekday.Monday => println("I hate Mondays")
      case Weekday.Sunday => println("The weekend is already over? :( ")
    }
}

<console>:18: warning: match may not be exhaustive.
It would fail on the following inputs: Friday, Saturday, Thursday, Tuesday, 
Wednesday
           weekday match {
           ^
test: (weekday: Weekday)Unit

除了非详尽的模式匹配警告,enumeratum还提供:

  • 列出可能的值(因为这些值需要在Enum继承上实现)
  • 默认的序列化/反序列化方法(有和没有异常抛出)
scala> Weekday.withName("Monday")
res0: Weekday = Monday

scala> Weekday.withName("Momday")
java.util.NoSuchElementException: Momday is not a member of Enum 
(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
  at enumeratum.Enum$$anonfun$withName$1.apply(Enum.scala:82)
  at enumeratum.Enum$$anonfun$withName$1.apply(Enum.scala:82)
  at scala.Option.getOrElse(Option.scala:121)
  at enumeratum.Enum$class.withName(Enum.scala:81)
  at Weekday$.withName(<console>:13)
  ... 43 elided

scala> Weekday.withNameOption("Monday")
res2: Option[Weekday] = Some(Monday)

scala> Weekday.withNameOption("Momday")
res3: Option[Weekday] = None

向枚举添加额外的值。它非常类似于我们给简单的密封盒对象添加额外的值:

sealed abstract class Weekday( val name: String,
                               val abbreviation: String,
                               val isWorkDay: Boolean) extends EnumEntry

case object Weekday extends Enum[Weekday] {
  val values = findValues
  case object Monday extends Weekday("Monday", "Mo.", true)
  case object Tuesday extends Weekday("Tuesday", "Tu.", true)
  case object Wednesday extends Weekday("Wednesday", "We.", true)
  case object Thursday extends Weekday("Thursday", "Th.", true)
  case object Friday extends Weekday("Friday", "Fr.", true)
  case object Saturday extends Weekday("Saturday", "Sa.", false)
  case object Sunday extends Weekday("Sunday", "Su.", false)
}

排序可以通过与封闭层次(sealed hierarchies)结构相同的方式实现。 只需与有序特质(trait)混合,并实现比较方法:

sealed abstract class Weekday(val order: Int) extends EnumEntry with Ordered[Weekday] {
   def compare(that: Weekday) = this.order - that.order
 }

 object Weekday extends Enum[Weekday] {
   val values = findValues

   case object Monday extends Weekday(2)
   case object Tuesday extends Weekday(3)
   case object Wednesday extends Weekday(4)
   case object Thursday extends Weekday(5)
   case object Friday extends Weekday(6)
   case object Saturday extends Weekday(7)
   case object Sunday extends Weekday(1)
 }

实例类型与单例类型

如果一个方法返回当前对象的引用this,那就可以把方法的调用串起来写:

scala> class Document {
     |   private var title = ""
     |   private var author = ""
     |   def setTitle(title: String) = { this.title = title; this }
     |   def setAuthor(author: String) = { this.author = author; this }
     |   override def toString = getClass.getName + "[title=" + title +
     |     ",author=" + author + "]"
     | }
defined class Document

scala> val doc = new Document
doc: Document = Document[title=,author=]

scala> doc.setTitle("Scala for the Impatient").setAuthor("Cay Horstmann")
res0: Document = Document[title=Scala for the Impatient,author=Cay Horstmann]

但是如果Document的子类Book的话,那就会有问题了:

scala> class Book extends Document {
     |   private var chapters = new scala.collection.mutable.ArrayBuffer[String]
     |   def addChapter(chapter: String) = { chapters += chapter; this }
     |   override def toString = super.toString + "[chapters=" + chapters + "]"
     | }
defined class Book

scala> val book = new Book
book: Book = Book[title=,author=][chapters=ArrayBuffer()]

scala> book.setTitle("scala for the Impatient").addChapter("Chap I")
<console>:11: error: value addChapter is not a member of Document
              book.setTitle("scala for the Impatient").addChapter("Chap I")
                                                       ^

因为方法setTitle()是父类Document的方法,返回类型也被绑定为父类。

解决方法:对于任何引用v,调用v.type可以取得引用的类型。值可能是vnull。 这里声明setTitle()的返回类型为this.type

class Document {
  private var title = ""
  private var author = "" 
  def setTitle(title: String): this.type = { this.title = title; this }
  def setAuthor(author: String): this.type = { this.author = author; this }
  override def toString = getClass.getName + "[title=" + title + ",author=" + 
	  author + "]"
}

class Book extends Document {
  private var chapters = new scala.collection.mutable.ArrayBuffer[String]
  def addChapter(chapter: String) = { chapters += chapter; this }
  override def toString = super.toString + "[chapters=" + chapters + "]"
}

object Main extends App {
  val book = new Book
  book.setTitle("Scala for the Impatient").addChapter("Chapter 1 ...") 
  println(book)
}

单例对象也可以通过type取得它的类型:

object Title // This object is used an an argument for a fluent interface

class Document {
  private var title = ""
  private var useNextArgAs: Any = null
  def set(obj: Title.type): this.type = { useNextArgAs = obj; this }
  def to(arg: String) = if (useNextArgAs == Title) title = arg;
  override def toString = getClass.getName + "[title=" + title + "]"
}

val book = new Document

注意是Title.type而不是TitleTitle是一个单例对象,不是类型。

调用时:

scala> book.set(Title).to("Scala for the Impatient")

scala> println(book)
$line4.$read$$iw$$iw$Document[title=Scala for the Impatient]

可以写在更加接近人类语言的:

scala> book set Title to "Scala for the Impatient"

scala> println(book)
$line4.$read$$iw$$iw$Document[title=Scala for the Impatient]

反射

类型检查和转换

Scala Java
obj.isInstanceOf[Clazz] obj instanceof Clazz
obj.asInstanceOf[Clazz] (Clazz) obj
obj.to<Clazz> 基本类型的强转,如:val f: Float = 3.14.toFloat (Clazz) obj 基本类型的强转,如:float f = (float) 3.14
classOf[Clazz] Clazz.class
obj.getClass obj.getClass

asInstanceOf强转不兼容的类型就会出错,所以推荐用to<Clazz>这种成员方法。 因为有成员方法支持的强转肯定是兼容的类型。

val c = new C
val clazz = c.getClass              // method from java.lang.Object
val clazz2 = classOf[C]             // Scala method: classOf[C] ~ C.class
val methods = clazz.getMethods      // method from java.lang.Class<T>

类似于Java的T.class表达式,Scala的classOf[T]方法返回运行时表示的类。

  • classOf[T]方法可以方便地取得类的相关信息。
  • obj.getClass成员方法可以取得当前实例的类信息。

然而classOf[T]getClass的返回结果会因为JVM的类型擦除机制而略有不同。

scala> classOf[C]
res0: java.lang.Class[C] = class C

scala> c.getClass
res1: java.lang.Class[_] = class C

这就是下面代码的结果与预期不同:

val xClass: Class[X] = new X().getClass //it returns Class[_], nor Class[X]

val integerClass: Class[Integer] = new Integer(5).getClass //similar error

有一些与getClass方法相关的待修复错误,James Moore报告的错误在2001年标记为修复 :

Scala 2.9.1版时getClass方法的效果:

scala> "foo".getClass 
       res0: java.lang.Class[_ <: java.lang.String] = class java.lang.String

Back in 2009:

It would be useful if Scala were to treat the return from getClass() as a java.lang.Class[T] forSome { val T : C } where C is something like the erasure of the static type of the expression on which getClass is called

It would let me do something like the following where I want to introspect on a class but shouldn't need a class instance.

I also want to limit the types of classes I want to introspect on, so I use Class[_ <: Foo]. But this prevents me from passing in a Foo class by using Foo.getClass() without a cast.

Note: regarding getClass, a possible workaround would be:

class NiceObject[T <: AnyRef](x : T) {
  def niceClass : Class[_ <: T] = x.getClass.asInstanceOf[Class[T]]
}

implicit def toNiceObject[T <: AnyRef](x : T) = new NiceObject(x)

scala> "Hello world".niceClass                                       
res11: java.lang.Class[_ <: java.lang.String] = class java.lang.String

相比类型检查与转换,Scala更加推荐模式匹配的方式。

构造函数

scala> class User(val id: String, val name: String) {
     |   def this(id: String) { this(id, "unknow") }
     |   def this() { this("unknow", "unknow") }
     | }
defined class User

scala> val user = new User
user: User = User@13c1f28b

scala> val clazz = user.getClass
clazz: Class[_ <: User] = class User

scala> clazz.getConstructors
res7: Array[java.lang.reflect.Constructor[_]] = Array(public User(), 
	public User(java.lang.String), public User(java.lang.String,java.lang.String))

scala> val ctr = clazz.getConstructor(classOf[String], classOf[String])
ctr: java.lang.reflect.Constructor[_ <: User] = public User(java.lang.String,java.lang.String)

scala> val u = ctr.newInstance("007", "James Bond")
u: User = User@2fe48c89