Trait和类型转换

1. Trait

Trait是指可以混入或融入一个类层次结构的行为。比如说,先对Friend建模,然后将其混入任何类:Men、Women、Dog等,而不用让它们都从一个公共的基类继承下来。

假定我们已经建模出Human,现在,想让它成为朋友。朋友是能够倾听你说话的人。所以,我们要给Human类增加一个listen方法,下面是:

class Human(val name: String) {
    def listen() = println("Your friend " + name + " is listenning")
}

class Man(override val name: String) extends Human(name)

class Woman(override val name: String) extends Human(name)

上面代码的一个不足之处在于,朋友这方面的特性不太突出,而且它被并到Human类里。另外,开发几周后,我们意识到我们忘记了人类最好的朋友——狗是伟大的朋友——当我们有太多无法释怀时,它们会安静地听我们叙述。但是,怎样才能让够成为我们的朋友呢?我们不能为此就让Dog从Human继承下来。Java解决这个问题的方式是创建一个接口Friend,让Human和Dog都实现这个接口。我们不得不在这两个类里提供不同实现,不管是不是真的不同。

这就是Scala的trait介入的地方了。Trait像一个拥有部分实现的接口。trait里定义和初始化的val和var会在混入trait的类的内部得到实现。定义过而未初始化的val和var则认为是抽象的,需要由混入这些trait的类实现。下面将Friend这个概念重新实现为trait:

trait Friend {
    val name: String

    def listen() = println("Your friend " + name + " is listening")
}

class Human(val name: String) extends Friend

class Man(override val name: String) extends Human(name) // 使用override是因为重写了基类的name字段

class Woman(override val name: String) extends Human(name)
}

这里,把Friend定义为trait。它有一个名叫name的val,被当作abstract对待。此外还有一个listen()方法。name实际的定义或实现由混入这个trait的类提供。

Human类混入了Friend trait。如果类并不继承其他任何类的话,那么可以使用extends关键字混入trait。Human类及其派生类Man和Woman简单的使用了trait提供的listen()方法的实现。如果需要的话,也可以改写这个实现。

混入trait的数量可以是任意的。用关键字with就可以混入更多的trait。如果类已经继承了另一个类,就像下面这个例子里的Dog,还可以用关键字with混入第一个trait。除了混入trait之外,下面在Dog里改写listen()方法。

class Animal

class Dog(val name: String) extends Animal with Friend {
    override def listen(): Unit = println(name + "'s listening quietly")
}

一个类被混入trait之后,通过它的实例可以调用到trait的方法,也可以把它的引用当做trait的应用。

    def helpAsFriend(friend: Friend) = friend listen()
    
    def main(args: Array[String]): Unit = {
        val john = new Man("John")
        val sara = new Woman("Sara")
        val comet = new Dog("Comet")

        john.listen() // Your friend John is listening
        sara.listen() // Your friend Sara is listening
        comet.listen() // Comet's listening quietly

        val mansBestFriend: Friend = comet
        mansBestFriend.listen() // Comet's listening quietly

        helpAsFriend(sara) // Your friend Sara is listening
        helpAsFriend(comet) // Comet's listening quietly
    }

trait看上去很像类,但是还有一些很大的差别。首先,它们需要混入类去实现那些已经声明的而未初始化的(即抽象的)变量和值。其次,它们的构造器不能有任何参数。trait会编译成Java的接口,还有对应的实现类,里面包含了trait实现的方法。

多重继承通常会带来方法冲突的问题,trait并不会为这个问题所扰。通过延迟绑定混入类的方法,它们有效的回避了这一点。如此一来,在trait里调用super可能解析成另一个trait的方法,也可能会解析成混入类的方法。

2.选择性混入

在上面的例子里,Friend trait混入到了Dog类里。这样就可以将Dog的任意实例当作Friend;也就是说,所有Dog都是Friend。
此外,还可以在实例一级对trait进行选择性混入,这样的话,就可以把特定的类的实例当作trait。看下面例子:

class Cat(val name:String) extends Animal

Cat并没有混入Friend trait,所以,不能把Cat的实例当作Friend。

        def useFriend(friend: Friend)=friend listen

        val alf = new Cat("Alf")
//        val friend:Friend = alf // error

//        useFriend(alf) // error

然而,Scala确实可以为爱猫人提供帮助,需要的话,我们可以专门把特殊的宠物当作Friend。创建实例时,只要简单的用with关键字标记一下即可:

        def useFriend(friend:Friend)=friend listen

        val snowy = new Cat("Snowy") with Friend
        val friend:Friend = snowy
        friend.listen // Your friend Snowy is listening

        useFriend(snowy) // Your friend Snowy is listening

Scala给予了我们极大的灵活性:把类的所有实例当作trait,或是只选择需要的实例当作trait。如果想把trait用于事先存在的类上,后者就显得有用了。

3. 以trait进行装饰

Trait可用于装饰对象,使器具备一些能力。假设我们要对申请者进行不同的检查——信贷、犯罪记录、雇佣记录等。我们并不总是对所有的检查项感兴趣。公寓申请人需要检查信贷和犯罪记录,而就业申请人则需要检查犯罪记录和之前的雇佣记录。如果依靠创建特定的类对这些组合进行检查的话,最终,会为所需要检查的各种排列组合都创建一个类。而且,如果决定进行额外的检查,就不得不改变处理这组检查的类。不,我们要避免这种类的激增。我们可以更具有成效一些,对每种情况,只混入特定的检查。

abstract class Check {
    def check(): String = "Checked Application Details..."
}

对不同类型的检查,比如信贷、犯罪记录和雇佣记录,我们都会创建向下面这样的trait:

trait CreditCheck extends Check {
    override def check(): String = "Checked Credit..." + super.check()
}

trait EmploymentCheck extends Check{
    override def check(): String = "Checked Employment..." + super.check()
}

trait CriminalRecordCheck extends Check {
    override def check(): String = "Check Criminal Records..." + super.check()
}

这些trait都继承自Check,因为我们只想把它们混入继承自Check的类。继承这个类给予了我们两个能力。首先,这些trait只能混入继承自Check的类。其次,在这些trait里可以使用Check的方法。

我们感兴趣的使增强或是修饰check()方法的实现,所以,需要将其标记为override。这里check()实现调用了super.check()。在trait里,通过super调用的方法会经历一个延迟的过程。
这个调用并不是对基类的调用,而是对其左边混入的trait的调用
——如果这个trait已经是混入的最左边trait,那么这个调用就会解析成混入这个trait的类的方法。
目前为止,在这个例子里有一个抽象类,三个trait,没有任何具体类——因为根本不需要。检查公寓申请时,可以用一个实例把上面的trait和类放在一起:

        val apartmentApplication = new Check with CreditCheck with CriminalRecordCheck

        // Check Criminal Records...Checked Credit...Checked Application Details...
        println(apartmentApplication check)

        val employmentApplication = new Check with  CriminalRecordCheck with EmploymentCheck

        // Checked Employment...Check Criminal Records...Checked Application Details...
        println(employmentApplication check)

最右的trait开始调用check()。然后,顺着super.check(),将调用传递到其左边的trait。最左边的trait调用的时真正实例的check()。

在Scala中,trait是一个强有力的工具,可以用它混入横切关注点。使用它们可以较低的成本创建出高度可扩展的代码。无需创建一个拥有大量类和接口的层次结构,就可以快速地把必要的代码投入使用。

4. Trait方法的延迟绑定

上面的例子里,Check类的check()方法是具体的,trait都是从这个类继承的。我们见识到了,在trait里对super.check()的调用时如何绑定到其左边的trait或是其混入的类的。但如果基类的方法时抽象的,就会变得有点复杂。下面进一步探索一番。
先写一个抽象类Writer,它有一个抽象方法writeMessage():

abstract class Writer {
    def writeMessage(message: String)
}

任何继承这个类的类都要实现writeMessage()方法。如果有一个trait继承了这个抽象类,并且用super调用了这个抽象方法,Scala会要求将方法声明为abstract override。将这两个关键字组合到一起看上去有些奇怪。关键字override告诉Scala,要为基类的一个已知方法提供一个实现。同时,还表示,这个方法实际最后的"终极"实现由混入这个trait的类提供。下面是一个例子,这个trait继承了上面的那个类:

trait UpperCaseWriter extends Writer {
    abstract override def writeMessage(message: String) = super.writeMessage(message.toUpperCase())
}

trait ProfanityFileredWriter extends Writer {
    abstract override def writeMessage(message: String): Unit = super.writeMessage(message.replace("stupid", "s-----"))
}

在这段代码里,为了调用super.writeMessage,Scala做了两件事。首先,它对这个调用进行了延迟绑定。其次,它会要求混入这些trait的类提供该方法的实现。ProfanityFilteredWriter只负责处理有些粗鲁的单词——且仅当它以小写形式出现。这是为了体现混入的顺序。

现在来用一下这些trait。先来写个类StringWriterDelegate,继承自抽象类Writer,将写消息的操作委托给一个StringWriter实例:

class StringWriterDelegate extends Writer {
    val writer = new StringWriter

    override def writeMessage(message: String): Unit = writer.write(message)

    override def toString(): String = writer.toString
}

在上面StringWriterDelegate的定义里可以混入一个或多个trait,不过,在这里,我们选择的事在创建这个类的实例时混入trait

        val myWriterProfanityFirst = new StringWriterDelegate with UpperCaseWriter with ProfanityFileredWriter

        val myWriterProfanityLast = new StringWriterDelegate with ProfanityFileredWriter with UpperCaseWriter

        myWriterProfanityFirst writeMessage "There is no sin except stupidity"
        myWriterProfanityLast writeMessage "There is no sin except stupidity"

        println(myWriterProfanityFirst) // THERE IS NO SIN EXCEPT S-----ITY
        println(myWriterProfanityLast) // THERE IS NO SIN EXCEPT STUPIDITY

在第一个语句里,ProfanityFilteredWriter是最右的trait,所以,它会先起作用。然而,在第二个语句中,它会后起作用。

5. 隐式类型转换

假设我们要创建一个应用,其中包含了几种日期和时间的操作。如果代码可以写成下面这样,就会相当方便,更加可读:

2 days ago
5 day from_now

上面的代码看起来不像代码,更像是数据输入——这是DSL的特征之一。可选的点和括号在这里起到了作用。在第一个语句里,我们调用了2的days()方法,传入一个变量ago。在第二个语句里,调用了5的方法,传的变量是from_now。

如果编译上面的代码,Scala会提示days()不是Int的方法。是的,Int没有提供这个方法,但是这并不能阻止我们写出这样的代码。就让Scala安静地把Int转换成什么东西,帮我们完成上面这个操作——进入隐式类型转换的世界吧!

隐式类型转换可以帮助我们扩展语言,创建“专用于特定应用和领域”的词汇或语法,也可以帮助我们创建属于自己的领域专用语言。

为了先理解这些概念,我们从一个恶心的代码开始,然后把它重构成一个漂亮的类。

我们需要定义变量ago和from_now,让Scala接收days()方法。定义变量很简单,接收方法却不容易。我们创建一个类DateHelper,其构造函数可以以一个Int为参数:

import java.util._

class DateHelper(number: Int) {
    def days(when: String): Date = {
        var date = Calendar.getInstance()
        when match {
            case "ago" => date.add(Calendar.DAY_OF_MONTH, -number)
            case "from_now" => date.add(Calendar.DAY_OF_MONTH, number)
            case _ => date
        }
        date.getTime
    }
}

DateHelper类提供我们想要的days()方法。现在,我们所需要做的就是把Int转化成DateHelper。可以用一个方法来做这件事,接收一个Int,返回一个DateHelper的实例。简单的把方法标记为implicit,只要它在当前范围内存在(通过当前import可见,或是位于当前文件),Scala就会自动调用它。
代码如下:

    def main(args: Array[String]): Unit = {

        implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)

        val ago = "ago"
        val from_now = "from_now"

        val past = 2 days ago
        val appointment = 5 days from_now

        println(past)
        println(appointment)
    }

如果把上面的代码同DateHelper的定义一起运行,Scala就会自动把给定的数字转换为一个DateHelper实例,然后,调用days()方法。

现在,代码已经可以工作了,是时候稍作清理了。我们并不想在每次需要转换时都去写隐式转换器。把这个转换器放到一个单独的单独的单例对象里,可以获得更好的重用性,也更加易用。可以把转换器挪到DateHelper的伴生对象用。

import java.util._

class DateHelper(number: Int) {
    def days(when: String): Date = {
        var date = Calendar.getInstance()
        when match {
            case "ago" => date.add(Calendar.DAY_OF_MONTH, -number)
            case "from_now" => date.add(Calendar.DAY_OF_MONTH, number)
            case _ => date
        }
        date.getTime
    }

}

object DateHelper {
    val ago = "ago"
    val from_now = "from_now"

    implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)

}

导入DateHelper时,Scala会自动的找到转换器。这是因为Scala会在当前范围和导入的范围内进行转换。

下面是一个例子,用到了在DateHelper里写的隐式转换。

        import DateHelper._
        val past = 2 days ago
        val appointment = 5 days from_now

        println(past)
        println(appointment)

在Predef对象里,Scala已经定义了一些隐式转换,Scala会默认导入它们。这样的话,比如说,当我们写 1 to 3时,Scala就会隐式的将1从Int转换为其富封装器RichInt,然后,调用to()方法。

Scala一次至多应用一个隐式转换。当前范围内,如果发现通过类型转换有助于操作、方法调用或类型转换的成功完成,就会进行转换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值