Scala中的implicit
關鍵字對於咱們初學者像是一個謎同樣的存在,一邊驚訝於代碼的簡潔,html
一邊像在迷宮裏打轉同樣地去找隱式的代碼,所以咱們團隊結合目前的開發工做,將implicit
做爲一個專題進行研究,了一些心得。前端
在研究的過程中,咱們注重三方面:java
implicit
?implicit
包含什麼,有什麼內在規則?implicit
的應用模式有哪些?Scala在面對編譯出現類型錯誤時,提供了一個由編譯器自我修復的機制,編譯器試圖去尋找
一個隱式implicit
的轉換方法,轉換出正確的類型,完成編譯。這就是implicit
的意義。git
咱們正在作Guardian
系統的升級,Guardian
是公司內部的核心繫統,提供統一權限管控、
操做審計、單點登陸等服務。系統已經有4年多的歷史了,已經難以知足目前的須要,好比:
當時僅提供了RESTFul的服務接口,而隨着性能需求的提升,有些服務使用Tcp消息完成遠程
調用;另外,在RESTFull接口的協議方面,咱們也想作一些優化。github
而現狀是公司內部系統已經所有接入Guardian
,要接入新版,不可能一次所有遷移,甚至
要花很長一段時間才能完成遷移工做,所以新版接口必須同時支持新老兩個版本接口協議。編程
所以咱們必須解決兩個問題:併發
咱們但願對接口層提供一個穩定的Service
接口,以避免業務的變更影響前端接口代碼,常規
的作法是咱們在Service
接口上定義多種版本的方法(重載),好比鑑權服務:app
trait AuthService { // 兼容老版本的鑑權業務方法 def auth(p: V1HttpAuthParam): Future[V1HttpAuthResult] // 新版本的鑑權業務方法 def auth(p: V2HttpAuthParam): Future[V2HttpAuthResult] // 新版本中支持的對Tcp消息鑑權的業務方法 def auth(p: V2TcpMsg): Future[V2TcpMsg] }
這種作法的問題在於一旦業務發生變化,出現了新的參數,勢必要修改AuthService
接口,
添加新的接口方法,接口不穩定。框架
假若有一個通用的auth
方法就行了:ide
trait AuthParam {} trait StableAuthService{ // 穩定的鑑權接口 def auth(p: AuthParam) }
這樣,咱們就能夠按照下面的方式調用:
//在老版本的REST WS接口層: val result = authService auth V1HttpAuthParam response(result) //在新版本的REST WS接口層: val result = authService auth V2HttpAuthParam response(result) // .... 在更多的服務接口層,任意的傳入參數,得到結果
很明顯,這樣的代碼編譯出錯。 由於在authService中沒有這樣的方法簽名。
再舉個簡單的例子, 咱們想在打印字符串時,添加一些分隔符,下面是最天然的調用方式:
"hello,world" printWithSeperator "*"
很明顯,這樣的代碼編譯出錯。 由於String 沒有這樣的方法。
Scala在面對編譯出現類型錯誤時,提供了一個由編譯器自我修復的機制,編譯器試圖去尋找
一個隱式implicit
的轉換方法,轉換出正確的類型,完成編譯。這就是implicit
的意義。
Scala 中的implicit
包含兩個方面:
隱式參數一樣是編譯器在找不到函數須要某種類型的參數時的一種修復機制,咱們能夠採用顯式的柯里化式
的隱式參數申明,也能夠進一步省略,採用implicitly
方法來獲取所須要的隱式變量。
隱式參數相對比較簡單,Scala中的函數申明提供了隱式參數的語法,在函數的最後的柯里化參數
列表中能夠添加隱式implicit
關鍵字進行標記, 標記爲implicit
的參數在調用中能夠省略,
Scala編譯器會從當前做用域中尋找一個相同類型的隱式變量,做爲調用參數。
在Scala的併發庫中就大量使用了隱式參數,好比Future:
// Future 須要一個隱式的ExecutionContext // 引入一個默認的隱式ExecutionContext, 不然編譯不經過 import scala.concurrent.ExecutionContext.Implicits.default Future { sleep(1000) println("I'm in future") }
對於一些常量類的,可共用的一些對象,咱們能夠用隱式參數來簡化咱們的代碼,好比,咱們的應用
通常都須要一個配置對象:
object SomeApp extends App { //這是咱們的全局配置類 class Setting(config: Config) { def host: String = config.getString("app.host") } // 申明一個隱式的配置對象 implicit val setting = new Setting(ConfigFactory.load) // 申明隱式參數 def startServer()(implicit setting: Setting): Unit = { val host = setting.host println(s"server listening on $host") } // 無需傳入隱式參數 startServer() }
甚至,Scala爲了更進一步減小隱式參數的申明代碼,咱們均可以不須要再函數參數上顯示的申明,在scala.Predef
包中,提供了一個implicitly
的函數,幫助咱們找到當前上下文中所須要類型的
隱式變量:
@inline def implicitly[T](implicit e: T) = e // for summoning implicit values from the nether world
所以上面的startServer
函數咱們能夠簡化爲:
// 省略隱式參數申明 def startServer(): Unit = { val host = implicitly[Setting].host println(s"server listening on $host") }
須要注意的是,進一步簡化以後,代碼的可讀性有所損失,調用方並不知道startServer
須要一個隱式的
配置對象,要麼增強文檔說明,要麼選用顯式的申明,這種權衡須要團隊達成一致。
回顧一下前面說到的小例子,讓字符串可以帶分隔符打印:
"hello,world" printWithSeperator "*"
此時,Scala編譯器嘗試從當前的表達式做用域範圍中尋找可以將String
轉換成一個具備printWithSeperator
函數的對象。
爲此,咱們提供一個PrintOps
的trait
,有一個printWithSeperator
函數:
trait PrintOps { val value: String def printWithSepeator(sep: String): Unit = { println(value.split("").mkString(sep)) } }
此時,編譯仍然不經過,由於Scala編譯器並無找到一個能夠將String
轉換爲PrintOps
的方法!那咱們申明一個:
def stringToPrintOps(str: String): PrintOps = new PrintOps { override val value: String = str }
OK, 咱們能夠顯示地調用stringToPrintOps
了:
stringToPrintOps("hello,world") printWithSepeator "*"
離咱們的最終目標只有一步之遙了,只須要將stringToPrintOps
方法標記爲implicit
便可,除了爲String
添加stringToPrintOps
的能力,還能夠爲其餘類型添加,完整代碼以下:
object StringOpsTest extends App { // 定義打印操做Trait trait PrintOps { val value: String def printWithSeperator(sep: String): Unit = { println(value.split("").mkString(sep)) } } // 定義針對String的隱式轉換方法 implicit def stringToPrintOps(str: String): PrintOps = new PrintOps { override val value: String = str } // 定義針對Int的隱式轉換方法 implicit def intToPrintOps(i: Int): PrintOps = new PrintOps { override val value: String = i.toString } // String 和 Int 都擁有 printWithSeperator 函數 "hello,world" printWithSeperator "*" 1234 printWithSeperator "*" }
Scala編譯器是按照怎樣的套路來尋找一個能夠應用的隱式轉換方法呢? 在Martin Odersky的Programming in Scala, First Edition中總結了如下幾條原則:
implicit
標記的方法,這點很好理解,在上面的代碼也有演示,若是不申明爲implicit
stringToPrintOps
方法封裝在其餘對象(加入叫Test)中,雖然Test
對象也在做用域範圍以內,但編譯器不會嘗試使用Test.stringToPrintOps
進行轉換,這就是單一標識符的概念。stringToPrintOps
方法在PrintOps
的伴生對象中申明也是有效的,ScalaC
類型參數,而實際類型爲A
,做用域內A => B
,B => C
的隱式方法,Scala編譯器不會嘗試先調用A => B
,再調用B => C
。def m(a: A): Unit = ??? def m(b: B): Unit = ??? val b: B = new B //存在一個隱式的轉換方法 B => A implicit def b2a(b: B): A = ??? m(b) //隱式方法不會被調用,優先使用顯式的 m(b: B): Unit
隱式轉換的核心在於將錯誤的類型經過查找隱式方法,轉換爲正確的類型。基於Scala編譯器的這種隱式轉換機制,一般有兩種應用
模式:Magnet Pattern
和Method Injection
。
Magnet Pattern
模式暫且翻譯爲磁鐵模式
, 解決的是方法參數類型的不匹配問題,可以優雅地解決本文開頭所提出的問題,
用一個通用的Service
方法簽名來屏蔽不一樣版本、不一樣類型服務的差別。
磁鐵模式的核心在於,將函數的調用參數和返回結果封裝爲一個磁鐵參數,這樣方法的簽名就統一爲一個了,不須要函數重載;再
定義不一樣參數到磁鐵參數的隱式轉換函數,利用Scala的隱式轉換機制,達到相似於函數重載的效果。
磁鐵模式普遍運用於Spray Http 框架,該框架已經遷移到Akka Http中。
下面,咱們一步步來實現一個磁鐵模式,來解決本文開頭提出的問題。
定義Magnet
參數和使用Magnet
參數的通用鑑權服務方法
// Auth Magnet參數 trait AuthMagnet { type Result def apply(): Result } // Auth Service 方法 trait AuthService { def auth(am: AuthMagnet): am.Result = am() }
實現不一樣版本的AuthService
//v1 auth service trait V1AuthService extends AuthService //v2 auth service trait V2AuthService extends AuthService
實現不一樣版本AuthService
的伴生對象,添加適當的隱式轉換方法
//V1 版本的服務實現 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 業務委託到magnet的apply中實現 println("這是V1 Auth Service") V1AuthResponse() } override type Result = V1AuthResponse } } //V2 版本的服務實現 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 業務委託到magnet的apply中實現 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()
控制檯輸出結果爲:
這是V1 Auth Service v1 resource response: V1AuthResponse() 這是V2 Auth Service v2 resource response: V2AuthResponse()
Method Injection 暫且翻譯爲方法注入,意思是給一個類型添加沒有定義的方法,實際上也是經過隱式轉換來實現的,
這種技術在Scalaz中普遍使用,Scalaz爲咱們提供了和Haskell相似的函數式編程庫。
本文中的關於printWithSeperator
方法的例子其實就是Method Injection
的應用,從表面上看,便是給String
和Int
類型添加了printWithSeperator
方法。
與Magnet Pattern
不一樣的是轉型所針對的對象,Magnet Pattern
是針對方法參數進行轉型,Method Injection
是針對調用對象進行轉型。
舉個簡單的例子,Scala
中的集合都是一個Functor
,均可以進行map
操做,可是Java
的集合框架卻沒有,
若是須要對java.util.ArrayList
等進行map
操做則須要先轉換爲Scala
對應的類型,很是麻煩,藉助Method Injection
,咱們能夠提供這樣的輔助工具,讓Java
的集合框架也成爲一種Functor
,具有map
能力:
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A ⇒ B): F[B] }
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) }
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]轉換爲FunctorOps的隱式轉換方法 implicit def jlistToFunctorOps[E](jl: JList[E]): FunctorOps[JList, E] = new FunctorOps[JList, E](jl) }
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]
Implicit 是Scala語言中處理編譯類型錯誤的一種修復機制,利用該機制,咱們能夠編寫出任意參數和返回值的多態方法(這種多
態也被稱爲Ad-hoc polymorphism
-- 任意多態),實現任意多態,咱們一般使用Magnet Pattern
磁鐵模式;同時還能夠
給其餘類庫的類型添加方法來對其餘類庫進行擴展,一般將這種技術稱之爲Method Injection
。