隱式轉(zhuǎn)換和隱式參數(shù)
- 如果使用別人的代碼庫(kù)华匾,無(wú)法進(jìn)行修改,Scala進(jìn)行擴(kuò)展的方法是隱式轉(zhuǎn)換和隱式參數(shù)。允許省略掉冗余且明顯的細(xì)節(jié)。
隱式轉(zhuǎn)換
- 隱式轉(zhuǎn)換通常在兩個(gè)開(kāi)發(fā)完全不知道對(duì)方存在的軟件或類庫(kù)時(shí)非常有用肉盹。如果雙方都描述了同一樣概念,隱式轉(zhuǎn)換可以減少?gòu)囊粋€(gè)類型顯式轉(zhuǎn)換成另一個(gè)類型的需要疹尾。
隱式規(guī)則
- 隱式定義指的是那些我們?cè)试S編譯器插入程序以解決類型錯(cuò)誤的定義上忍。如果
x + y
不能通過(guò)編譯,編譯器可能會(huì)改為convert(x) + y
纳本,其中convert
是某種可用的隱式轉(zhuǎn)換窍蓝,如果convert(x)
的對(duì)象支持+
動(dòng)作,那么這個(gè)改動(dòng)就可能修復(fù)程序繁成,讓它通過(guò)類型檢查并正確運(yùn)行吓笙。如果convert
真的是某種簡(jiǎn)單的轉(zhuǎn)換函數(shù),可以不顯式地寫出這個(gè)方法巾腕。
- 隱式定義指的是那些我們?cè)试S編譯器插入程序以解決類型錯(cuò)誤的定義上忍。如果
-
- 隱式轉(zhuǎn)換受如下規(guī)則的約束:
- 標(biāo)記規(guī)則:只有標(biāo)記為
implicit
的定義才可用面睛。implicit
用來(lái)標(biāo)記哪些聲明可被編譯器用作隱式定義。編譯器只會(huì)從那些顯式標(biāo)記為implicit
的定義中選擇祠墅。
- 標(biāo)記規(guī)則:只有標(biāo)記為
-
- 作用域規(guī)則:被插入的隱式轉(zhuǎn)換必須是當(dāng)前作用域的單個(gè)標(biāo)識(shí)符侮穿,而且跟隱式轉(zhuǎn)換的源類型或者目標(biāo)類型有關(guān)聯(lián)歌径。必須將隱式轉(zhuǎn)換引入到當(dāng)前作用域才能使得它們可用毁嗦,例如,編譯器不會(huì)引入
varaiable.convert
這種語(yǔ)法回铛,必須要引入它狗准,使其成為單個(gè)標(biāo)識(shí)符克锣。
- 2.1. 編譯器會(huì)在隱式轉(zhuǎn)換的源類型或目標(biāo)類型的伴生對(duì)象中查找隱式定義。例如嘗試將
Dollar
對(duì)象傳遞給一個(gè)接收Euro
的函數(shù)腔长,編譯器會(huì)在Dollar
和Euro
的伴生對(duì)象中尋找隱式轉(zhuǎn)換袭祟。在伴生對(duì)象中定義的隱式轉(zhuǎn)換可以不引入直接使用。就是隱式定義是在當(dāng)前作用域有效的捞附,而不是全部有效巾乳。
- 作用域規(guī)則:被插入的隱式轉(zhuǎn)換必須是當(dāng)前作用域的單個(gè)標(biāo)識(shí)符侮穿,而且跟隱式轉(zhuǎn)換的源類型或者目標(biāo)類型有關(guān)聯(lián)歌径。必須將隱式轉(zhuǎn)換引入到當(dāng)前作用域才能使得它們可用毁嗦,例如,編譯器不會(huì)引入
- 每次一個(gè)規(guī)則:每次只能有一個(gè)隱式定義被插入。
- 顯式優(yōu)先原則:只要代碼按照編寫的樣子能夠通過(guò)類型檢查鸟召,就不會(huì)啟動(dòng)隱式轉(zhuǎn)換让禀。
- 命名一個(gè)隱式轉(zhuǎn)換:隱式轉(zhuǎn)換可以使用任何名稱拂铡,名稱并不重要,只有在顯式引入該轉(zhuǎn)換的時(shí)候使用以及決定在程序的某個(gè)位置都有哪些隱式轉(zhuǎn)換可用時(shí)用到。例如弊添,在一個(gè)對(duì)象中有兩個(gè)隱式轉(zhuǎn)換,
intToString
裙顽,StringToStringWrapper
蛙婴,如果只想將String
轉(zhuǎn)換為StringWrapper
,而不想將Int
轉(zhuǎn)換為String
舔糖,可以只引入第二個(gè)隱式轉(zhuǎn)換娱两。
- 命名一個(gè)隱式轉(zhuǎn)換:隱式轉(zhuǎn)換可以使用任何名稱拂铡,名稱并不重要,只有在顯式引入該轉(zhuǎn)換的時(shí)候使用以及決定在程序的某個(gè)位置都有哪些隱式轉(zhuǎn)換可用時(shí)用到。例如弊添,在一個(gè)對(duì)象中有兩個(gè)隱式轉(zhuǎn)換,
- 4.嘗試隱式轉(zhuǎn)換的地方:1.轉(zhuǎn)換到一個(gè)預(yù)期的類型;2.對(duì)某個(gè)選擇接收端的轉(zhuǎn)換金吗;3.隱式參數(shù)
A. 隱式轉(zhuǎn)換到一個(gè)預(yù)期的類型
- 當(dāng)編譯器發(fā)現(xiàn)
X
而需要Y
的時(shí)候谷婆,查找能夠?qū)?code>X轉(zhuǎn)換為Y
的隱式轉(zhuǎn)換。注意隱式轉(zhuǎn)換的引入需要在使用之前辽聊,不然編譯器不會(huì)發(fā)現(xiàn)這個(gè)隱式轉(zhuǎn)換纪挎。scala> val i: Int = 3.5 <console>:7: error: type mismatch; scala> implicit def doubleToInt(x: Double) = x.toInt doubleToInt: (x: Double)Int //這個(gè)隱式轉(zhuǎn)換需要放在定義語(yǔ)句之前。
doubleToInt
是單個(gè)標(biāo)識(shí)符跟匆,如果不是定義在當(dāng)前作用域中异袄,可以使用import
或者extend
或者with
特質(zhì)來(lái)導(dǎo)入。類似Double
轉(zhuǎn)向Int
這種通用類型轉(zhuǎn)向受限類型的轉(zhuǎn)換會(huì)丟失精度玛臂,但是反方向的轉(zhuǎn)換是定義得通的烤蜕,需要自己定義。在Predef
中有從Int
到Double
的轉(zhuǎn)換迹冤。
B. 轉(zhuǎn)換接收端
隱式轉(zhuǎn)換還能應(yīng)用關(guān)于方法調(diào)用的接收端讽营,也就是方法被調(diào)用的那個(gè)對(duì)象。
-
這個(gè)隱式轉(zhuǎn)換主要有兩種用途泡徙,1.接收端轉(zhuǎn)換允許我們更平滑地將新類繼承到已有的類繼承關(guān)系圖譜中橱鹏,2.支持在語(yǔ)言中編寫領(lǐng)域特定語(yǔ)言
(DSL)
。假如寫下obj.doIt
,但是obj
中并不存在名為doIt
的成員莉兰,編譯器會(huì)在放棄之前嘗試插入轉(zhuǎn)換挑围。在本例中,這個(gè)轉(zhuǎn)換需要應(yīng)用于接收端糖荒,也就是obj
杉辙,編譯器會(huì)尋找obj
到預(yù)期類型的轉(zhuǎn)換,這個(gè)預(yù)期類型中擁有名為doIt
的成員捶朵。1. 與新類型互操作
- 接收端轉(zhuǎn)換的一個(gè)主要用途是讓新類型和已有類型的集成更順滑蜘矢。可以讓客戶端的代碼不改變综看,就像是在使用新類型一樣硼端。
1 + new Rational(3,4)
可插入一個(gè)隱式轉(zhuǎn)換,最后變成intToRational(1) + new Rational(3,4)
寓搬,完美解決Int
類型中沒(méi)有+(rational: Rational)
這個(gè)方法珍昨。
2. 模擬新的語(yǔ)法
- 隱式轉(zhuǎn)換的另一個(gè)用途是模擬添加新的語(yǔ)法,例如
Map
中的語(yǔ)法:Map(1 -> "1")
句喷,整體的操作過(guò)程是這樣的镣典。編譯器插入any2ArrowAssoc
的轉(zhuǎn)換,將1
轉(zhuǎn)換為帶有->
方法的ArrowAssoc
的對(duì)象唾琼,從而可以調(diào)用->
兄春,這看起來(lái)就像是一個(gè)新的語(yǔ)法。如果某個(gè)對(duì)象調(diào)用了不屬于自己的方法锡溯,那么很有可能是使用了隱式轉(zhuǎn)換赶舆。可以使用這些富包裝類模式做出以類庫(kù)形式定義的內(nèi)部DSL
祭饭。
3. 隱式類
-
Scala 2.10
引入了隱式類來(lái)簡(jiǎn)化富包裝類的編寫芜茵。隱式類是以implicit
打頭的類,對(duì)于這樣的類倡蝙,編譯器會(huì)生成一個(gè)從類的構(gòu)造方法參數(shù)到類本身的隱式轉(zhuǎn)換九串。例如,
case class Rectangle(width: Int, height: Int)
如果經(jīng)常使用這個(gè)類寺鸥,可以使用富包裝類來(lái)簡(jiǎn)化構(gòu)造工作猪钮,定義:
implicit class RectangleMaker(width: Int) { def x(height: Int) = Rectangle(width, height) } // Automatically generated implicit def RectangleMaker(width: Int) = new RectangleMaker(width)
對(duì)于以上的隱式類,會(huì)自動(dòng)生成類構(gòu)造參數(shù)到該類對(duì)象的一個(gè)轉(zhuǎn)換胆建】镜停可以直接使用
3 x 4
這樣的形式。scala> val myRectangle = 3 x 4 myRectangle: Rectangle = Rectangle(3,4)
并不是任何類定義前面度可以放
implicit
笆载,隱式類不能是樣例類扑馁,并且其構(gòu)造方法必須有且僅有一個(gè)參數(shù)涯呻。隱式類必須存在于一個(gè)對(duì)象、類或者特質(zhì)里面檐蚜。 - 接收端轉(zhuǎn)換的一個(gè)主要用途是讓新類型和已有類型的集成更順滑蜘矢。可以讓客戶端的代碼不改變综看,就像是在使用新類型一樣硼端。
C. 隱式參數(shù)
- 編譯器會(huì)插入隱式定義的最后一個(gè)地方是參數(shù)列表魄懂,編譯器有時(shí)候會(huì)將
someCall(a)
替換為someCall(a)(b)
沿侈,通過(guò)追加一個(gè)參數(shù)列表來(lái)完成某個(gè)函數(shù)的調(diào)用闯第。隱式參數(shù)提供的是整個(gè)最后一組currying
的參數(shù)列表,而不僅僅是最后一個(gè)參數(shù)缀拭。例如咳短,someCall(a)
可能根據(jù)實(shí)際情況被替換為someCall(a)(b, c, d)
。如果要讓編譯器隱式地填充隱式參數(shù)蛛淋,首先需要定義這樣一個(gè)符合預(yù)期類型的變量咙好。填充的變量也必須聲明為implicit
的,如果不是這樣褐荷,編譯器不會(huì)使用它來(lái)填充缺失的列表勾效。如果變量不是當(dāng)前作用域內(nèi)的單個(gè)標(biāo)識(shí)符,也不會(huì)被采納叛甫。implicit
關(guān)鍵字是應(yīng)用到整個(gè)參數(shù)列表而不是單個(gè)參數(shù)的层宫。def greet(name: String)(implicit prompt: PreferredPrompt, drink: PreferredDrink) = {}
- 由于編譯器是在作用域內(nèi)通過(guò)類型匹配來(lái)填充隱式參數(shù),所以一般希望類型能夠特殊從而避免誤匹配的出現(xiàn)其监。隱式參數(shù)最常使用的場(chǎng)景是提供關(guān)于在更靠前的參數(shù)列表中已經(jīng)顯式提到的類的信息萌腿。
def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T
ordering
說(shuō)明了已知類型T
更多的信息。有了T
抖苦,ordering
的類型就已知毁菱,可以使用隱式參數(shù)隱式插入。
- 隱式參數(shù)的代碼風(fēng)格锌历,最好是對(duì)隱式參數(shù)使用定制名稱的類型贮庞,比如
PreferredPrompt
和PreferredDrink
,而不是String
和String
究西,而且這個(gè)類型命名時(shí)贸伐,至少使用一個(gè)能明確其智能的名字,比如Ordering
怔揩。如果定義成為implicit (T,T) => T
捉邢,該參數(shù)并沒(méi)有透露出任何關(guān)于該烈性用途的信息,范圍太廣商膊,很容易誤使用伏伐。
上下文界定
- 當(dāng)一個(gè)參數(shù)是隱式定義的時(shí)候,在函數(shù)體內(nèi)又使用了這個(gè)隱式參數(shù)作為參數(shù)傳遞晕拆,這時(shí)可以將不用寫這個(gè)參數(shù)藐翎。
def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = { val maxRest = maxListOrdering(rest)(ordering) //這個(gè)ordering可以省略 } def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = { if (ordering.gt(x, maxRest)) x else maxRest //為了避免使用ordering材蹬, //可以使用庫(kù)函數(shù) > implicit[Ordering[T]],該函數(shù)返回的是Ordering[T]的隱式對(duì)象吝镣。 //這樣ordering就可以隨意命名堤器。 }
Scala
允許聲調(diào)這個(gè)參數(shù)的名稱并使用上下文界定來(lái)縮短方法簽名末贾。[T: Ordering]
是一個(gè)上下文界定闸溃,context bound
,做了兩件事情拱撵。1. 引入類型參數(shù)T
辉川,2.添加一個(gè)Ordering[T]
的隱式參數(shù),并不需要知道這個(gè)參數(shù)的名字拴测。最后的代碼如下:def maxList[T : Ordering](elements: List[T]): T = elements match { case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) if (implicitly[Ordering[T]].gt(x, maxRest)) x else maxRest }
當(dāng)有多個(gè)轉(zhuǎn)換可選時(shí)
- 如果有多個(gè)轉(zhuǎn)換可選乓旗,
Scala
會(huì)拒絕插入,隱式轉(zhuǎn)換在這個(gè)轉(zhuǎn)換是顯而易見(jiàn)的并且純粹是樣板代碼的時(shí)候最好用集索。Scala
目前采取的措施是使用可用轉(zhuǎn)換中更具體的轉(zhuǎn)換屿愚,具體體現(xiàn)在,該轉(zhuǎn)換的入?yún)㈩愋褪莿e的轉(zhuǎn)換的子類型务荆;兩者都是方法妆距,具體轉(zhuǎn)換所在的類擴(kuò)展自通用轉(zhuǎn)換所在的類。
調(diào)試
- 調(diào)試的時(shí)候可以顯示地寫出來(lái)轉(zhuǎn)換用以明確編譯器插入的轉(zhuǎn)換到底是哪一個(gè)蛹含。也可以對(duì)編譯器設(shè)置選項(xiàng)
-Xprint:typer
毅厚,使用這個(gè)選項(xiàng)來(lái)運(yùn)行scalac
,可以看到添加了隱式轉(zhuǎn)換之后的代碼浦箱。使用的方法:scalac -Xprint:typer test.scala
吸耿。隱式轉(zhuǎn)換還是不能濫用的。