特质(trait)是scala里代码服用的基础单元。特质封装了方法和字段的定义,并可以通过“混入”到类中重用它们。与类的继承时每个类都只能继承唯一的超类不同,类可以混入任意多个特质。特质的定义除了使用关键字trait之外,与类定义无异,如代码1-1
代码1-1
trait Bird { def fly = println("鸟飞翔") def singing }
这个特质名为Fish,它没有声明超类,因此和类一样,有个默认的超类AnyRef。它定义了一个具体方法fly,也定义了一个抽象方法singing,等待被混入的类实现。一旦特质被定义了,就可以使用extends或with关键字,把它混入到类中,scala的混入特质并不是继承它们,将在本文的后面说明。
模拟一个场景,一个机器人继承了人类,人会说话,而机器人想要继续继承鱼的游泳、和鸟的飞翔和唱歌,需要用到特质,如代码1-2,Rebot类最先开始继承Person类,接着混入Fish特质和Bird特质,因此,机器人除了能工作,还能模拟鸟飞翔
代码1-2
scala> class Person { def say = println("人说话") } trait Fish { def swim = println("鱼游泳") } trait Bird { def fly = println("鸟飞翔") def singing } class Rebot extends Person with Fish with Bird { def work = println("机器人工作") def singing = println("机器人唱歌") } defined class Person defined trait Fish defined trait Bird defined class Rebot scala> val rebot = new Rebot() rebot: Rebot = [email protected] scala> rebot.work机器人工作 scala> rebot.fly 鸟飞翔 scala> rebot.singing 机器人唱歌 scala> val fish: Fish = rebot fish: Fish = [email protected] scala> fish.swim 鱼游泳
另外,因为Rebot这个类混入了Fish特质,所以可以用Fish的变量去接收Rebot对象,特质有点类似Java的抽象类,可以声明抽象方法和具体方法,但是Java的一个类只能继承一个抽象类,而scala一个类却可以混入多个特质,另外,特质不能带有任何类参数。代码1-3是合法的,而代码1-4则会报错
代码1-3
class Cat(color: String, age: Int)
代码1-4
trait Cat(color: String, age: Int)
如果想在特质里加入参数时,可如代码1-5这样做
代码1-5
scala> trait Cat { val color: String val age: Int def cry = println("小猫喵喵叫") def printColor = println("小猫的颜色是" + color) def printAge = println("小猫的年龄是" + age + "岁")}defined trait Cat scala> val cat = new { val color = "白色" val age = 1 } with Catcat: Cat = [email protected] scala> cat.printColor小猫的颜色是白色 scala> cat.printAge小猫的年龄是1岁 scala> cat.cry小猫喵喵叫
Ordered特质
对象的比较是程序里常见的操作,比方定义一个有理数对象,用户可能调用<或者>=来判断两个对象之间的大小关系,如果没有Ordered特质之前,我们一般会像代码1-6这样做,Rational类是有理数类,类参数n代表分子,类参数d代表分母,私有成员g是n和d的最大公约数,我们会定义操作符来判断两个有理数对象的大小关系
代码1-6
scala> class Rational(n: Int, d: Int) { require(d != 0) private val g = gcd(n, d) val number = n / g val denom = d / g override def toString = number + "/" + denom private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) def <(that: Rational) = number * that.denom < that.number * denom def >(that: Rational) = that < this def ==(that: Rational) = (number == that.number && denom == that.denom) def <=(that: Rational) = this < that || this == that def >=(that: Rational) = this > that || this == that } defined class Rational scala> val x = new Rational(1, 2) x: Rational = 1/2 scala> val y = new Rational(1, 3) y: Rational = 1/3 scala> x > y res11: Boolean = true
通过定义符号,我们可以比较对象间的大小关系,但scala专门提供了一个特质解决这样的问题,这个特质就是Ordered,Ordered特质让你仅仅只实现一个方法compare,使你的类可以使用>、<、>=、<=全套的比较方法,如代码1-7
代码1-7
scala> class Rational(n: Int, d: Int) extends Ordered[Rational] { require(d != 0) private val g = gcd(n, d) val number = n / g val denom = d / g override def toString = number + "/" + denom private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) def compare(that: Rational) = number * that.denom - that.number * denom } defined class Rational scala> val a = new Rational(1, 3) a: Rational = 1/3 scala> val b = new Rational(1, 5) b: Rational = 1/5 scala> a > b res12: Boolean = true scala> val c = new Rational(1, 2) c: Rational = 1/2 scala> val d = new Rational(2, 5) d: Rational = 2/5 scala> c < d res13: Boolean = false scala> val e = new Rational(1, 3) e: Rational = 1/3 scala> val f = new Rational(1, 5) f: Rational = 1/5 scala> e >= f res14: Boolean = true
compare主要返回两个有理数对象做计算最后的结果,如果结果大于0则代表前一个对象大于后一个对象,如果结果小于0则代表前一个对象小于后一个对象,如果结果等于0代表两个对象相等
特质用来做可堆叠的改变
举个例子,思考一下对一个整数队列堆叠改动。队列有两种操作:put,把整数放入队列,和get,从队列取出整数,队列是先进先出的,因此get应该依整数进入队列时的顺序把它们取出来。
假设有一个类实现了这样的队列,你可以定义特质执行如下的改动:
- Doubling:把所有放入到队列的数字加倍。
- Incrementing:把所有放入到队列的数字增值
- Filtering:从队列中过滤掉负数
这三种特质代表了改动,因为它们改变了原始队列的行为而并非定义了全新的队列。这三种同样也是可堆叠的。
代码1-8为抽象的IntQueue类,put方法把整数添加到队列中,get方法返回并移除开头的整数,length返回队列的长度
abstract class IntQueue { def get(): Int def put(x: Int) def length(): Int }
代码1-9的BasicIntQueue类是抽象类IntQueue的实现
代码1-9
class BasicIntQueue extends IntQueue { private val buf = new scala.collection.mutable.ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) = buf += x def length() = buf.length }
代码1-10是运行时的样子:
代码1-10
scala> val queue = new BasicIntQueue queue: BasicIntQueue = [email protected] scala> queue.put(-1) scala> queue.put(2) scala> queue.put(3) scala> queue.length() res3: Int = 3 scala> queue.get() res4: Int = -1 scala> queue.get() res5: Int = 2 scala> queue.get() res6: Int = 3
现在用特质改变它的行为,代码1-11展示了过滤掉负数,Filtering做了两件事情,第一件是它定义了超类IntQueue,这个定义意味着它只能混入扩展了IntQueue,因此可以把Filtering混入到BasicIntQueue,第二件事情是特质在声明为抽象的方法中有一个super的调用。这种调用对于普通的类来说是非法的,执行时必然失败。然而对于特质来说,则能调用成功。因为特质的super调用是动态绑定的。特质Filtering的super调用将直到被混入另一个特质或类之后,有了具体的方法定义时才工作。
这种安排对于实现可堆叠改动的特质来说是常常要用到的,为了告诉编译器你的目的,你必须在这种方法打上abstract override的标志。这种标识符的组合仅在特质成员的定义中被认可,在类中不行,它意味着特质碧玺混入某个具有期待方法的具体定义的类中。
代码1-11
trait Filtering extends IntQueue { abstract override def put(x: Int) = if (x >= 0) super.put(x) }
代码1-12中,queue变量只是简单地指明了一个类并混入一个特质,可以用这样的形式来替代命名类,联系代码1-11和代码1-12,我们将-1、2、3依次放入队列,但最后返回的队列长度只有2,因为-1被过滤掉了
代码1-12
scala> val queue = new BasicIntQueue with Filtering queue: BasicIntQueue with Filtering = [email protected] scala> queue.put(-1) scala> queue.put(2) scala> queue.put(3) scala> queue.length() res3: Int = 2 scala> queue.get() res4: Int = 2 scala> queue.get() res5: Int = 3
我们再加入两个特质,如代码1-13
代码1-13
trait Doubling extends IntQueue { abstract override def put(x: Int) = super.put(2 * x) } trait Incrementing extends IntQueue { abstract override def put(x: Int) = super.put(x + 1) }
代码1-14,我们可以看到,queue最长的长度为3,然而我们却放入了4个整数,两个负数两个正数,并且返回的数跟我们原先放入的数不同,越靠近右侧的特质越先起作用。Incrementing最先调用,对所有放入的数加1,接着就是Filtering,我们放入的-10+1后为-9,依旧小于0,所有我们放入的整数只有-1、2、3能到达Doubling,在到达Doubling,3个数经过前两个特质的转化后,变为0、3、4,经过Doubling后,每个数乘以2,为0、6、8,最后放入队列的就是0、6、8。
代码1-14
scala> val queue = new BasicIntQueue with Doubling with Filtering with Incrementing queue: BasicIntQueue with Doubling with Filtering with Incrementing = [email protected] scala> queue.put(-10) scala> queue.put(-1) scala> queue.put(2) scala> queue.put(3) scala> queue.length() res10: Int = 3 scala> queue.get() res11: Int = 0 scala> queue.get() res12: Int = 6 scala> queue.get() res13: Int = 8
特质是一种继承多个类似于类的结构的方式,但是它与许多语言中的多重继承有很重要的差别。其中的一个尤为重要:super的解释。对于多重继承来说,super调用导致的方法调用可以在调用发生的地方明确决定。而对于特质来说,方法调用是由类和被混入到类的特质的线性化决定的。