返回介绍

数学基础

统计学习

深度学习

工具

Scala

五、Trait

发布于 2023-07-17 23:38:23 字数 17361 浏览 0 评论 0 收藏 0

  1. ScalaJava 中,不支持多重继承,任何一个类都继承自单个父类。C++ 支持多重继承,一个类可以继承自多个父类。

    为了实现类似多重继承的效果,Java 采用接口interface 来实现,而Scala 采用特质trait 来实现。

    traitScala 代码复用的基础单元:trait 将方法、字段封装起来,然后通过将它们混入mix in 类的方式来实现复用。

    特质很像Java 接口,但是特质比接口更强大:它不仅可以定义方法,还可以定义字段来保持状态。

  2. 通过trait 关键字来定义特质,其定义除了关键字不一样,其它都和类的定义相同。

    
    
    xxxxxxxxxx
    trait T { def f = println("This is a trait") }

    在特质定义中你可以做任何在类定义中做的事,语法也完全相同。除了以下两种情况:

    • 特质不能有任何“类”参数,即:那些传入类的主构造方法的参数。

      你可以这样定义一个类:class C:(x:Int,y:Int),但是无法这样定义一个特质:trait C:(x:Int,y:Int)

    • 类中的 super 调用是静态绑定的,而在特质中 super 是动态绑定的。

      • 在类中编写 super.toString 这样的代码时,你可以明确知道实际调用的是哪个实现。

      • 在特质中编写 super.toString 这种代码时,你无法知道实际调用的是哪个实现,因为定义特质时super 并没有被定义。

        具体哪个实现被调用,在每次该特质混入到某个具体类时,都会重新判定。

        这种super 的奇怪行为是特征能够实现可叠加修改stackable modification 的关键。

  3. 特质也有继承关系,如果未明确给出超类,则它默认继承自AnyRef

  4. 一旦定义好特质,就可以通过extends 或者 with 关键字将其混入mix in到类中。

    一般是混入特质,而不是继承特质。因为混入和继承有很多重要区别。

  5. 可以通过extends 来混入特质,这种情况下,类会隐式的继承了特质的超类。从特质继承的方法跟从超类继承的方法用起来一样。

    
    
    xxxxxxxxxx
    class Parent{ def f1 = println("This is Parent class") } ​ trait T extends Parent{ def f2 = println("This is a trait") } ​ class Child extends T{ // Child 隐式继承了 Parent,Child 还可以使用 T 的方法 def f3 = println("This is Child class") } ​ val c = new Child() c.f1 // 打印:This is Parent class c.f2 // 打印:This is a trait c.f3 // 打印:This is Child class

    同时特质也定义了一个类型,但是有几个约束:

    • 不能直接new 一个特质。
    • 只能使用特质中定义、以及特质从它的超类中继承而来的字段和方法。此时也能够支持多态和动态绑定。
    
    
    xxxxxxxxxx
    val c: T = new Child() // c 的类型是 T c.f3 // 编译失败,因为 T 没有 f3 成员 c.f2 // 打印:This is a trait c.f1 // 打印:This is Parent class val t = new T() // 编译失败,无法直接 new 一个特质
  6. 如果类已经通过extends 继承自某个超类,如果还希望混入特质,则通过with 关键字。

    
    
    xxxxxxxxxx
    class Parent{ def f1 = println("This is Parent class") } trait T { def f2 = println("This is a trait") } ​ class Child extends Parent with T { // Child 显式继承了 Parent,并混入了 T def f3 = println("This is Child class") } ​ val c = new Child() c.f1 // 打印:This is Parent class c.f2 // 打印:This is a trait c.f3 // 打印:This is Child class
  7. 如果希望混入多个特质,则可以通过多个 with 子句添加。

    • 如果显式继承自父类,则父类采用extends,每个特质一个 with 子句。
    • 如果没有显式继承,则第一个特质采用extends,后面每个特质一个 with 子句。
    
    
    xxxxxxxxxx
    class Parent{ def f1 = println("This is Parent class") } trait T1 { def f2 = println("This is a trait1") } trait T2 { def f3 = println("This is a trait2") } ​ class Child extends Parent with T1 with T2 { // Child 显式继承了 Parent,并混入了 T1,T2 def f4 = println("This is Child class") }
  8. 类可以重写特质的成员,语法和重写超类中的成员一样。

  9. 特质的一个主要用途是:自动给类添加基于已有方法的新方法。即:特质可以丰富一个瘦接口,使其成为一个富接口。

    瘦接口和富接口代表了面向对象设计中经常面临的取舍,在接口实现方和接口使用方之间的权衡。

    • 富接口有很多方法,对调用方很方便,但是实现方需要做更多的工作。
    • 瘦接口方法较少,因此实现起来更容易,但是需要调用方编写更多的代码。

    Java 通常采用瘦接口,而Scala 倾向于采用富接口。因为Scala 可以通过给特质添加具体方法的形式,而这种投入是一次性的。你只需要在特质中实现这些方法一次,然后混入类中。你不需要在每个混入该特质的类中重新实现一遍。

    因此和没有特质的语言相比,Scala 中实现富接口的代价更小。

  10. 要用特质来实现富接口,只需要定义一个拥有为数不多的抽象方法(接口中的瘦的部分)和可能数量很多的具体方法(这些具体方法基于那些抽象方法编写)的特质。然后你可以将这个特质混入到某个类,在类中实现接口中瘦的部分,最终得到一个拥有完整富接口实现的类。

    
    
    xxxxxxxxxx
    abstract trait T{ /***** 瘦接口部分:抽象 *****/ def interface1(n:Int):Int def interface2(name:String):String /***** 富接口部分:具体 *****/ def f1(m:Int) = {/* 调用 interface1 和 interface2 的具体实现 */} def f2(m:Int,n:String) = {/* 调用 interface1 和 interface2 的具体实现 */} } ​ class C extends T{ // C 拥有了富接口 /****** 实现瘦接口 ******/ override def interface1(n:Int) = {/* 具体实现 */} override def interface2(name:String) = {/* 具体实现 */} }
  11. 当需要实现某个可复用的行为集合时,都需要决定是用特质还是抽象类。有一些参考意见:

    • 如果某个行为不会被复用,则用具体的类。

    • 如果某个行为可能被用于多个互不相关的类,则用特质。

    • 如果想要从Java 代码中继承某个行为,用抽象类。

      Java 类继承和从Scala 类继承几乎一样,唯有一个例外:如果某个Scala 只有抽象方法,则它会被翻译成Java 的接口。

    • 如果计划将某个行为以编译好的形式分发,且预期会有外部的组织编写继承自它的类,则倾向于使用抽象类。因为当某个特质增加或者减少成员时,任何继承自该特质的类都需要被重新编译,哪怕这些类并没有任何改变。

      如果外部的使用方只是调用到这个行为,而没有继承,则特质也是可以的。

    • 如果没有任何线索,则推荐从特质开始。因为特质能让你保留更多选择。

  12. 许多有经验的Scala 程序员都在实现的初期采用特质。每个特质可以描述整个概念的一小块。随着设计逐步固化和稳定,这些小块可以通过特质混入,被组合成更完整的概念。

5.1 Ordered Trait

  1. 特质的一个应用场景是比较大小。通常为了支持大小比较操作,我们会定义一些方法:

    
    
    xxxxxxxxxx
    class C { def < (that:C) = {/* 小于比较 */ } def > (that:C) = that < this def <= (that:C) = (this < that) || (this == that) def >= (that:C) = (this > that) || (this == that) }

    实际上这个场景太普遍,因此Scala 提供了专用的特质来解决,这个特质叫Ordered

  2. Ordered 特质定义了 <,>,<=,>= ,你需要实现compare 方法,而这些方法将基于compare 方法来实现。

    因此Ordered 特质允许你通过只实现一个 compare 方法来增强某个类,从而使其具有完整的比较操作。

    
    
    xxxxxxxxxx
    class C(n:Int) extends Ordered[C]{ val num = n def compare(that:C) = this.num - that.num }
    • Ordered 特质要求混入时提供一个类型参数,如这里的Ordered[C] ,其中C 是你要比较元素的类。

    • compare 方法用于比较调用者(这里是this),和传入对象(这里是that)。

      • 如果二者相等,则返回 0 。
      • 如果调用者较小,则返回负值。
      • 如果调用者较大,则返回正值。
  3. 注意:Ordered 特质并不会帮你定义equals 方法,因为它做不到。因为当你用compare 来实现equals 时,需要检查传入对象的类型。而由于Java 的类型擦除机制,Ordered 特质自己无法完成这个检查。

    因此你需要自定义equals 方法,哪怕你已经继承了 Ordered

5.2 可叠加的修改

  1. 特质除了用于将瘦接口转化成富接口之外,还可以用于为类提供可叠加的修改。

  2. 一个典型的例子:对整数队列进行修改:

    • 可以将所有放入队列的整数翻倍。
    • 可以将所有放入队列的整数加一。
    • 可以将队列中的负数去掉。
    
    
    xxxxxxxxxx
    import scala.collection.mutable.ArrayBuffer ​ abstract class MyQueue{ def get(): Int def put(x:Int) } class BasicQueue extends MyQueue{ private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x:Int) = {buf += x} } ​ val queue = new BasicQueue queue.put(10) println(queue.get()) // 打印结果:10 ​ ​ trait DoubleOp extends MyQueue{ abstract override def put(x:Int) = {super.put(2*x)} } trait IncOp extends MyQueue{ abstract override def put(x:Int) = {super.put(x+1)} } ​ class DoubleBasicQueue extends BasicQueue with DoubleOp ​ val queue2 = new DoubleBasicQueue queue2.put(10) println(queue2.get()) // 打印结果:20

    这里有两个要点:

    • DoubleOp 声明了一个超类MyQueue,因此该特质只能够被混入那些同样继承自MyQueue 的类。

    • 该特质在一个声明为抽象的方法里做了一个super 的调用。

      • 对于普通的类而言,这样的调用是非法的,因为它们在运行时必定会失败。因为父类的 put 方法是抽象的,并未实现。
      • 对于特质来说,这样的调用实际上可以成功。因为特质中的super 是动态绑定的,只要在给出了该方法具体定义的特质或类之后混入,DoubleOp 特质里的super 调用就可以正常工作。

      为了告诉编译器你是特意如此,必须将这样的方法标记为 abstract override 。这样的修饰符组合只允许用在特质的成员上,不允许用在类的成员上。其含义是:该特质必须混入某个拥有该方法具体定义的类中。

  3. 如果类仅仅是继承然后混入特质,而并没有任何新的代码,这时候可以直接new而不必定义一个有名字的类。

    
    
    xxxxxxxxxx
    class DoubleBasicQueue extends BasicQueue with DoubleOp val queue2 = new DoubleBasicQueue

    可以简化为:

    
    
    xxxxxxxxxx
    val queue2 = new BasicQueue with DoubleOp
  4. 这里的特质主要用作是代表某种修改modification,因为它们修改了底层类的状态,而不是定义并修改自己的状态。

    • 这些特质是可叠加的stackable
    • 可以从这些特质中任意选择,将它们混入类。
  5. 混入特质的顺序是重要的。大致上讲,越靠右出现的特质越先起作用。

    当你调用某个带有混入的类的方法时,最靠右端的特质中的方法最先被调用。如果该方法调用super,则它将调用左侧紧挨着它的那个特质的方法,以此类推。

5.3 特质和多重继承

  1. 特质是一种从多个像类一样结构继承的方式,但是它和C++ 中的多重继承有重大区别。其中一个尤为重要的区别是:对super 的解读。

    • 在多重继承中,super 调用的方法在调用的地方就确定了。
    • 在特质中,super 调用的方法取决于类和混入类的特质的线性化linearization
  2. 以一个简单例子来说明:

    
    
    xxxxxxxxxx
    import scala.collection.mutable.ArrayBuffer ​ abstract class MyQueue{ def get(): Int def put(x:Int) } class BasicQueue extends MyQueue{ private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x:Int) = {buf += x} } ​ trait DoubleOp extends MyQueue{ abstract override def put(x:Int) = {super.put(2*x)} } trait IncOp extends MyQueue{ abstract override def put(x:Int) = {super.put(x+1)} }

    对于代码:

    
    
    xxxxxxxxxx
    val q = new BasicQueue with DoubleOp with IncOp q.put(1)
    • 如果是多重继承,则需要考虑q.put 究竟调用的是哪一个put 方法。

      如果规则是最后一个特质胜出,则DoubleOp 中的put 会被执行;如果规则是第一个特质胜出,则IncOp 中的put 会被执行。

      因此,这种方式无法实线可叠加的修改。

    • Scala 的规则是:线性化。

      当用new 实例化一个类的时候,Scala 会将类及其所有继承的类和特质都拿出来,将其线性的排列在一起。

      当你在某个类中调用 super 的时,被调用的方法是这个链条中向上最近的那个。如果除了最后一个方法外,所有的方法都调用了 super,则最终的结果就是叠加在一起的行为。

  3. 在任何线性化中,类总是位于所有它的超类和混入的特质之前。因此当你写下调用super 的方法时,该方法绝对是在修改超类和混入特质的行为。

  4. Scala 线性化的主要性质可以通过下面的例子说明:

    
    
    xxxxxxxxxx
    class Animal { def f = println("This is Animal") } ​ trait Furry extends Animal { override abstract def f = { println("This is Furry") super.f } } trait HasLegs extends Animal { override abstract def f = { println("This is HasLegs") super.f } } trait FourLegged extends HasLegs { override abstract def f = { println("This is FourLegged") super.f } } class Cat extends Animal with Furry with FourLegged { override def f = { println("This is Cat") super.f } }

    其继承体系为:

    注意:

    • 即使Animalf 是具体的,trait 中的f 也可以是抽象的。
    • 具体的类Cat 中,f 必须是具体的,也就是不能有abstract

    执行代码:

    
    
    xxxxxxxxxx
    var cat = new Cat cat.f /* 输出为: This is Cat This is FourLegged This is HasLegs This is Furry This is Animal /*

    Cat (class Cat extends Animal with Furry with FourLegged) 的线性化从后到前计算过程如下:

    • 最后一部分是超类Animal 的线性化。这段线性化被直接复制过来而不加修改。

      由于Animal 并不显式扩展自某个超类,也没有混入任何特质,因此其线性化看起来是这样的:Animal -> AnyRef -> Any

    • 线性化倒数第二部分是首个混入特质 Furry 的线性化。因为所有已经出现在Animal 线性化中的类都不再重复出现,每个类在Cat 的线性化当中只出现一次。因此结果为:

      Furry ->Animal -> AnyRef -> Any

    • 接下来是 FourLegged 的线性化。同样的,任何之前出现的类都不再重复出现,因此结果为:

      FourLegged -> HasLegs -> Furry ->Animal -> AnyRef -> Any

    • 最后,Cat 线性化中的第一个类是它自己,最终Cat 的线性化结果为:

      Cat -> FourLegged -> HasLegs -> Furry ->Animal -> AnyRef -> Any

    当这些类和特质中的任何一个通过supper 调用某个方法时,被调用的是在线性化链条中出现在其右侧的首个实现。

    如果定义为:

    
    
    xxxxxxxxxx
    class Cat extends Animal with Furry with FourLegged { override def f = { println("This is Cat") } }

    因为Cat 中没有 super 调用,则并不会执行线性化链条搜索。

    
    
    xxxxxxxxxx
    var cat = new Cat cat.f /* 输出为: This is Cat */

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文