【Scala】Scala特质

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 特质 Scala里相当于Java接口的是Trait(特征)。实际上它比接口还功能强大。与接口不同的是,它还可以定义...
    JasonDing阅读 1,104评论 0 1
  • 版权申明:转载请注明出处。文章来源:http://bigdataer.net/?p=317 总体来说,scala中...
    bigdataer阅读 404评论 0 2
  • 面向对象编程之类 定义一个简单的类 // 定义类,包含field以及方法 // 创建类的对象,并调用其方法 get...
    义焃阅读 727评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 这几天中午吃完饭,各组老师都找间教室自行排练节目。因为学校要欢度国庆,举行文艺汇演。汇演中有学生的节目,也有...
    凡泛阅读 299评论 0 2