- 除了在
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í)行孵滞。
- 編譯器發(fā)現(xiàn)我們試圖對
如果希望執(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)換咖杂,我們可以為任何類型添加toJSON
和toXML
方法。如果對象中未定義toString
方法蚊夫,我們也能通過隱式轉(zhuǎn)換定義該方法诉字。[P135] - 代碼示例見書 P135 - 136
- Scala 不允許同時使用
implicit
和case
,因?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]