传送门:Scala基础—类继承
- Scala的类继承
- 调用超类的构造方法
- 重写超类的成员 3.1.不被继承的成员 3.2.不可重写的成员 3.3.无参方法与字段
- 子类型多态与动态绑定
- 抽象类
- 关于多重继承
- Scala类的层次结构
因为Scala没有多重继承,为了提高代码复用率提出了特质的概念,在定义上它主要有以下几个特点(结合单例对象):
- 特质用关键字
“trait”
为开头来定义的,它与单例对象很像,两者都不能有入参。但是,单例对象天生就是具体的,特质天生就是抽象的,不过不需要用“abstract”
来说明。 - 因为特质是抽象的,所以可以包含抽象成员(成员可以全是抽象,或者全是具体,或者有抽象也有具体),而单例对象却不行。
- 两者都不能用new来实例化,因为特质是抽象的,而单例对象已经是具体的对象。
- 类、单例对象和特质三者一样,内部可以包含字段和方法,甚至包含其他类、单例对象、特质的定义。
- 特质用于代码重用,这与抽象基类的作用相似。不过,特质常用于混入在不相关的类中,而抽象基类则用于构成有继承层次的一系列相关类。
在混入特质时它有以下几个特点:
-
特质可以被其它类、单例对象和特质
“混入”
。这里使用术语“混入”而不是“继承”,是因为特质在超类方法调用上采用 线性化机制 ,与多重继承有很大的区别。其它方面,“混入”和“继承”其实是一样的。例如,某个类混入一个特质后,就包含了特质的所有公有成员,而且也可以用“override”来重写特质的成员。 -
Scala只允许继承自一个类,但是对特质的混入数量却没有限制,故而可用于替代多重继承语法。而且对于类的继承必须写在最前面,不能先混入特质,然后再继承类。
-
要混入一个特质,可以使用关键字
“extends”
。但如果“extends”
已经被占用了,比如已经拿去继承一个类或混入一个特质,那么后续则通过关键字“with”
来混入其他特质。也即混入特质既可以使用“extends”
,也可以使用with
。 -
extends
只能使用一次,且是用在最前面,但是with
可以使用多次。
class MyClass{
val c = "Alex"
val print = () => println(c)
}
trait A {
val absA : String
private val a = "Trait A"
}
trait B {
val b = "Trait B"
}
class sub extends MyClass with A with B{
val absA = "absA"//抽象成员加不加override都可以重写
override val a = "wzx"//出错,私有成员访问不到
val myprint = () => println(b,c)
}
再看一下几种错误的写法!!!
- 前面已经说过,不止
class、object
,trait
也可以继承自其他class
,或混入任意个trait
,这样该特质就是关键字“extends”引入的那个类/特质的子特质,注意只有使用extends
才能成为子特质,也即with
一个特质不能称为该特质的子特质。 - 如果没有继承和混入,那么这个
trait
就是AnyRef
类的子特质。当某个class、object或trait
混入一个trait
时,会隐式继承自这个trait
的超类。 - 也就是说,类/单例对象/特质的超类,都是由“extends”引入的类或混入的特质决定的。具体理解一下就是:
extends
引入类,那么这个类就是超类,这是肯定的;如果混入特质,并且特质使用extends
继承了一个类, 那么这个类会自动成为所有混入该特质的超类。
不过混入特质时有一个限制条件:那就是要混入该特质的类/单例对象/特质,它的超类必须是待混入特质的超类,或者是待混入特质的超类的子类。因为特质是多重继承的替代品,那就有“继承”的意思。既然是继承,混入特质的类/单例对象/特质的层次,就必须比待混入特质的层次要低。例如:
scala> class A
defined class A
scala> class B extends A
defined class B
scala> class C
defined class C
scala> trait D extends A
defined trait D
scala> trait E extends B
defined trait E
scala> class Test1 extends D
defined class Test1
scala> class Test2 extends A with D
defined class Test2
scala> class Test3 extends B with D
defined class Test3
scala> class Test4 extends C with D
:13: error: illegal inheritance; superclass C
is not a subclass of the superclass A
of the mixin trait D
class Test4 extends C with D
^
scala> class Test5 extends A with E
:13: error: illegal inheritance; superclass A
is not a subclass of the superclass B
of the mixin trait E
class Test5 extends A with E
- 类Test1直接混入特质D,这样隐式继承自D的超类——类A,所以合法。
- 类Test2继承自类A,A是D的超类,所以也允许混入特质D。
- 类Test3继承类A的子类B,A是D的超类,所以也允许混入特质D。
- 类Test4的超类是C,而C与A没有任何关系,所以非法。
- 类Test5的超类是A,特质E的超类是B,首先A不是E的超类B的子类;其次,虽然E的超类是B,B的超类是A,但是仍然没有符合A是E的超类这个条件。因为Scala没有多继承,所以你可以理解为超类的超类不能成为超类。
如果想快速构造一个混入某些特质的实例,可以使用如下语法:
new Trait1 with Trait2 ... { definition }
这其实是定义了一个匿名类,这个匿名类混入了这些特质,并且花括号内是该匿名类的定义。然后使用new构造了这个匿名类的一个对象,其等效的代码就是:
class AnonymousClass extends Trait1 with Trait2 ... { definition }
new AnonymousClass
除此之外,还可以在最前面加上一个想要继承的超类:
new SuperClass with Trait1 with Trait2 ... { definition }
五、子类型
一个类或者单例对象,如果混入了一个特质(extends和with
都行),那么它们就是该特质的子类型。凡是需要特质的地方,都可以由该特质的子类型来替换。
import scala.collection.mutable.ArrayBuffer
trait Pet {
val name: String
}
class animal
object Dog extends Pet {
val name: String = "Harry"
}
class Cat(val name: String) extends animal with Pet
val cat = new Cat("Sally")
val animals = ArrayBuffer.empty[Pet]
animals.append(Dog)
animals.append(cat)
animals.foreach(pet => println(pet.name)) // Prints Harry Sally
六、特质的线性化叠加计算
多重继承一个很明显的问题是,当子类调用超类的方法时,若多个超类都有该方法的不同实现,那么需要附加额外的语法来确定具体调用哪个版本。Scala的特质则是采取一种线性化的规则来调用特质中的方法,这与大多数语言不一样。在特质里,“super”调用是动态绑定的。也就是说,按特质本身的定义,无法确定super调用的具体行为;直到特质混入某个类或别的特质,有了具体的超类方法,才能确定super的行为。这是实现线性化的基础。
想要通过混入特质来实现某个方法的线性叠加,那么要注意以下要点:
- ①需要在特质里定义同名同参的方法,并加关键字组合
“abstract override”
,注意这不是重写,而是告诉编译器该方法用于线性叠加。这个关键字组合只能用在特质里,不允许用在其他地方。 - ②这个关键字组合也意味着该特质必须被某个拥有该方法具体定义的类混入(这个类的方法可以是自己实现或者重写的,也可以是继承的,总之它拥有该方法),也就是这个类定义了该方法的最终行为。如果被没有拥有该方法具体定义的类给混入了,那么就会报错。
- ③需要混入特质进行线性化计算的类,在定义时不能立即混入特质。这样做会让编译器认为这个类是在重写末尾那个特质的方法,而且当类的上一层超类是抽象类时还会报错。应该先定义这个类的子类来混入特质,然后构造子类的对象。或者直接用第三点讲的
“new SuperClass with Trait1 with Trait2 ...”
来快速构造一个子类对象。 - ④特质对该方法的定义必须出现
“super.方法名(参数)”
。 - ⑤方法的执行顺序遵循线性化计算公式,起点是公式里从左往右数的第一个特质,外部传入的参数也是由起点接收;起点的
“super.方法名(参数)”
将会调用起点右边第一个特质的同名方法,并把起点的计算结果作为参数传递过去;依此类推,最后结果会回到最左边的类本身。可以理解为特质是按一定顺序对入参进行各种变换,最后把变换后的入参交给类来计算。 - ⑥要回到类本身,说明这个类直接或间接重写或实现了基类的方法。并且定义中如果也出现了
“super.方法名(参数)”
,那么会调用它的上一层超类的实现版本。或者这个类没有重写,那就一定要有继承自超类的实现。
线性化计算公式:
- ①最左边是类本身。
- ②在类的右边写下定义时最后混入的那个特质,并接着往右按继承顺序写下该特质的所有超类和超特质。
- ③继续往右写下倒数第二个混入的特质,以及其超类和超特质,直到写完所有特质。
- ④所有重复项只保留最右边那个,并在最右边加上
AnyRef和Any
。
为了具体说明,以如下代码为例:
// test.scala
abstract class A {
def m(s: String): String
}
class X extends A {
def m(s: String) = "X -> " + s
}
trait B extends A {
abstract override def m(s: String) = super.m("B -> " + s)
}
trait C extends A {
abstract override def m(s: String) = super.m("C -> " + s)
}
trait D extends A {
abstract override def m(s: String) = super.m("D -> " + s)
}
trait E extends C {
abstract override def m(s: String) = super.m("E -> " + s)
}
trait F extends C {
abstract override def m(s: String) = super.m("F -> " + s)
}
class G extends X {
override def m(s: String) = "G -> " + s
}
val x = new G with D with E with F with B
println(x.m("End"))
- 首先,需要混入特质进行线性化计算的类G在定义时没有立即混入特质,即只有
“class G extends X”
,而是通过“new G with D with E with F with B”
来构造G的匿名子类的对象。 - 其次,注意基类A是一个抽象类,类X实现了抽象方法m,类G重写了X的m,其余特质也用
“abstract override”
重写了m,这保证了m最终会回到类G。 - 最后,基类A的m的返回类型“String”的声明是必须的,因为抽象方法无法推断返回类型,不声明就默认是Unit。
根据线性化计算公式可得(蓝色表示起点,红色表示重复,类X不参与计算):
- 起点是B,传入参数“End”会得到
“B -> End”
; - 然后B的
super.m
调用F的m,并传入计算得到的“B -> End”
,那么F会得到“F -> B -> End”
,再继续向右调用; - 最后A的m是抽象的,无操作可执行,转而回到G的m,所以最后传给G的参数实际是
“D -> C -> E -> F -> B -> End”
,得到的结果也就是“G -> D -> C -> E -> F -> B -> End”
。
通过实际运行可得:
PS E:\Microsoft VS\Scala> scala test.scala G -> D -> C -> E -> F -> B -> End
如果G的m也有super或没有重写,那么会调用X的m,最后的结果是最左边多个X:
// test.scala
...
class G extends X {
override def m(s: String) = super.m("G -> " + s)
}
...
PS E:\Microsoft VS\Scala> scala test.scala X -> G -> D -> C -> E -> F -> B -> End
如果立即混入特质,则相当于普通的方法重写:
// test.scala
...
class G extends X with D with E with F with B {
override def m(s: String) = "G -> " + s
}
val x = new G
...
PS E:\Microsoft VS\Scala> scala test.scala G -> End
如果上一层超类是抽象类,立即混入会引发错误:
// test.scala
...
class G extends A with D with E with F with B {
override def m(s: String) = "G -> " + s
}
val x = new G
...
PS E:\Microsoft VS\Scala> scala test.scala E:\Microsoft VS\Scala\Chapter 12.\traittest.scala:23: error: overriding method m in trait B of type (s: String)String; method m needs `abstract override’ modifiers override def m(s: String) = "G -> " + s ^ one error found