美文网首页
【Scala】Scala特质

【Scala】Scala特质

作者: w1992wishes | 来源:发表于2019-02-25 15:33 被阅读0次

    PS:本篇主要内容来自 《scala 编程》一书。

    一、特质的基本概念

    在 Scala 中 Trait 为重用代码的一个基本单位。Trait 中封装了方法和字段,并可以混入到类中复用。

    和 java 中的抽象类类似,Trait 中的方法可以有实现也可以没有实现,且同样只支持单一继承,但通过 with 混入类中,一个类可以混入任意多个 Trait,这和 java 中的接口类似。

    由此可以看出 Trait 集合了 Interface 和抽象类的优点,同时又没有破坏单一继承的原则。

    二、特质如何工作

    定义一个 Trait 的方法和定义一个类的方法非常类似,除了它使用 Trait 而非 class 关键字 。

    trait Philosophical{
      def philosophize() {
        println("I consume memeory, therefor I am!")
      }
    }
    

    这个 Trait 名为 Philosophical 。它没有声明基类,因此和类一样,有个缺省的基类 AnyRef。它定义了一个方法,叫做 philosophize。

    一但定义好 Trait,它就可以用来和一个类混合,这可以使用 extends 或 with 来混合一个 trait。

    class Frog extends Philosophical{
      override def toString="gree"
    }
    

    使用 extends 为 Frog 添加 Philosophical Trait 属性,因此 Frog 缺省继承自 Philosophical 的父类 AnyRef,这样 Frog 类也具有了 Philosophical 的性质。

    scala> val frog = new Frog
    frog: Frog = green
    
    scala> frog.philosophize
    I consume memeory, therefor I am!
    

    可以看到 Frog 添加了 Philosophical(哲学性)也具有了哲学家的特性,可以说出类似“我思故我在”的话语了。

    和 Interface 一样,Trait 也定义一个类型

    scala> val phil:Philosophical = frog
    phil: Philosophical = green
    
    scala> phil.philosophize
    I consume memeory, therefor I am!
    

    如果你需要把某个 Trait 添加到一个有基类的子类中,使用 extends 继承基类,而可以通过 with 添加 Trait。

    class Animal
    class Frog extends Animal with Philosophical{
      override def toString="green"
    }
    

    还和 Interface 类似,可以为某个类添加多个 Trait 属性,此时使用多个 with 即可

    class Animal
    trait HasLegs 
    class Frog extends Animal with Philosophical with HasLegs{
      override def toString="green"
    }
    

    上述例子中,类 Frog 都继承了 Philosophical 的 philosophize 实现。此外 Frog 也可以重载 philosophize 方法。语法与重载基类中定义的方法一样。

    class Animal
    trait HasLegs 
    class Frog extends Animal with Philosophical with HasLegs{
      override def toString="green"
      def philosophize() {
        println("It ain't easy being " + toString + "!")
      }
    }
    

    因为 Frog 的这个新定义仍然混入了特质 Philosophize,所以仍然可以把它当作这种类型的变量使用。但是由于 Frog 重载了 Philosophical 的 philosophize 实现,当调用它的时候,会得到新的回应:

    scala> val phrog:Philosophical = new Frog
    phrog: Philosophical = green
    
    scala> phrog.philosophize
    It ain't easy being green!
    

    这时或许推导出以下结论:Trait 就像是带有具体方法的 Java 接口,不过其实它能做的更多。Trait 可以,比方说,声明字段和维持状态值。实际上,可以用 Trait 定义做任何用类定义做的事,并且语法也是一样的,除了两点。

    第一点,Trait 不能有任何“类”参数,也就是说,传递给类的主构造器的参数。换句话说,尽管你可以定义如下的类:

    class Point(x: Int, y: Int
    

    但下面的 Trait 定义直接报错:

    scala> trait NoPoint(x:Int,y:Int)
    <console>:1: error: traits or objects may not have parameters
           trait NoPoint(x:Int,y:Int)
    

    第二点,不论在类的哪个角落,super 调用都是静态绑定的,而在特质中,它们是动态绑定的。具体可以参考第四节。

    三、瘦接口还是胖接口

    3.1、瘦接口变成胖接口

    Trait 的一种主要应用方式是可以根据类已有的方法自动为类添加方法。也就是说,Trait 可以使得一个瘦接口变得丰满些,把它变成胖接口。

    选择瘦接口还是胖接口的体现了面向对象设计中常会面临的在实现者与接口用户之间的权衡。胖接口有更多的方法,对于调用者来说更便捷。客户可以捡一个完全符合他们功能需要的方法。另一方面瘦接口有较少的方法,对于实现者来说更简单。然而调用瘦接口的客户因此要写更多的代码。由于没有更多可选的方法调用,他们或许不得不选一个不太完美匹配他们所需的方法并为了使用它写一些额外的代码。

    Java 的接口常常是过瘦而非过胖。例如,从 Java 1.4 开始引入的 CharSequence 接口,是对于字串类型的类来说通用的瘦接口,它持有一个字符序列。下面是把它看作 Scala 中 Trait 的定义:

    trait CharSequence { 
      def charAt(index: Int): Char 
      def length: Int 
      def subSequence(start: Int, end: Int): CharSequence 
      def toString(): String 
    }
    

    尽管类 String 成打的方法中的大多数都可以用在任何 CharSequence 上,Java 的 CharSequence 接口定义仅提供了 4 个方法。如果 CharSequence 代以包含全部 String 接口,那它将为 CharSequence 的实现者压上沉重的负担。任何实现 Java 里的 CharSequence 接口的程序员将不得不定义一大堆方法。因为 Scala 的 Trait 可以包含具体方法,这使得创建胖接口大为便捷。

    在 Trait 中添加具体方法使得胖瘦对阵的权衡大大倾向于胖接口。不像在 Java 里那样,在 Scala 中添加具体方法是一次性的劳动。只要在 Trait 中实现方法一次,而不再需要在每个混入 Trait 的方法中重新实现它。因此,与没有 Trait 的语言相比,Scala 里的胖接口没什么工作要做。

    要使用 Trait 加强接口,只要简单地定义一个具有少量抽象方法的 Trait——Trait 接口的瘦部分——和潜在的大量具体方法,所有的都实现在抽象方法之上。然后就可以把丰满了的 Trait 混入到类中,实现接口的瘦部分,并最终获得具有全部胖接口内容的类。

    3.2 、Trait 示例–Rectangular 对象

    在设计绘图程序库时常常需要定义一些具有矩形形状的类型:比如窗口,bitmap 图像,矩形选取框等。为了方便使用这些矩形对象,函数库对象类提供了查询对象宽度和长度的方法(比如 width,height)和坐标的 left,right,top 和 bottom 等方法。然而在实现这些函数库的这样方法,如果使用 Java 来实现,需要重复大量代码,工作量比较大(这些类之间不一定可以定义继承关系)。但如果使用 Scala 来实现这个图形库,那么可以使用 Trait,为这些类方便的添加和矩形相关的方法。

    首先如果使用不使用 Trait ,需要定义一些基本的几何图形类如 Point 和Rectangle:

    class Point(val x:Int, val y:Int)
    
    class Rectangle(val topLeft:Point, val bottomRight:Point){
      def left =topLeft.x
      def right =bottomRight.x
      def width=right-left 
      // and many more geometric methods
    }
    

    Rectangle 类的主构造函数使用左上角和右下角坐标,然后定义了 left,right,和 width 一些常用的矩形相关的方法。

    可能还定义了一下 UI 组件(它并不是使用 Retangle 作为基类),其可能的定义如下:

    abstract class Component {
      def topLeft :Point
      def bottomRight:Point
      def left =topLeft.x
      def right =bottomRight.x
      def width=right-left
      // and many more geometric methods
    }
    

    可以看到 left,right,width 定义和 Rectangle 的定义重复。可能函数库还会定义其它一些类,也可能重复这些定义。使用 Trait,就可以消除这些重复代码。

    trait Rectangular {
      def topLeft:Point
      def bottomRight:Point
      def left =topLeft.x
      def right =bottomRight.x
      def width=right-left
      // and many more geometric methods
    }
    

    然后修改 Component 类定义使其“融入”Rectangular 特性:

    abstract class Component extends Rectangular{
     //other methods
    }
    

    同样也修改 Rectangle 定义:

    class Rectangle(val topLeft:Point, val bottomRight:Point) extends Rectangular{
      // other methods
    }
    

    这样就把矩形相关的一些属性和方法抽象出来,定义在 Trait 中,凡是“混合”了这个 Rectangluar 特性的类自动包含了这些方法。

    四、Trait 用来实现可叠加的修改操作

    我们已经看到 Trait 的一个主要用法,将一个瘦接口变成胖接口,本篇我们介绍 Trait 的另外一个重要用法,为类添加一些可以叠加的修改操作。Trait 能够修改类的方法,并且能够通过叠加这些操作(不同组合)修改类的方法。

    我们来看这样一个例子,修改一个整数队列,这个队列有两个方法:put 为队列添加一个元素,get 从队列读取一个元素。队列是先进先出,因此 get 读取的顺序和 put 的顺序是一致的。

    对于上面的队列,我们定义如下三个 Trait 类型:

    • Doubling : 队列中所有元素 * 2。
    • Incrementing: 队列中所有元素递增。
    • Filtering: 过滤到队列中所有负数。

    这三个 Trait 代表了修改操作,因为它们可以用来修改队列类对象,而不定义全新的队列。

    这三个操作是可以叠加的,也就是说,可以通过这三个基本操作的任意不同组合和原始的队列类“混合”,从而可以得到所需要的新的队列类的修改操作。

    为了实现这个整数队列,可以定义这个整数队列的一个基本实现如下:

    import scala.collection.mutable.ArrayBuffer
    abstract class IntQueue {
      def get():Int
      def put(x:Int)
    }
    class BasicIntQueue extends IntQueue{
      private val buf =new ArrayBuffer[Int]
      def get()= buf.remove(0)
      def put(x:Int) { buf += x }
    }
    

    下面我们可以使用这个实现,来完成队列的一些基本操作:

    scala> val queue = new BasicIntQueue
    queue: BasicIntQueue = BasicIntQueue@60d134d3
    scala> queue.put (10)
    scala> queue.put(20)
    scala> queue.get()
    res2: Int = 10
    scala> queue.get()
    res3: Int = 20
    

    这个实现完成了对象的基本操作,看起来了还可以,但如果此时有新的需求,希望在添加元素时,添加元素的双倍,并且过滤掉负数,可以直接修改 put 方法 来完成,但之后需求又变了,添加元素时,添加的为参数的递增值,也可以修改 put 方法,这样显得队列的实现不够灵活。

    我们来看看如果使用 Trait 会有什么结果,我们实现 Doubling,Incrementing,Filtering 如下:

    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)}
    }
    
    trait Filtering extends IntQueue{
      abstract override def put (x:Int){
        if(x>=0) super.put(x)
      }
    }
    

    我们可以看到所有的 Trait 实现都已 IntQueue 为基类,这保证这些 Trait 只能和同样继承了 IntQueue 的类“混合”,比如和 BasicIntQueue 混合。

    此外 Trait 的 put 方法中使用了 super,通常情况下对于普通的类这种调用是不合法的,但对于 trait来说,这种方法是可行的,这是因为 trait 中的 super 调用是动态绑定的,只要和这个 Trait 混合在其他类或 Trait 之后,而这个其它类或 Trait 定义了 super 调用的方法即可。这种方法是实现可以叠加的修改操作是必须的,并且注意使用 abstract override 修饰符,这种使用方法仅限于 Trait 而不能用作 Class 的定义上。

    有了这三个 Trait 的定义,我们可以非常灵活的组合这些 Trait 来修改 BasicIntQueue 的操作。

    首先我们使用 Doubling Trait:

    scala> val queue = new BasicIntQueue with Doubling
    queue: BasicIntQueue with Doubling = $anon$1@3b004676
    scala> queue.put(10)
    scala> queue.get()
    res1: Int = 20
    

    这里通过 BasicIntQueue 和 Doubling 混合,我们构成了一个新的队列类型,每次添加的都是参数的倍增。

    我们在使用 BasicIntQueue 同时和 Doubling 和 Increment 混合,注意我们构造两个不同的整数队列,不同是 Doubling 和 Increment 的混合的顺序。

    scala> val queue1 = new BasicIntQueue with Doubling with Incrementing
    queue1: BasicIntQueue with Doubling with Incrementing = $anon$1@35849932
    scala> val queue2 = new BasicIntQueue with Incrementing  with Doubling
    queue2: BasicIntQueue with Incrementing with Doubling = $anon$1@4a4cdea2
    scala> queue1.put(10)
    scala> queue1.get()
    res4: Int = 22
    scala> queue2.put(10)
    scala> queue2.get()
    res6: Int = 21
    

    相关文章

      网友评论

          本文标题:【Scala】Scala特质

          本文链接:https://www.haomeiwen.com/subject/ipdgyqtx.html