【Scala筆記——道】Scala 隱式Implicit

Scala隱式

隱式到底是什麼呢?

scala 中隱式相較於java 來講是一種全新的特性。那麼隱式到底是什麼呢?
隱式存在三種基本使用方式:
- 隱式屬性
- 隱式方法
- 隱式對象java

隱式屬性例如python

implicit val int size = 5

def caculate(a : Int)(implicit val size) = a * size

在這種狀況下,隱式是做爲一種上下文屬性進行輸入,更復雜的狀況就像上文中講到的執行上下文和功能控制。其本質上是一種資源的注入,隱藏/省略資源的顯示配置,而達到裝載的目的。
講到這裏相信很容易聯想到Spring中的IOC,在Spring/Guice的IOC中咱們經過註解/配置這種形式來實現注入資源實例化。Spring/Guice中對於注入的資源,咱們能夠經過xml或者註解完成資源屬性的配置,可是這種方法存在不足,xml或者註解配置都屬於靜態配置,若是咱們須要一些動態特性的時候須要額外的去作不少工做。
就好比咱們有一個支付Controller,在支付方式是美金的時候咱們須要調用美金服務,在支付方式爲人民幣的時候咱們須要調用人民幣服務。數據庫

class PayController @Inject()settlementService: SettlementService) { // 這種狀況下只能注入一種結算方式,沒法實現動態結算 def doSettle(bill: Bill) = { settlementService.doSettle(bill) } } 

若是要實現這種結算,咱們就必須手動經過代碼在判斷 bill.getType 後手動實現對應結算功能。但這樣就引入了耦合。編程

而隱式屬性無疑是一種更好的方式,這裏能夠自由注入結算方式api

 class PayController ()(implicit val settlementService: SettlementService) { def doSettle(bill: Bill) = { settlementService.doSettle(bill) } }

函數方法本質上是進行一種轉化,這種轉化不依賴上下文,也就是說
f(a) => b 不會影響任何其餘的狀態,也能夠稱做無反作用。
隱式函數方法,本質上也是一種函數方法,能夠看作是對於元素的一種轉化關係,由 a => b。
在java中 facade模式是比較經常使用的一種模式,facade模式提供的是對於接口信息的封裝。
在系統開發或業務開發中facade模式是使用比較頻繁的,在java中可能咱們對應不一樣系統接口會提供不一樣facade,但對於不一樣facade的轉化都須要在代碼中手動裝填。
經過方法級隱式轉化咱們能夠方便的實現接口級的隱式轉化。markdown

例以下文中咱們對於訂單進行擴展,實際結算的訂單可能涉及線上和線下兩種訂單,但最終訂單信息都會被轉化爲內部的 BillInfo。
這裏咱們經過方法級隱式轉化,直接實現 OnlineBillFacade/OfflineBillFacade => BillInfo
而不須要大量判斷代碼實現邏輯控制。session

sealed trait Bill
case class OnlineBillFacade(count: Int, platform: Platform, currency: Currency) extend Bill

case class OfflineBillFacade(count: Int, address: String, shop: Shop, currency: Currency) extend Bill

case class BillInfo( flowNumber: Long, createTime: Long, state: State, count: Int, platform: Platform, address: String, shop: Shop, currency: Currency) extend Bill


object BillConverter {

  implicit def onlineBill2BillInfo(facade: OnlineBillFacade) : BillInfo = ...
  implicit def offlineBillFacade(facade: OfflineBillFacade) : BillInfo  = ...
}

class PayController ()(implicit val settlementService: SettlementService) {

  def doSettle[T <: Bill](bill: T) = {
    settlementService.doSettle((BillInfo)bill)
  }

}

對於隱式對象,無疑是對於AOP思想的進一步的探索。在AOP中咱們想要不改變源碼還要增長功能,AOP中咱們經過動態代理實現功能的擴展。
經過動態代理,咱們能夠方便的實現切面控制。面向切面編程實際上有一個前提,就是咱們的一切其實都得圍繞接口進行設計,切面所能控制的最小粒度是就是方法級。
而且因爲是泛型配置,事實上若是要在切面中使用通知時,還須要對於輸入參數進行篩選判斷而完成泛型管理,這部分工做很不利於擴展,事實上這裏咱們沒有辦法對於泛型進行類型強約束。app

而隱式對象爲咱們帶來了全新的可能,因爲隱式對象是面向POJO,所以隱式對象相較AOP擁有更細的粒度控制。而且因爲是針對POJO,隱式對象不須要進行邊界界定。
經過隱式對象,咱們能夠真正在不改變原有代碼基礎上實現功能的擴展。函數

 應用

  • 執行上下文
  • 功能控制
  • 限定可用實例
  • 隱式證據
  • 類型擦除
  • 改善報錯
  • 虛類型

執行上下文

通用的上下文信息經過隱式默認實現,下降耦合ui

編寫事務、數據庫鏈接、線程池以及用戶會話時隱式參數上下文也一樣適合使用。使用方法參數能組合行爲,而將方法參數設置爲隱式參數可以使 API 變得更加簡潔。

// 導入了可供編譯器使用的全局默認值
import scala.concurrent.ExecutionContext.Implicits.global

apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]

功能控制

經過引入受權令牌,咱們能夠控制某些特定的 API 操做只能供某些用戶調用,咱們也可使用受權令牌決定數據可見性,而隱式用戶會話參數也許就包含了這類令牌信息。

def createMenu(implicit session: Session): Menu = {
  val defaultItems = List(helpItem, searchItem)
  val accountItems =
  if (session.loggedin()) List(viewAccountItem, editAccountItem)
  else List(loginItem)
  Menu(defaultItems ++ accountItems)
}

限定可用實例

對具備參數化類型方法中的類型參數進行限定,使該參數只接受某些類型的輸入

package implicits

object Implicits {
    import implicits.javadb.JRow


    implicit class SRow (jrow: JRow){

        def get[T](colName: String)(implicit toT: (JRow, String) => T): T =
            toT(jrow, colName)
    }


    implicit val jrowToInt: (JRow, String) => Int = (jrow: JRow, colName: String) => jrow.getInt(colName)
    implicit val jrowToDouble: (JRow, String) => Double = (jrow: JRow, colName: String) => jrow.getDouble(colName)
    implicit val jrowToString: (JRow, String) => String = (jrow: JRow, colName: String) => jrow.getText(colName)

    def main(args: Array[String]) = {
        val row = javadb.JRow("one" -> 1, "two" -> 2.2, "three" -> "THREE!")
        val oneValue1: Int = row.get("one")
        val twoValue1: Double = row.get("two")
        val threeValue1: String = row.get("three")
// val fourValue1: Byte = row.get("four")
        // 不編譯該行
        println(s"one1 -> $oneValue1")
        println(s"two1 -> $twoValue1")
        println(s"three1 -> $threeValue1")
        val oneValue2 = row.get[Int]("one")
        val twoValue2 = row.get[Double]("two")
        val threeValue2 = row.get[String]("three")

// val fourValue2 = row.get[Byte]("four")
        // 不編譯該行
        println(s"one2 -> $oneValue2")
        println(s"two2 -> $twoValue2")
        println(s"three2 -> $threeValue2")
    }
}


package database_api {

    case class InvalidColumnName(name: String)
        extends RuntimeException(s"Invalid column name $name")

    trait Row {
        def getInt      (colName: String): Int
        def getDouble   (colName: String): Double
        def getText     (colName: String): String
    }
}

package javadb {
    import database_api._

    case class JRow(representation: Map[String, Any]) extends Row {
        private def get(colName: String): Any =
            representation.getOrElse(colName, throw InvalidColumnName(colName))

        def getInt      (colName: String): Int      = get(colName).asInstanceOf[Int]
        def getDouble   (colName: String): Double   = get(colName).asInstanceOf[Double]
        def getText     (colName: String): String   = get(colName).asInstanceOf[String]
    }

    object JRow {
        def apply(pairs: (String, Any)*) = new JRow(Map(pairs :_*))
    }
}

隱式證據

有時候,咱們只須要限定容許的類型,並不須要提供額外的處理。換句話說,咱們須要
「證據」證實提出的類型知足咱們的需求。如今咱們將討論另一種被稱爲隱式證據的相
關技術來對容許的類型進行限定,並且這些類型無需繼承某一共有的超類。

trait TraversableOnce[+A] ... {
...
def toMap[T, U](implicit ev: <:<[A, (T, U)]): immutable.Map[T, U]
...
}

咱們曾說起過,可使用中綴表示法表示由兩個類型參數所組成的類型,所以下列兩種表
達式是等價的:
<:<[A, B]
A <:< B
在 toMap 中, B 其實是一個 pair:
<:<[A, (T, U

類型擦除

object M {
implicit object IntMarker
implicit object StringMarker
def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit = println(s"Seq[Int]: $seq")
def m(seq: Seq[String])(implicit s: StringMarker.type): Unit =
println(s"Seq[String]: $seq")
}

虛類型 Phantom Type

相似於類型擦除,虛類型僅用於標記。

虛類型自己實際上不屬於隱式轉換的範疇,但這裏其實和類型擦除在使用之上有必定的類似之初。

虛類型主要有如下兩個優勢:
- 使無效狀態沒法表明。最好的體現就是 List、Cons 以及 Nil的關係
- 攜帶類型級別的一些信息

例如經過虛類型限制距離單位

case class Distance[A](x: Double) extends AnyVal

case object Kilometer
case object Mile

def marathonDistance: Distance[Kilometer.type] = Distance[Kilometer.type](42.195)

def distanceKmToMiles(kilos: Distance[Kilometer.type]): Distance[Mile.type] =
 Distance[Mile.type](kilos.x * 0.621371)

def marathonDistanceInMiles: Distance[Mile.type] = distanceKmToMiles( marathonDistance )

隱式報錯

@implicitNotFound(msg =
"Cannot construct a collection of type ${To} with elements of type ${Elem}" +
" based on a collection of type ${From}.")
trait CanBuildFrom[-From, -Elem, +To] {...}

類型類模式

不一樣於java 子類型多態, 這一功能也被成爲 特設多態(ad hoc polymorphism)
scala java 共有 參數化多態(paremetric polymorphism)

case class Address(street: String, city: String)
case class Person(name: String, address: Address)
trait ToJSON {
def toJSON(level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String,String) =
(INDENTATION * level, INDENTATION * (level+1))
}
implicit class AddressToJSON(address: Address) extends ToJSON {
def toJSON(level: Int = 0): String = {
val (outdent, indent) = indentation(level)
s"""{ |${indent}"street": "${address.street}", |${indent}"city": "${address.city}" |$outdent}""".stripMargin
}
}
implicit class PersonToJSON(person: Person) extends ToJSON {

探究隱式

正如上述使用場景所述,隱式在scala中給咱們帶來不少驚喜。經過隱式,咱們也能夠更好的解決上下文處理、邊界處理、類型擦除等問題。

隱式不足

爲什麼不適用簡單類型 + 類型類模式

  • 額外時間編寫隱式代碼
  • 編譯會花費額外時間
  • 運行開銷,隱式代碼本質是反射
  • 隱式特徵與其餘 Scala 特徵,尤爲是子類型特徵發生交集時,會產生一些技術問題 [scala-debate email 郵件組]
trait Stringizer[+T] {
def stringize: String
}
implicit class AnyStringizer(a: Any) extends Stringizer[Any] {
def stringize: String = a match {
case s: String => s
case i: Int => (i*10).toString
case f: Float => (f*10.1).toString
case other =>
throw new UnsupportedOperationException(s"Can't stringize $other")
}
}
val list: List[Any] = List(1, 2.2F, "three", 'symbol)
list foreach { (x:Any) =>
try {
println(s"$x: ${x.stringize}")
} catch {
case e: java.lang.UnsupportedOperationException => println(e)
}
}

咱們定義了一個名爲 Stringizer 的抽象體。若是按照以前 ToJSON 示例的作法,咱們會爲全部咱們但願能字符串化的類型建立隱式類。這自己就是一個問題。若是咱們但願處理一組不一樣的類型實例,咱們只能在 list 類型的 map 方法內隱式地傳入一個 Stringizer 實例。
所以,咱們就必須定義一個 AnyStringerize 類,該類知道如何對咱們已知的全部類型進行處理。這些類型甚至還包含用於拋出異常的 default 子句。這種實現方式很是不美觀,同時也違背了面向對象編程中的一條核心規則——你不該該使用 switch 語句對可能發生變化的類型進行判斷。相反,你應該利用多態分發任務,這相似於 toString 方法在 Scala 和 Java 語言中的運做方式。

隱式使用注意

  • 不管什麼時候都要爲隱式轉換方法指定返回類型。不然,類型推導推斷出的返回類型可能會致使預料以外的結果。
  • 雖然編譯器會執行一些「方便」用戶的轉換。可是目前來看這些轉換帶來的麻煩多過益處(之後推出的 Scala 也許會修改這些行爲)。

例如:
假如你爲某一類型定義方法 + ,並試圖將該方法應用到某一不屬於該類型的實例上,
那麼編譯器會調用該實例的 toString 方法,這樣一來便能執行 String 類型的 + 操做(合
並字符串操做)。這能夠解釋某些特定狀況下出現像 String 是錯誤類型的奇怪錯誤

與此同時,若是有必要的話,編譯器會將方法的輸入參數自動組合成一個元組。有時候這
一行爲會給人形成困擾。幸運的是,Scala 2.11 如今會拋出警告信息。

scala> def m(pair:Tuple2[Int,String]) = println(pair)
scala> m(1, "two")
<console>:9: warning: Adapting argument list by creating a 2-tuple:
this may not be what you want.
signature: m(pair: (Int, String)): Unit
given arguments: 1, "two"
after adaptation: m((1, "two"): (Int, String))
m(1,"two")

隱式解析規則

  • Scala 會解析無須輸入前綴路徑的類型兼容隱式值。換句話說,隱式值定義在相同做用域中。例如:隱式值定義在相同代碼塊中,隱式值定義在相同類型中,隱式值定義在伴生對象中(若是存在的話),或者定義在父類型中。
  • Scala 會解析那些導入到當前做用域的隱式值(這也無須輸入前綴路徑)。

  • 隱式類 Scala匹配。 將挑選匹配度最高的隱式。舉個例子,若是隱式參數類型是 Foo 類型,而當前做用域中既存在 Foo 類型的隱式值又存在AnyRef 類型的隱式值,那麼 Scala 會挑選類型爲 Foo 的隱式值。

  • 隱式值匹配。兩個或多個隱式值可能引起歧義(例如:它們具備相同的類型),編譯錯誤會被觸發。
相關文章
相關標籤/搜索