Scala中的implicit
關(guān)鍵字對于我們初學(xué)者像是一個謎一樣的存在砍艾,一邊驚訝于代碼的簡潔,
一邊像在迷宮里打轉(zhuǎn)一樣地去找隱式的代碼,因此我們團(tuán)隊結(jié)合目前的開發(fā)工作只搁,將implicit
作為一個專題進(jìn)行研究赤兴,了一些心得妖滔。
在研究的過程當(dāng)中,我們注重三方面:
- 為什么需要
implicit
? -
implicit
包含什么桶良,有什么內(nèi)在規(guī)則座舍? -
implicit
的應(yīng)用模式有哪些?
為什么需要Implicit?
Scala在面對編譯出現(xiàn)類型錯誤時陨帆,提供了一個由編譯器自我修復(fù)的機(jī)制曲秉,編譯器試圖去尋找
一個隱式implicit
的轉(zhuǎn)換方法,轉(zhuǎn)換出正確的類型疲牵,完成編譯承二。這就是implicit
的意義。
我們正在做Guardian
系統(tǒng)的升級纲爸,Guardian
是公司內(nèi)部的核心系統(tǒng)亥鸠,提供統(tǒng)一權(quán)限管控、
操作審計、單點登錄等服務(wù)负蚊。系統(tǒng)已經(jīng)有4年多的歷史了神妹,已經(jīng)難以滿足目前的需要,比如:
當(dāng)時僅提供了RESTFul的服務(wù)接口家妆,而隨著性能需求的提高灾螃,有些服務(wù)使用Tcp消息完成遠(yuǎn)程
調(diào)用;另外揩徊,在RESTFull接口的協(xié)議方面腰鬼,我們也想做一些優(yōu)化。
而現(xiàn)狀是公司內(nèi)部系統(tǒng)已經(jīng)全部接入Guardian
塑荒,要接入新版熄赡,不可能一次全部遷移,甚至
要花很長一段時間才能完成遷移工作齿税,因此新版接口必須同時支持新老兩個版本接口協(xié)議彼硫。
因此我們必須解決兩個問題:
- 兼容老版本協(xié)議, 以便能夠平滑升級
- 支持多種協(xié)議,以滿足不同業(yè)務(wù)系統(tǒng)的需求
我們希望對接口層提供一個穩(wěn)定的Service
接口凌箕,以免業(yè)務(wù)的變動影響前端接口代碼拧篮,常規(guī)
的做法是我們在Service
接口上定義多種版本的方法(重載),比如鑒權(quán)服務(wù):
trait AuthService {
// 兼容老版本的鑒權(quán)業(yè)務(wù)方法
def auth(p: V1HttpAuthParam): Future[V1HttpAuthResult]
// 新版本的鑒權(quán)業(yè)務(wù)方法
def auth(p: V2HttpAuthParam): Future[V2HttpAuthResult]
// 新版本中支持的對Tcp消息鑒權(quán)的業(yè)務(wù)方法
def auth(p: V2TcpMsg): Future[V2TcpMsg]
}
這種做法的問題在于一旦業(yè)務(wù)發(fā)生變化牵舱,出現(xiàn)了新的參數(shù)串绩,勢必要修改AuthService
接口,
添加新的接口方法芜壁,接口不穩(wěn)定礁凡。
假如有一個通用的auth
方法就好了:
trait AuthParam {}
trait StableAuthService{
// 穩(wěn)定的鑒權(quán)接口
def auth(p: AuthParam)
}
這樣,我們就可以按照下面的方式調(diào)用:
//在老版本的REST WS接口層:
val result = authService auth V1HttpAuthParam
response(result)
//在新版本的REST WS接口層:
val result = authService auth V2HttpAuthParam
response(result)
// .... 在更多的服務(wù)接口層慧妄,任意的傳入?yún)?shù)顷牌,獲得結(jié)果
很明顯,這樣的代碼編譯出錯塞淹。 因為在authService中沒有這樣的方法簽名窟蓝。
再舉個簡單的例子, 我們想在打印字符串時,添加一些分隔符饱普,下面是最自然的調(diào)用方式:
"hello,world" printWithSeperator "*"
很明顯运挫,這樣的代碼編譯出錯。 因為String 沒有這樣的方法费彼。
Scala在面對編譯出現(xiàn)類型錯誤時滑臊,提供了一個由編譯器自我修復(fù)的機(jī)制,編譯器試圖去尋找
一個隱式implicit
的轉(zhuǎn)換方法箍铲,轉(zhuǎn)換出正確的類型雇卷,完成編譯。這就是implicit
的意義。
Implicit包含什么关划,有什么內(nèi)在規(guī)則小染?
Scala 中的implicit
包含兩個方面:
- 隱式參數(shù)(implicit parameters)
- 隱式轉(zhuǎn)換(implicit conversion)
隱式參數(shù)(implicit parameters)
隱式參數(shù)同樣是編譯器在找不到函數(shù)需要某種類型的參數(shù)時的一種修復(fù)機(jī)制,我們可以采用顯式的柯里化式
的隱式參數(shù)申明贮折,也可以進(jìn)一步省略裤翩,采用implicitly
方法來獲取所需要的隱式變量。
隱式參數(shù)相對比較簡單调榄,Scala中的函數(shù)申明提供了隱式參數(shù)的語法踊赠,在函數(shù)的最后的柯里化參數(shù)
列表中可以添加隱式implicit
關(guān)鍵字進(jìn)行標(biāo)記, 標(biāo)記為implicit
的參數(shù)在調(diào)用中可以省略每庆,
Scala編譯器會從當(dāng)前作用域中尋找一個相同類型的隱式變量筐带,作為調(diào)用參數(shù)。
在Scala的并發(fā)庫中就大量使用了隱式參數(shù)缤灵,比如Future:
// Future 需要一個隱式的ExecutionContext
// 引入一個默認(rèn)的隱式ExecutionContext, 否則編譯不通過
import scala.concurrent.ExecutionContext.Implicits.default
Future {
sleep(1000)
println("I'm in future")
}
對于一些常量類的伦籍,可共用的一些對象,我們可以用隱式參數(shù)來簡化我們的代碼腮出,比如帖鸦,我們的應(yīng)用
一般都需要一個配置對象:
object SomeApp extends App {
//這是我們的全局配置類
class Setting(config: Config) {
def host: String = config.getString("app.host")
}
// 申明一個隱式的配置對象
implicit val setting = new Setting(ConfigFactory.load)
// 申明隱式參數(shù)
def startServer()(implicit setting: Setting): Unit = {
val host = setting.host
println(s"server listening on $host")
}
// 無需傳入隱式參數(shù)
startServer()
}
甚至,Scala為了更進(jìn)一步減少隱式參數(shù)的申明代碼,我們都可以不需要再函數(shù)參數(shù)上顯示的申明,在scala.Predef
包中迅箩,提供了一個implicitly
的函數(shù),幫助我們找到當(dāng)前上下文中所需要類型的
隱式變量:
@inline def implicitly[T](implicit e: T) = e // for summoning implicit values from the nether world
因此上面的startServer
函數(shù)我們可以簡化為:
// 省略隱式參數(shù)申明
def startServer(): Unit = {
val host = implicitly[Setting].host
println(s"server listening on $host")
}
需要注意的是立倍,進(jìn)一步簡化之后,代碼的可讀性有所損失侣滩,調(diào)用方并不知道startServer
需要一個隱式的
配置對象,要么加強(qiáng)文檔說明变擒,要么選用顯式的申明君珠,這種權(quán)衡需要團(tuán)隊達(dá)成一致。
隱式轉(zhuǎn)換(implicit conversion)
回顧一下前面說到的小例子娇斑,讓字符串能夠帶分隔符打硬咛怼:
"hello,world" printWithSeperator "*"
此時,Scala編譯器嘗試從當(dāng)前的表達(dá)式作用域范圍中尋找能夠?qū)?code>String轉(zhuǎn)換成一個具有printWithSeperator
函數(shù)的對象毫缆。
為此唯竹,我們提供一個PrintOps
的trait
,有一個printWithSeperator
函數(shù):
trait PrintOps {
val value: String
def printWithSepeator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
此時苦丁,編譯仍然不通過浸颓,因為Scala編譯器并沒有找到一個可以將String
轉(zhuǎn)換為PrintOps
的方法!那我們申明一個:
def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
OK, 我們可以顯示地調(diào)用stringToPrintOps
了:
stringToPrintOps("hello,world") printWithSepeator "*"
離我們的最終目標(biāo)只有一步之遙了,只需要將stringToPrintOps
方法標(biāo)記為implicit
即可产上,除了為String
添加stringToPrintOps
的能力棵磷,還可以為其他類型添加,完整代碼如下:
object StringOpsTest extends App {
// 定義打印操作Trait
trait PrintOps {
val value: String
def printWithSeperator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
// 定義針對String的隱式轉(zhuǎn)換方法
implicit def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
// 定義針對Int的隱式轉(zhuǎn)換方法
implicit def intToPrintOps(i: Int): PrintOps = new PrintOps {
override val value: String = i.toString
}
// String 和 Int 都擁有 printWithSeperator 函數(shù)
"hello,world" printWithSeperator "*"
1234 printWithSeperator "*"
}
隱式轉(zhuǎn)換的規(guī)則 -- 如何尋找隱式轉(zhuǎn)換方法
Scala編譯器是按照怎樣的套路來尋找一個可以應(yīng)用的隱式轉(zhuǎn)換方法呢晋涣? 在Martin Odersky的Programming in Scala, First Edition中總結(jié)了以下幾條原則:
- 標(biāo)記規(guī)則:只會去尋找?guī)в?code>implicit標(biāo)記的方法仪媒,這點很好理解,在上面的代碼也有演示谢鹊,如果不申明為
implicit
只能手工去調(diào)用算吩。 - 作用域范圍規(guī)則:
- 只會在當(dāng)前表達(dá)式的作用范圍之內(nèi)查找,而且只會查找單一標(biāo)識符的函數(shù)佃扼,上述代碼中赌莺,
如果stringToPrintOps
方法封裝在其他對象(加入叫Test)中,雖然Test
對象也在作用域范圍之內(nèi)松嘶,但編譯器不會嘗試使用Test.stringToPrintOps
進(jìn)行轉(zhuǎn)換艘狭,這就是單一標(biāo)識符的概念。 -
單一標(biāo)識符有一個例外翠订,如果
stringToPrintOps
方法在PrintOps
的伴生對象中申明也是有效的巢音,Scala
編譯器也會在源類型或目標(biāo)類型的伴生對象內(nèi)查找隱式轉(zhuǎn)換方法,本規(guī)則只會在轉(zhuǎn)型有效尽超。而一般的慣例官撼,會將隱式轉(zhuǎn)換方法封裝在伴生對象中。 - 當(dāng)前作用域上下文的隱式轉(zhuǎn)換方法優(yōu)先級高于伴生對象內(nèi)的隱式方法
- 只會在當(dāng)前表達(dá)式的作用范圍之內(nèi)查找,而且只會查找單一標(biāo)識符的函數(shù)佃扼,上述代碼中赌莺,
- 不能有歧義原則:在相同優(yōu)先級的位置只能有一個隱式的轉(zhuǎn)型方法似谁,否則Scala編譯器無法選擇適當(dāng)?shù)倪M(jìn)行轉(zhuǎn)型傲绣,編譯出錯。
- 只應(yīng)用轉(zhuǎn)型方法一次原則:Scala編譯器不會進(jìn)行多次隱式方法的調(diào)用巩踏,比如需要
C
類型參數(shù)秃诵,而實際類型為A
,作用域內(nèi)
存在A => B
,B => C
的隱式方法塞琼,Scala編譯器不會嘗試先調(diào)用A => B
,再調(diào)用B => C
菠净。 - 顯示方法優(yōu)先原則:如果方法被重載,可以接受多種類型彪杉,而作用域中存在轉(zhuǎn)型為另一個可接受的參數(shù)類型的隱式方法毅往,則不會
被調(diào)用,Scala編譯器優(yōu)先選擇無需轉(zhuǎn)型的顯式方法派近,例如:def m(a: A): Unit = ??? def m(b: B): Unit = ??? val b: B = new B //存在一個隱式的轉(zhuǎn)換方法 B => A implicit def b2a(b: B): A = ??? m(b) //隱式方法不會被調(diào)用攀唯,優(yōu)先使用顯式的 m(b: B): Unit
Implicit的應(yīng)用模式有哪些?
隱式轉(zhuǎn)換的核心在于將錯誤的類型通過查找隱式方法渴丸,轉(zhuǎn)換為正確的類型侯嘀×砹瑁基于Scala編譯器的這種隱式轉(zhuǎn)換機(jī)制,通常有兩種應(yīng)用
模式:Magnet Pattern
和Method Injection
残拐。
Magnet Pattern
Magnet Pattern
模式暫且翻譯為磁鐵模式
, 解決的是方法參數(shù)類型的不匹配問題途茫,能夠優(yōu)雅地解決本文開頭所提出的問題,
用一個通用的Service
方法簽名來屏蔽不同版本溪食、不同類型服務(wù)的差異囊卜。
磁鐵模式的核心在于,將函數(shù)的調(diào)用參數(shù)和返回結(jié)果封裝為一個磁鐵參數(shù)错沃,這樣方法的簽名就統(tǒng)一為一個了栅组,不需要函數(shù)重載;再
定義不同參數(shù)到磁鐵參數(shù)的隱式轉(zhuǎn)換函數(shù)枢析,利用Scala的隱式轉(zhuǎn)換機(jī)制玉掸,達(dá)到類似于函數(shù)重載的效果。
磁鐵模式廣泛運用于Spray Http 框架醒叁,該框架已經(jīng)遷移到Akka Http中司浪。
下面,我們一步步來實現(xiàn)一個磁鐵模式把沼,來解決本文開頭提出的問題啊易。
-
定義
Magnet
參數(shù)和使用Magnet
參數(shù)的通用鑒權(quán)服務(wù)方法// Auth Magnet參數(shù) trait AuthMagnet { type Result def apply(): Result } // Auth Service 方法 trait AuthService { def auth(am: AuthMagnet): am.Result = am() }
-
實現(xiàn)不同版本的
AuthService
//v1 auth service trait V1AuthService extends AuthService //v2 auth service trait V2AuthService extends AuthService
-
實現(xiàn)不同版本
AuthService
的伴生對象,添加適當(dāng)?shù)碾[式轉(zhuǎn)換方法//V1 版本的服務(wù)實現(xiàn) object V1AuthService { case class V1AuthRequest() case class V1AuthResponse() implicit def toAuthMagnet(p: V1AuthRequest): AuthMagnet {type Result = V1AuthResponse} = new AuthMagnet { override def apply(): Result = { // v1 版本的auth 業(yè)務(wù)委托到magnet的apply中實現(xiàn) println("這是V1 Auth Service") V1AuthResponse() } override type Result = V1AuthResponse } } //V2 版本的服務(wù)實現(xiàn) object V2AuthService { case class V2AuthRequest() case class V2AuthResponse() implicit def toAuthMagnet(p: V2AuthRequest): AuthMagnet {type Result = V2AuthResponse} = new AuthMagnet { override def apply(): Result = { // v2 版本的auth 業(yè)務(wù)委托到magnet的apply中實現(xiàn) println("這是V2 Auth Service") V2AuthResponse() } override type Result = V2AuthResponse } }
-
編寫兩個版本的資源接口(demo)
trait V1Resource extends V1AuthService { def serv(): Unit = { val p = V1AuthRequest() val response = auth(p) println(s"v1 resource response: $response") } } trait V2Resource extends V2AuthService { def serv(): Unit = { val p = V2AuthRequest() val response = auth(p) println(s"v2 resource response: $response") } } val res1 = new V1Resource {} val res2 = new V2Resource {} res1.serv() res2.serv()
控制臺輸出結(jié)果為:
這是V1 Auth Service v1 resource response: V1AuthResponse() 這是V2 Auth Service v2 resource response: V2AuthResponse()
Method Injection
Method Injection 暫且翻譯為方法注入饮睬,意思是給一個類型添加沒有定義的方法租谈,實際上也是通過隱式轉(zhuǎn)換來實現(xiàn)的,
這種技術(shù)在Scalaz中廣泛使用捆愁,Scalaz為我們提供了和Haskell類似的函數(shù)式編程庫割去。
本文中的關(guān)于printWithSeperator
方法的例子其實就是Method Injection
的應(yīng)用,從表面上看昼丑,即是給String
和
Int
類型添加了printWithSeperator
方法呻逆。
與Magnet Pattern
不同的是轉(zhuǎn)型所針對的對象,Magnet Pattern
是針對方法參數(shù)進(jìn)行轉(zhuǎn)型,
Method Injection
是針對調(diào)用對象進(jìn)行轉(zhuǎn)型矾克。
舉個簡單的例子页慷,Scala
中的集合都是一個Functor
,都可以進(jìn)行map
操作胁附,但是Java
的集合框架卻沒有,
如果需要對java.util.ArrayList
等進(jìn)行map
操作則需要先轉(zhuǎn)換為Scala
對應(yīng)的類型滓彰,非常麻煩控妻,借助Method Injection
,我們可以提供這樣的輔助工具揭绑,讓Java
的集合框架也成為一種Functor
弓候,具備map
能力:
- 首先定義一個Functor
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A ? B): F[B] }
- 再定義一個FunctorOps
final class FunctorOps[F[_], A](l: F[A])(implicit functor: Functor[F]) { def map[A, B](f: A ? B): F[B] = functor.map(l)(f) }
- 在FunctorOps的伴生對象中定義針對java.util.List[E]的隱式Funcotr實例和針對java.util.List[E]到
FunctorOps的隱式轉(zhuǎn)換方法object FunctorOps { // 針對List[E]的functor implicit val jlistFunctor: Functor[JList] = new Functor[JList] { override def map[A, B](fa: JList[A])(f: (A) => B): JList[B] = { val fb = new JLinkList[B]() val it = fa.iterator() while(it.hasNext) fb.add(f(it.next)) fb } } // 將List[E]轉(zhuǎn)換為FunctorOps的隱式轉(zhuǎn)換方法 implicit def jlistToFunctorOps[E](jl: JList[E]): FunctorOps[JList, E] = new FunctorOps[JList, E](jl) }
- 愉快滴使用map啦
val jlist = new util.ArrayList[Int]() jlist.add(1) jlist.add(2) jlist.add(3) jlist.add(4) import FunctorOps._ val jlist2 = jlist map (_ * 3) println(jlist2) // [3, 6, 9, 12]
總結(jié)
Implicit 是Scala語言中處理編譯類型錯誤的一種修復(fù)機(jī)制郎哭,利用該機(jī)制,我們可以編寫出任意參數(shù)和返回值的多態(tài)方法(這種多
態(tài)也被稱為Ad-hoc polymorphism
-- 任意多態(tài))菇存,實現(xiàn)任意多態(tài)夸研,我們通常使用Magnet Pattern
磁鐵模式;同時還可以
給其他類庫的類型添加方法來對其他類庫進(jìn)行擴(kuò)展依鸥,通常將這種技術(shù)稱之為Method Injection
亥至。
參考資料
- 《Programming in Scala》中關(guān)于隱式轉(zhuǎn)換和隱式參數(shù)章節(jié): http://www.artima.com/pins1ed/implicit-conversions-and-parameters.html
- 《The Magnet Pattern》http://spray.io/blog/2012-12-13-the-magnet-pattern/