本文由 Rhyme 發表在 ScalaCool 團隊博客。前端
Traits特質,一個咱們既熟悉又陌生的特性。熟悉是由於你會發現它和你平時在Java中使用的interface接口有着很大的類似之處,而陌生又是由於Traits的新玩法會讓你打破對原有接口的認知,進入一個更具備挑戰性,玩法更高級的領域。因此,在一開始,咱們能夠對Traits
有一個初步的認識:它是一個增強版的interface
。以後,隨着你對它瞭解的深刻,你就會發現相比Java接口,Traits跟類更爲類似。再以後,你或許會覺察到,Traits在嘗試着將抽象更好地融爲一個總體。編程
在Java中爲了不多重繼承所帶來的昂貴代價(方法或字段衝突、菱形繼承等問題),Java的設計者們使用了interface接口。而爲了解決Java接口沒法進行stackable modifications
(即沒法使用對象狀態進行迭代)、沒法提供字段等侷限,在Scala中,咱們使用Traits
特質而非接口。ide
trait Animal {
val typeOf: String = "哺乳動物" // 帶有默認值的字段
def move(): Unit = { // 帶有默認實現的方法
println("walk")
}
def eat() //未實現的抽象方法
}
複製代碼
以上代碼相似於如下的Java代碼學習
public interface Animal {
String typeOf = "哺乳動物";
default void move() {
System.out.println("walk");
}
void eat();
}
複製代碼
在Scala中使用關鍵字trait
而不interface
,和Java接口同樣,trait
也能夠有默認方法的實現。也就是說Java接口有的,trait
基本上也都有,並且實現起來要優雅許多。 之因此要說相似於以上的Java代碼,緣由在於trait
擁有的是字段typeOf
,而interface
擁有的是靜態屬性typeOf
。這是interface
和trait
的一點區別。可是再仔細觀察思考這一點區別,更好更靈活的字段設計,是否使得trait
更好地組織了抽象,使得它們成爲了一個更好的總體。spa
和Java同樣,Scala只支持單繼承,但卻能夠有任意數量的特質。在Scala中,咱們不稱接口被implements
實現了,而是traits
被mix in混入了類中。scala
class Bird extends Animal {
override val typeOf: String = "蛋生動物"
override def eat(): Unit = {
println("eat bugs")
}
override def move(): Unit = {
println("fly")
}
}
複製代碼
以上代碼中,Bird
類混入了特質Animal
。當類混入了多個特質時,須要使用with
關鍵字設計
trait Egg
class Bird extends Animal with Egg{
override val typeOf: String = "蛋生動物"
override def eat(): Unit = {
println("eat bugs")
}
override def move(): Unit = {
println("fly")
}
}
複製代碼
在Scala中,咱們將extends with
的這種語法解讀爲一個總體,例如在以上代碼中,咱們將extends Animal with Egg
看作一個總體,而後被Bird
類混入。從這裏你是否也可以感覺到 trait
在嘗試着將抽象更好地融爲一個總體。指針
到這裏,你或許可以發現,相比Java interface
,trait
和類更加類似。而事實也確實如此,trait
能夠具有類的全部特性,除了缺乏構造器參數。這一點trait
可使用構造器字段來達到一樣的效果。也就是說你不能想給類傳入構造器參數那樣給特質傳入參數。具體代碼這裏就再也不演示。code
其實在這裏咱們能夠簡單地思考一番,爲何要把trait
設計得這麼像一個class
,是設計者們有意爲之,仍是無心間的巧合。其實,無論怎麼樣,我的認爲,但從設計層面來說,class
類的設計就比trait
更加具有一致性,class產生的對象就能夠被很好的管理,爲何咱們不像管理對象同樣來管理咱們的抽象呢?cdn
Traits
最多見的兩種使用方式:一種是和Java接口相似,用於設計富接口,另外一種是Traits獨有的stackable modifications
。這裏就說到了interface
和trait
的第二個區別,Traits支持stackable modificatio
,使它可以使用對象狀態,能夠對對象狀態進行靈活地迭代。
富接口的應用要歸功於interface
中對默認方法這一特性的支持,一方面鬆綁了類和接口之間實現與被實現之間的強關係,另外一方面爲程序的可擴展性代入了很大的靈活性。trait
在這一方面的應用和Java的沒有很大的區別。而trait
中的默認方法的實現背後採用的也是interface
中的default
默認方法。
trait Hello {
def hello(): Unit = {println("hello")
}
}
複製代碼
interface Hello2 {
default void hello() {...}
}
複製代碼
關於stackable modifications
,顧名思義,咱們將modification
保存在了一個stack
棧中。也就是說咱們能夠對運算的結果進行不斷的迭代處理,已達到咱們想要的結果。這對於想要分佈處理並獲得某一結果的需求來講是很是有用的。
這裏咱們借用一下programming in scala
中的例子
abstract class IntQueue {
def get(): Int
def put(x: Int)
}
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x: Int) {
buf += x
}
}
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)
}
}
複製代碼
在以上代碼中咱們定義了一個抽象的隊列,有put
和get
方法,在類BasicIntQueue中提供了相應的實現方法。同時又定義了三個特質Doubling
、Incrementing
、Filtering
,它們都繼承了IntQueue抽象類(還記得以前講過的,trait
能夠具有類的全部特性),並重寫了其中的方法。Doubling
將處理結果*2,Incrementing
特質將處理結果作了+1處理,Filtering
將過濾掉<0的值。
咱們在來看如下的運行結果
scala> val queue = (new BasicIntQueue with Incrementing with Filtering)
queue: BasicIntQueue with Incrementing with Filtering...
scala> queue.put(-1); queue.put(0); queue.put(1)
scala> queue.get()
res15: Int = 1
scala> queue.get()
res16: Int = 2
複製代碼
scala> val queue = (new BasicIntQueue with Filtering with Incrementing)
queue: BasicIntQueue with Filtering with Incrementing...
scala> queue.put(-1); queue.put(0); queue.put(1)
scala> queue.get()
res17: Int = 0
scala> queue.get()
res18: Int = 1
scala> queue.get()
res19: Int = 2
複製代碼
仔細觀察以上的代碼,瞭解了上面的代碼,你基本也就瞭解了stackable modifications
。
首先,你能夠觀察到,以上的兩段代碼總體類似,卻獲得不一樣的運行結果,緣由只是由於特質Filtering
和Incrementing
混入的順序不一樣。咱們仔細查看一下特質中的方法實現,能夠發如今特質中都經過super
關鍵字調用了父類的方法。而以上狀況的產生緣由就在於此。trait
中的super
是支持stackable modifications
的根本關鍵。
在trait
中的super
是動態綁定的,而且super
調用的是另外一個特質中的方法,具體哪一個特質中的方法被調用須要取決於特質被混入的順序。對於通常的序列,咱們能夠採用"從後往前"的順序來推斷super
的調用順序。
就拿以上的代碼而言。
new BasicIntQueue with Incrementing with Filtering
複製代碼
代碼的super的執行順序按照從後往前的規則依次是
Filtering -> Incrementing -> BasicIntQueue
複製代碼
舉個具體的例子
例如這個時候我執行了put(1)
的代碼,那麼按照上面的執行順序,
先執行Filtering
的put
方法判斷值是否大於1,發現合法,將值1傳給Incrementing
中的put
方法,Incrementing
中的put
方法將值加1以後傳給BasicIntQueue
而後將最終的值2放入隊列中。
以上代碼的執行過程就是stackable modifications
的核心。所以到這裏,你或許也能理解以上由於混入順序不一樣而出現的不一樣結果了吧。
另外,說到動態性,咱們在這裏也能夠簡單地聊幾句。在Java中,super
的靜態性與trait
中super
的動態性造成了鮮明的對比。而動態性所帶來的種種優點與強大,咱們也已經在這一小節的內容中見識了一二。其實動態性抽離出來是一種設計思想,而它也早已在咱們的身邊大展拳腳。例如咱們熟知的IOC依賴注入,AOP面向切面編程,以及前端的動態壓縮技術等等,可以列舉的還有不少,而它們的背後就是動態性的思想,你越是靈活,可以作的事也就越多。
trait Test {
val name:String = "hello" //特質構造器的一部分
println(name); // 特質構造器的一部分
}
複製代碼
正如你在以上代碼中所見的,在特質大括號中包裹的執行語句均屬於特質構造器的一部分。
特質構造器的順序以下:(參考自《快學Scala》)
extends
以後的類)舉個例子
class SavingAccount extends Account with FileLogger with ShortLogger
trait ShortLogger extends Logger
trait FileLogger extends Logger
複製代碼
以上構造器將按以下順序執行
Account
(超類)Logger
(第一個特質的父特質)FileLogger
(第一個特質)ShortLogger
(從左往右第二個特質,它的父特質Logger
已經被構造,再也不重複構造)SavingAccount
(類構造器)其實以上構造器順序實現的背後使用的是一種叫"線性化"的技術。
拿以上的代碼做爲例子
class SavingAccount extends Account with FileLogger with ShortLogger
複製代碼
以上的代碼將被線性化解析爲
>>
的意思是右側將先被構造
lin(SavingsAccount) = SavingsAccount >> lin(ShortLogger) >> lin(FileLogger) >> lin(Account)
= SavingsAccount >> (ShortLogger >> Logger) >> (FileLogger >> Logger) >> Account
= SavingsAccount >> ShortLogger >> FileLogger >> Logger >> Account
複製代碼
仔細觀察如下線性化的結果,你會發現,以上的順序就是構造器執行的順序。同時,線性化也給出了super
的執行順序,舉例來講,在ShortLogger
中調用super
將調用右側的FileLogger
中的方法,而FileLogger
中的super
將調用右側Logger
中的方法,依次類推。
所以因爲特質構造器的執行時間要早於類構造器的執行,所以在初始化特質中的字段時要額外注意字段的執行時間,避免出現空指針的狀況。例如如下代碼就會出現錯誤
trait Hello {
val name:String
val out = new PrintStream(name)
}
val test = new Test with Hello {
val name = "Rhyme" // Error 類構造器晚於特質構造器
}
複製代碼
解決方法有提早定義
或者懶值
採用提早定義的代碼以下所示
val test = new {
val name = "Rhyme" //先於全部的構造器執行
}Test with Hello
複製代碼
採用提早定義的方式使得代碼不太雅觀,咱們還可使用懶值的方式
採用懶值的方式以下
trait Hello {
val name:String
lazy val out = new PrintStream(name) // 使用懶值,延遲name的初始化
}
複製代碼
懶值在每次使用前都回去檢查字段是否已經初始化,存在必定的使用開銷。使用前須要仔細考慮
因爲篇幅限制,關於trait
的探索,咱們就到此爲止。但願本文可以對你學習和了解trait
提供一點幫助。在下一章咱們將介紹trait
稍微高級一點的用法,自身類型和結構類型。