《Scala 程序設(shè)計(jì)》學(xué)習(xí)筆記 Chapter 5:隱式詳解

  • 除了在 Predef 對象中自動加載的那些隱式對象外,其他在源碼中出現(xiàn)的隱式對象均不是本地對象兽赁。[P112]

隱式參數(shù)

  • 在方法中使用 implicit 關(guān)鍵字標(biāo)記隱式參數(shù)。當(dāng)調(diào)用方法時未輸入隱式參數(shù)時驻粟,如果代碼所處的作用域存在類型兼容值友扰,類型兼容值會從作用域中調(diào)出并被使用,反之赚爵,系統(tǒng)會拋出錯誤棉胀。[P112]

    def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
    
    implicit val currentTaxRate = 0.08F
    ...
    val tax = calcTax(50000F)
    
  • 可以使用隱式方法動態(tài)計(jì)算隱式參數(shù)的值。隱式方法聲明在隱式對象中冀膝,隱式對象不具備任何參數(shù)唁奢,除非該參數(shù)同樣被標(biāo)示為隱式參數(shù)。[P113]

    case class ComplicatedSalesTaxData(
        baseRate: Float,
        isTaxHoliday: Boolean,
        storeId: Int)
    object ComplicatedSalesTax {
        private def extraTexRateForStore(id: Int): Float = {
            ...
        }
        implicit def rate(implicit cstd: ComplicatedSalesTaxData): Float = {
            if (cstd.isTaxHoliday) 0.0F
            else cstd.baseRate + extraTaxRateForStore(cstd.storeId)
        }
    }
    
    import ComplicatedSalesTax.rate
    implicit val myStore = ComplicatedSalesTaxData(0.06F, false, 1010)
    println(s"Tax on $amount = ${calcTax(amount)}")
    
  • 可以定義包含隱式參數(shù)的隱式方法窝剖。[P113]

  • 調(diào)用 implicitly 方法:[P114 - 原書有錯誤]

    • Predef 對象中定義了一個名為 implicitly 的方法麻掸。如果將 implicitly 方法與附加類型簽名( type signature addition )相結(jié)合,便能以一種有用且快捷的方式定義一個接收參數(shù)化類型隱式參數(shù)的函數(shù)赐纱。

      import math.Ordering
      case class MyList[A](list: List[A]) {
          def sortBy1[B](f: A => B)(implicit ord: Ordering[B]): List[A] = {
              list.sortBy(f)(ord)
          }
          def sortBy2[B: Ordering](f: A => B): List[A] = { // [B: Ordering] 的作用是限制 B 為 Ordering 的子類脊奋。
              list.sortBy(f)(implicitly[Ordering[B]])
          }
      }
      
      • 在上述代碼中熬北, sortBy1 接收一個額外的類型為 Ordering[B] 的隱式值作為其輸入。調(diào)用 sortBy1 方法時诚隙,在當(dāng)前作用域一定存在某一 Ordering[B] 的對象實(shí)例讶隐,該實(shí)例清楚地知道如何對我們所需要的 B 類型對象進(jìn)行排序。
      • Scala 為這種普遍的操作提供了一種簡化的方式久又,正如 sortBy2 使用的語法那樣:類型參數(shù) B 被稱為上下文定界( context bound )巫延,它暗指第二個參數(shù)列表(隱式參數(shù)列表)將接受 Ordering[B] 實(shí)例。implicitly 方法會對傳給函數(shù)的所有標(biāo)記為隱式參數(shù)的實(shí)例進(jìn)行解析地消。
    • 當(dāng)需要類型為參數(shù)化類型的隱式參數(shù)時烈评,當(dāng)類型參數(shù)屬于當(dāng)前作用域的其他一些類型時(例如 [B: Ordering] 代表了類型為 Ordering[B] 的隱式參數(shù) ),可以將上下文定界與 implicitly 方法結(jié)合起來犯建,以簡潔的方式解決這個問題讲冠。

隱式參數(shù)的適用場景

  • 通過隱式參數(shù)實(shí)現(xiàn)的常見習(xí)語( idiom ):[P115]
    • 能夠消除樣板代碼
    • 通過引入約束來減少 bug 數(shù)量以及使用參數(shù)化類型對某些方法允許的輸入?yún)?shù)類型進(jìn)行限定。

執(zhí)行上下文

  • 建議使用隱式參數(shù)傳入執(zhí)行上下文适瓦。另外竿开,編寫事務(wù)、數(shù)據(jù)庫連接玻熙、線程池以及用戶會話時隱式參數(shù)上下文也同樣適合使用否彩。[P115]

功能控制

  • 用隱含參數(shù)控制系統(tǒng)功能 [P115 - 116]
    • 引入授權(quán)令牌,控制某些特定的 API 操作只能供某些用戶調(diào)用嗦随,或者決定數(shù)據(jù)可見性等列荔。

    • 可以使用隱式用戶會話參數(shù)包含這類令牌信息。

      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)
      }
      

限定可用實(shí)例

  • 對具有參數(shù)化類型方法中的類型參數(shù)進(jìn)行限定枚尼,使該參數(shù)只接受某些類型的輸入贴浙。[P116]
  • 應(yīng)用 Scala API :[P116 - 117]
    • 將一個“構(gòu)造器”( builder )作為隱式參數(shù)傳入到 map 方法中。該構(gòu)造器知道如何構(gòu)造一個同種類型的新容器署恍。參考 TraversableLike

      trait TraversableLike[+A, +Repr] extends ... {
          ...
          def map[B, That] (f: A => B)(
              implicit bf: CanBuildFrom[Repr, B, That]): That = { ... }
          ...
      }
      

      map 操作符可以輸出的集合類型是由當(dāng)前存在的對應(yīng)的 CanBuildFrom 構(gòu)造器 實(shí)例所決定的崎溃,而這些構(gòu)造器在當(dāng)前作用域被聲明為隱式對象。
      ?

隱式證據(jù)

  • 只需要限定允許的類型盯质,不需要提供額外的處理袁串;這些類型無需繼承某一個共有的超類。[P120 - 121]
  • 一個例子:TraversableOnce [P121]
    • <:< 類型(定義在 Predef 中):用于限定類型參數(shù)呼巷。
      • <:<[A, B] 等價于 A <:< B
  • Predef 還定義了一個名為 =:= 的“證據(jù)”類型囱修,用于證明兩個類型之間的等價關(guān)系。[P122]

繞開類型擦除帶來的限制

  • 通過使用隱式對象提供的“證據(jù)”證明輸入滿足某些特定的類型約束王悍。[P122]

    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")
        }
    }
    

    (不推薦使用常用類型的隱式值破镰,因?yàn)槌S妙愋涂赡軙诙嗵幎x其對應(yīng)的隱式對象。)

改善報錯信息

  • 查看一下 scala.annotation.implicitNotFound 的 annotation 。[P124]

虛類型

  • 定義好的啤咽、沒有任何實(shí)例的類型被稱為虛類型。(只關(guān)心類型本身渠脉,而不會使用類型中的任何實(shí)例宇整。)?[P124 - 125]

  • 可插入字符串:println(f"args: ${args}%.2f" [P126]

  • 管道操作符:|> [P127]

    val pay1 = Payroll start e
    val pay2 = Payroll minus401k pay1
    val pay3 = Payroll minusInsurance pay2
    val pay4 = Payroll minusTax pay3
    val pay = Payroll minusFinalDeductions pay4
    

    等價于

    import Payroll._
    val pay = start(e) |>
        minus401k |>
        minusInsurance |>
        minusTax |>
        minusFinalDeductions
    

    其實(shí)質(zhì)是進(jìn)行了一步轉(zhuǎn)化:pay1 |> Payroll.minus401k => Payroll.minus401k(pay1)

隱式轉(zhuǎn)換

  • Scala 并不知道諸如 a -> b 這樣的語法的意義是什么,它實(shí)際上是運(yùn)用了方法 -> 和一個特殊的 Scala 特性 —— 隱式轉(zhuǎn)換芋膘。同時鳞青,由于 -> 并不是元組的字面量語法,因此 Scala 必須通過某些方式將該表達(dá)式轉(zhuǎn)化為元組 (a, b) 为朋。[P128 - 129]

    // In Predef
    implicit final class ArrowAssoc[A](val self: A) {
        def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
    }
    
  • 觀察編譯器行為(以 "one" -> "1" 為例)?:[P129]

    • 編譯器發(fā)現(xiàn)我們試圖對 String 對象執(zhí)行 -> 方法臂拓;
    • 由于 String 未定義 -> 方法,編譯器將檢查當(dāng)前作用域中是否存在定義了該方法的隱式轉(zhuǎn)換习寸;
    • 編譯器發(fā)現(xiàn)了 ArrowAssoc 類胶惰,創(chuàng)建其對象,并向其傳入 "one" 霞溪;
    • 編譯器解析表達(dá)式中的 -> 1 部分代碼并執(zhí)行孵滞。
  • 如果希望執(zhí)行隱式轉(zhuǎn)換,那么在聲明時必須使用 implicit 關(guān)鍵字鸯匹,能夠執(zhí)行隱式轉(zhuǎn)換的無外乎兩類:構(gòu)造方法中只接受單一參數(shù)的類型或者是只接受單一參數(shù)的方法坊饶。[P129]

  • 從 Scala 2.10 開始,隱式方法變成了 Scala 的可選特性殴蓬。假如希望使用該特性匿级,需要 import scala.language.implicitConversions ;或者使用全局編譯器選項(xiàng) -language:implicitConversions 染厅。[P130]

  • 編譯器進(jìn)行查找和使用轉(zhuǎn)換方法時的查詢規(guī)則:[P130]

    • 假如調(diào)用的對象和方法成功通過了組合類型檢查痘绎,那么類型轉(zhuǎn)換不會被執(zhí)行。
    • 編譯器只會考慮使用了 implicit 關(guān)鍵字的類和方法肖粮。
    • 編譯器只考慮當(dāng)前作用域內(nèi)的隱式類简逮,隱式方法,以及目標(biāo)類型的伴生對象中定義的隱式方法尿赚。
    • 假如當(dāng)前適用多條轉(zhuǎn)換方法散庶,那么將不會執(zhí)行轉(zhuǎn)換操作。編譯器要求有且必須只有一條滿足條件的隱式方法凌净,以免產(chǎn)生二義性悲龟。

構(gòu)建獨(dú)有的字符串插入器

  • 對于字符串,當(dāng)編譯器看到形如 x"..." 這樣的表達(dá)式時冰寻,它會查找 scala.StringContext 中定義的 x 方法须教。s"Hello, ${name}" 可以被轉(zhuǎn)換為 StringContext("Hello, ", "").s(name) 。[?P132]

  • 我們可以通過使用隱式轉(zhuǎn)換為 StringContext 添加新方法,對其進(jìn)行“擴(kuò)展”轻腺。[P132 - 133]

  • 集合中的 zip 方法能很方便的將兩個集合中的值縫合在一起:[P133]

    val keys = List("a", "b", "C")
    val values = List(1, 2, 3)
    val keysValues = keys zip values 
    // List((a, 1), (b, 2), (c, 3))
    

表達(dá)式問題

  • 在不修改源代碼的情況下擴(kuò)展模塊的期望被稱為表達(dá)式問題( expression problem )乐疆。[P134]
  • 使用繼承遇到的問題:不同子類需要的功能可能并不相同,父類可能需要定義許多對于某一個子類多余的功能贬养。[P134]
  • 單一職責(zé)原則( single responsibility principle )[P134]
  • 元編程( metaprogramming ):允許用戶在元編程運(yùn)行環(huán)境下挤土,不修改源代碼就可以修改類。[P134]
  • 通過 Scala 隱式轉(zhuǎn)換误算,我們可以通過靜態(tài)類型實(shí)現(xiàn)元編程的方法仰美,我們稱之為類型類( type class )。[P134]

類型類模式

  • Scala 的 case class 使用的語法比 Java 的 class 要有用得多儿礼。通過使用隱式轉(zhuǎn)換咖杂,我們可以為任何類型添加 toJSONtoXML 方法。如果對象中未定義 toString 方法蚊夫,我們也能通過隱式轉(zhuǎn)換定義該方法诉字。[P135]
  • 代碼示例見書 P135 - 136
  • Scala 不允許同時使用 implicitcase ,因?yàn)?case class 不會執(zhí)行通過隱式所自動生成的額外代碼知纷。[P136]
  • 擴(kuò)展方法( extend method )是類型類的另一用途奏窑。[P136]
  • 類型類提供的多態(tài)叫特設(shè)多態(tài)( ad hoc polymorphism ),其并未綁定類型系統(tǒng)屈扎。 [P137]
  • 三種多態(tài):子類型多態(tài) / 特設(shè)多態(tài) / 參數(shù)化多態(tài) [P137]

隱式所導(dǎo)致的技術(shù)問題

  • 增加代碼量埃唯。[P137]
  • 造成額外的運(yùn)行開銷:封裝類型會引入額外的中間層。[P137]
  • 當(dāng)隱式特征與其他 Scala 特征鹰晨,尤其是子類型特征發(fā)生交集時墨叛,會產(chǎn)生一些技術(shù)問題。[P137 - 138]
  • 如何避免:[P138]
    • 無論何時都要為隱式轉(zhuǎn)換方法指定返回類型模蜡。否則漠趁,類型推導(dǎo)推斷出的返回類型可能會導(dǎo)致預(yù)料之外的結(jié)果。
    • 避免使用編譯器提供的轉(zhuǎn)換忍疾。

隱式解析規(guī)則

  • Scala 會解析無須輸入前綴路徑的類型兼容隱式值闯传。(在相同代碼塊 / 相同類型 / 相同作用域 / 伴生對象中)[P139]
  • Scala 會解析那些導(dǎo)入到當(dāng)前作用域的隱式值,其優(yōu)先級高于已經(jīng)在當(dāng)前作用域的隱式值卤妒。[P139]
  • Scala 會自動選擇類型匹配度最高的隱式甥绿。如果引發(fā)歧義,編譯錯誤會被觸發(fā)则披。[P139]
  • Scala 庫中定義的隱式常常會被編譯器自動加載到作用域中共缕。[P139]

Scala 內(nèi)置的各種隱式

[P139 - 146]

合理使用隱式

  • 可以考慮將隱式值全部放到名為 implicits 的特殊包或?qū)ο笾小P146]
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末士复,一起剝皮案震驚了整個濱河市图谷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖便贵,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菠镇,死亡現(xiàn)場離奇詭異,居然都是意外死亡承璃,警方通過查閱死者的電腦和手機(jī)利耍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绸硕,“玉大人堂竟,你說我怎么就攤上這事魂毁〔E澹” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵席楚,是天一觀的道長咬崔。 經(jīng)常有香客問我,道長烦秩,這世上最難降的妖魔是什么垮斯? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮只祠,結(jié)果婚禮上兜蠕,老公的妹妹穿的比我還像新娘。我一直安慰自己抛寝,他們只是感情好熊杨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盗舰,像睡著了一般晶府。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钻趋,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天川陆,我揣著相機(jī)與錄音,去河邊找鬼蛮位。 笑死较沪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的失仁。 我是一名探鬼主播购对,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陶因!你這毒婦竟也來了骡苞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎解幽,沒想到半個月后贴见,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躲株,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年片部,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霜定。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡档悠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出望浩,到底是詐尸還是另有隱情辖所,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布磨德,位于F島的核電站缘回,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏典挑。R本人自食惡果不足惜酥宴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望您觉。 院中可真熱鬧拙寡,春花似錦、人聲如沸琳水。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炫刷。三九已至擎宝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浑玛,已是汗流浹背绍申。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顾彰,地道東北人极阅。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像涨享,于是被迫代替她去往敵國和親筋搏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容