Scala 是一門比較神奇的語言,提供了許多讓人措不及防的關鍵字與語法。雖然開頭有點僵硬,但是還是攔不住我說:這裏來了解一下 Scala 中 implicit 關鍵字吧。
implicit 可以翻譯成 “隱喻” -- 將兩個看似毫無關係的兩個東西,用看不見的絲將他們聯繫起來。比如月亮與思念、太陽與女神、“我去給你買個橘子”與“我是你爸爸”。可以讓收到隱喻後恍然大悟的人們瑟瑟發抖或者破口大罵。
以最後的一個隱喻作爲例子,我們首先創建兩個類。
IBugOrange
代表“我去買個橘子”。該類需要一個 Int
,代表希望買到的橘子數。totalMoney() 得到橘子一共需要的花費。
class IBugOrange(val number: Int) {
val per: Int = 1
def totalMoney() : Int = per * number
}
IAmFather
代表 “我是你爸爸”,現在有一個 talk() 方法,直接輸出 “爸爸” 想要說的話。
class IAmFather(val money: Int) {
def talk() = println(s"Stand here!($money)!")
}
賦值轉換
如果朱自清沒有寫《背影》,上面兩個類不會有什麼聯繫,於是我們無法把 IBugOrange
實例賦值給 IAmFather
:
val iAmFather: IAmFather = new IBugOrange(5) // error
爲了能夠完成上面的賦值操作,我們可以使用 implicit 創建一個隱喻:
implicit def originToFather(iBugOrange: IBugOrange): IAmFather
= new IAmFather(iBugOrange.totalMoney)
跟簡單,我們得到了一個使用 implicit
修飾的方法,該方法接受一個 IBugOrange
, 返回一個 IAmFather
,也就是建立了兩者的聯繫。
當我們將該隱喻放在賦值代碼的作用域中,就可以發現上面的賦值操作已經可以實現了。
implicit def originToFather(iBugOrange: IBugOrange): IAmFather
= new IAmFather(iBugOrange.totalMoney)
val iAmFather: IAmFather = new IBugOrange(5) // pass
隱喻函數也可以放在 IBugOrange 或 IAmFather 的聯盟對象中。或者將隱喻函數 import 到作用域中。
而上面的賦值能夠成功,得益於編譯器的辛勞。當編譯器發現正常的類型匹配失敗的時候,他會試圖在語句對應的作用域中尋找能夠完成對應類型轉換的隱喻函數,這裏就是函數 originToFather
, 將IBugOrange
轉成IAmFather
。
可以想象成編譯器將代碼轉換成了:
val iAmFather: IAmFather = originToFather(new IBugOrange(5))
提供原類型外的函數調用
通過上面的例子,可以發現其實就是編譯器幫忙生成了一個IAmFather
的對象出來,當然這樣我們就可以直接調用IAmFather
的方法了,像下面這樣:
new IBugOrange(6).talk()
當然不是 IBugOrange
擁有 talk() 方法,而是編譯器在中間進行了轉換:
originToFather(new IBugOrange(6)).talk()
在類上使用 implicit
implicit 可以修飾在 class 上(最外層的class不可以),能讓我們很簡單的實現裝飾器類,簡單點說就是在不影響原類的情況下爲原類添加更加豐富的函數。
比如我們希望 IAmFather
能夠直接飛過月臺,少一些蹣跚。
implicit class IAmFatherPlus(val iAmFather: IAmFather){
def fly() = {
print("I will fly!")
iAmFather.talk()
}
}
上面被 implicit
修飾的 IAmFatherPlus
需要一個 IAmFather
對象,意味着當我們在 IAmFather
對象上調用 fly() 方法的時候, 編譯器會直接幫助我們得到 IAmFatherPlus
:
new IAmFather(100).fly()
即實際爲:
new IAmFatherPlus(new IAmFather(100)).fly()
所以,得到輸出爲
I will fly!Stand here(100)!
函數參數上的 implicit
implicit 還有一個比較有意思用法就是可以將其修飾在函數的參數上,當函數調用的作用域中具有匹配類型的隱喻對象或者函數的時候,省去參數的傳遞,編譯器就讓函數就使用匹配的內容作爲參數。
比如我們 IAmFather
中定義一個 findSonLikeOrange
函數,讓 Father 去找他的 Son 喜歡的橘子。
def findSonLikeOrange(iSonLike: Int)(implicit orangeColorList: List[Int]): Int =
orangeColorList.count(_ == iSonLike)
可以發現 findSonLikeOrange
有兩個參數列表,第一個參數列表中的整型代表兒子喜歡的橘子。第二個參數列表中的 List 代表當下可買的橘子的顏色的列表,使用了 implicit 修飾。
每個兒子都有不同的愛好(有些彆扭),但是橘子攤只會有一個。
implicit val originList = List(1,2,2,3,4,5,6,2,2,1,3,5,6,7)
iAmFather1.findSonLikeOrange(3)
iAmFather2.findSonLikeOrange(3)
我們發現我們不需要顯式的將 originList 傳遞給 findSonLikeOrange 方法,方法也可以正常調用。
提取隱喻內容:
Scala 還提供了一個靜態方法,方便我們將添加了 implicit 關鍵字的對象或者函數方便的提起取出來使用:
def implicitly[T](implicit e: T) = e
可以發現只需要提供想要提取的內容的類型便可。例如提取出上剛纔的 List :
implicitly[List[Int]].length
其他注意項
- 在同一作用域中,我們不能聲明兩個同一類型的 implicit。在運行期 Scala 猜不透你想要使用哪一個implicit。
- 在 2.8 之後,當作用域中的幾個隱喻類型在同一條繼承結構中,Scala 會選擇更加具體的類型。在 2.8 之前, Scala 將會拒絕執行。
總結
雖然簡單,寫起來也是挺累的。
更多精彩,歡迎關注微信公衆號: Funny新青年