隱式轉(zhuǎn)換和隱式參數(shù)
- 如果使用別人的代碼庫,無法進(jìn)行修改,Scala進(jìn)行擴(kuò)展的方法是隱式轉(zhuǎn)換和隱式參數(shù)。允許省略掉冗余且明顯的細(xì)節(jié)。
隱式轉(zhuǎn)換
- 隱式轉(zhuǎn)換通常在兩個(gè)開發(fā)完全不知道對方存在的軟件或類庫時(shí)非常有用。如果雙方都描述了同一樣概念,隱式轉(zhuǎn)換可以減少從一個(gè)類型顯式轉(zhuǎn)換成另一個(gè)類型的需要。
隱式規(guī)則
- 隱式定義指的是那些我們允許編譯器插入程序以解決類型錯誤的定義。如果
x + y不能通過編譯,編譯器可能會改為convert(x) + y,其中convert是某種可用的隱式轉(zhuǎn)換,如果convert(x)的對象支持+動作,那么這個(gè)改動就可能修復(fù)程序,讓它通過類型檢查并正確運(yùn)行。如果convert真的是某種簡單的轉(zhuǎn)換函數(shù),可以不顯式地寫出這個(gè)方法。
- 隱式定義指的是那些我們允許編譯器插入程序以解決類型錯誤的定義。如果
-
- 隱式轉(zhuǎn)換受如下規(guī)則的約束:
- 標(biāo)記規(guī)則:只有標(biāo)記為
implicit的定義才可用。implicit用來標(biāo)記哪些聲明可被編譯器用作隱式定義。編譯器只會從那些顯式標(biāo)記為implicit的定義中選擇。
- 標(biāo)記規(guī)則:只有標(biāo)記為
-
- 作用域規(guī)則:被插入的隱式轉(zhuǎn)換必須是當(dāng)前作用域的單個(gè)標(biāo)識符,而且跟隱式轉(zhuǎn)換的源類型或者目標(biāo)類型有關(guān)聯(lián)。必須將隱式轉(zhuǎn)換引入到當(dāng)前作用域才能使得它們可用,例如,編譯器不會引入
varaiable.convert這種語法,必須要引入它,使其成為單個(gè)標(biāo)識符。
- 2.1. 編譯器會在隱式轉(zhuǎn)換的源類型或目標(biāo)類型的伴生對象中查找隱式定義。例如嘗試將
Dollar對象傳遞給一個(gè)接收Euro的函數(shù),編譯器會在Dollar和Euro的伴生對象中尋找隱式轉(zhuǎn)換。在伴生對象中定義的隱式轉(zhuǎn)換可以不引入直接使用。就是隱式定義是在當(dāng)前作用域有效的,而不是全部有效。
- 作用域規(guī)則:被插入的隱式轉(zhuǎn)換必須是當(dāng)前作用域的單個(gè)標(biāo)識符,而且跟隱式轉(zhuǎn)換的源類型或者目標(biāo)類型有關(guān)聯(lián)。必須將隱式轉(zhuǎn)換引入到當(dāng)前作用域才能使得它們可用,例如,編譯器不會引入
- 每次一個(gè)規(guī)則:每次只能有一個(gè)隱式定義被插入。
- 顯式優(yōu)先原則:只要代碼按照編寫的樣子能夠通過類型檢查,就不會啟動隱式轉(zhuǎn)換。
- 命名一個(gè)隱式轉(zhuǎn)換:隱式轉(zhuǎn)換可以使用任何名稱,名稱并不重要,只有在顯式引入該轉(zhuǎn)換的時(shí)候使用以及決定在程序的某個(gè)位置都有哪些隱式轉(zhuǎn)換可用時(shí)用到。例如,在一個(gè)對象中有兩個(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è)對象中有兩個(gè)隱式轉(zhuǎn)換,
- 4.嘗試隱式轉(zhuǎn)換的地方:1.轉(zhuǎn)換到一個(gè)預(yù)期的類型;2.對某個(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)換的引入需要在使用之前,不然編譯器不會發(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)換需要放在定義語句之前。doubleToInt是單個(gè)標(biāo)識符,如果不是定義在當(dāng)前作用域中,可以使用import或者extend或者with特質(zhì)來導(dǎo)入。類似Double轉(zhuǎn)向Int這種通用類型轉(zhuǎn)向受限類型的轉(zhuǎn)換會丟失精度,但是反方向的轉(zhuǎn)換是定義得通的,需要自己定義。在Predef中有從Int到Double的轉(zhuǎn)換。
B. 轉(zhuǎn)換接收端
隱式轉(zhuǎn)換還能應(yīng)用關(guān)于方法調(diào)用的接收端,也就是方法被調(diào)用的那個(gè)對象。
-
這個(gè)隱式轉(zhuǎn)換主要有兩種用途,1.接收端轉(zhuǎn)換允許我們更平滑地將新類繼承到已有的類繼承關(guān)系圖譜中,2.支持在語言中編寫領(lǐng)域特定語言
(DSL)。假如寫下obj.doIt,但是obj中并不存在名為doIt的成員,編譯器會在放棄之前嘗試插入轉(zhuǎn)換。在本例中,這個(gè)轉(zhuǎn)換需要應(yīng)用于接收端,也就是obj,編譯器會尋找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類型中沒有+(rational: Rational)這個(gè)方法。
2. 模擬新的語法
- 隱式轉(zhuǎn)換的另一個(gè)用途是模擬添加新的語法,例如
Map中的語法:Map(1 -> "1"),整體的操作過程是這樣的。編譯器插入any2ArrowAssoc的轉(zhuǎn)換,將1轉(zhuǎn)換為帶有->方法的ArrowAssoc的對象,從而可以調(diào)用->,這看起來就像是一個(gè)新的語法。如果某個(gè)對象調(diào)用了不屬于自己的方法,那么很有可能是使用了隱式轉(zhuǎn)換??梢允褂眠@些富包裝類模式做出以類庫形式定義的內(nèi)部DSL。
3. 隱式類
-
Scala 2.10引入了隱式類來簡化富包裝類的編寫。隱式類是以implicit打頭的類,對于這樣的類,編譯器會生成一個(gè)從類的構(gòu)造方法參數(shù)到類本身的隱式轉(zhuǎn)換。例如,
case class Rectangle(width: Int, height: Int)如果經(jīng)常使用這個(gè)類,可以使用富包裝類來簡化構(gòu)造工作,定義:
implicit class RectangleMaker(width: Int) { def x(height: Int) = Rectangle(width, height) } // Automatically generated implicit def RectangleMaker(width: Int) = new RectangleMaker(width)對于以上的隱式類,會自動生成類構(gòu)造參數(shù)到該類對象的一個(gè)轉(zhuǎn)換??梢灾苯邮褂?code>3 x 4這樣的形式。
scala> val myRectangle = 3 x 4 myRectangle: Rectangle = Rectangle(3,4)并不是任何類定義前面度可以放
implicit,隱式類不能是樣例類,并且其構(gòu)造方法必須有且僅有一個(gè)參數(shù)。隱式類必須存在于一個(gè)對象、類或者特質(zhì)里面。 - 接收端轉(zhuǎn)換的一個(gè)主要用途是讓新類型和已有類型的集成更順滑。可以讓客戶端的代碼不改變,就像是在使用新類型一樣。
C. 隱式參數(shù)
- 編譯器會插入隱式定義的最后一個(gè)地方是參數(shù)列表,編譯器有時(shí)候會將
someCall(a)替換為someCall(a)(b),通過追加一個(gè)參數(shù)列表來完成某個(gè)函數(shù)的調(diào)用。隱式參數(shù)提供的是整個(gè)最后一組currying的參數(shù)列表,而不僅僅是最后一個(gè)參數(shù)。例如,someCall(a)可能根據(jù)實(shí)際情況被替換為someCall(a)(b, c, d)。如果要讓編譯器隱式地填充隱式參數(shù),首先需要定義這樣一個(gè)符合預(yù)期類型的變量。填充的變量也必須聲明為implicit的,如果不是這樣,編譯器不會使用它來填充缺失的列表。如果變量不是當(dāng)前作用域內(nèi)的單個(gè)標(biāo)識符,也不會被采納。implicit關(guān)鍵字是應(yīng)用到整個(gè)參數(shù)列表而不是單個(gè)參數(shù)的。def greet(name: String)(implicit prompt: PreferredPrompt, drink: PreferredDrink) = {} - 由于編譯器是在作用域內(nèi)通過類型匹配來填充隱式參數(shù),所以一般希望類型能夠特殊從而避免誤匹配的出現(xiàn)。隱式參數(shù)最常使用的場景是提供關(guān)于在更靠前的參數(shù)列表中已經(jīng)顯式提到的類的信息。
隱式參數(shù)def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): Tordering說明了已知類型T更多的信息。有了T,ordering的類型就已知,可以使用隱式參數(shù)隱式插入。
- 隱式參數(shù)的代碼風(fēng)格,最好是對隱式參數(shù)使用定制名稱的類型,比如
PreferredPrompt和PreferredDrink,而不是String和String,而且這個(gè)類型命名時(shí),至少使用一個(gè)能明確其智能的名字,比如Ordering。如果定義成為implicit (T,T) => T,該參數(shù)并沒有透露出任何關(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, //可以使用庫函數(shù) > implicit[Ordering[T]],該函數(shù)返回的是Ordering[T]的隱式對象。 //這樣ordering就可以隨意命名。 }Scala允許聲調(diào)這個(gè)參數(shù)的名稱并使用上下文界定來縮短方法簽名。[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會拒絕插入,隱式轉(zhuǎn)換在這個(gè)轉(zhuǎ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í)候可以顯示地寫出來轉(zhuǎn)換用以明確編譯器插入的轉(zhuǎn)換到底是哪一個(gè)。也可以對編譯器設(shè)置選項(xiàng)
-Xprint:typer,使用這個(gè)選項(xiàng)來運(yùn)行scalac,可以看到添加了隱式轉(zhuǎn)換之后的代碼。使用的方法:scalac -Xprint:typer test.scala。隱式轉(zhuǎn)換還是不能濫用的。