Scala的字串直接使用 java.lang.String,本身並沒有安插字串至原字串的方法,所以下面的程式會編譯錯誤:
val old = "oz"
val young = old.insert(1, "r") // 編譯錯誤
println(young)
如果你想要有insert()方法可以使用,也許可以考慮使用 java.lang.StringBuilder,例如:
val old = "oz"
val young = new StringBuilder(old).insert(1, "r")
println(young) // orz
這樣的程式看起來較為冗長,如果你想要讓insert()方法看起來更像是字串本身所內建的方法,則可以使用隱式轉換(Implicit conversion),例如:
implicit def stringToBuilder(s: String) = new StringBuilder(s)
val old = "oz"
val young = old.insert(1, "r")
println(young) // orz
上面這段程式可以順利編譯並執行,由於你將stringToBuilder()標示為implicit,它就變成隱式函式。當編譯器在執行程式時,發現old是個字串,本身並沒有insert()方法,它試著找看看名稱空間範圍(Scope)內是否有接受字串型態的函式,如果就套用該函式。也就是說,上面的程式,最後編譯器會將之變為以下:
implicit def stringToBuilder(s: String) = new StringBuilder(s)
val old = "oz"
val young = stringToBuilder(old).insert(1, "r")
println(young)
所以實際上,young變數的型態是java.lang.StringBuilder。事實上,在Scala中,如果你使用字串時,呼叫了java.lang.String所沒有的方法時,卻會有對應的結果,就是套用了隱式轉換,例如:
scala> "12345".foreach(print)
12345
scala> "12345".reverse
res2: scala.runtime.RichString = 54321
scala>
12345
scala> "12345".reverse
res2: scala.runtime.RichString = 54321
scala>
注意到foreach與reverse都不是字串本身具有的方法,reverse所傳回的型態是 scala.runtime.RichString,這是因為在 scala.Predef 中定義了隱式函式stringWrapper()將字串轉為RichString:
package scala
object Predef {
....
implicit def stringWrapper(x: String) = new runtime.RichString(x)
....
}
object Predef {
....
implicit def stringWrapper(x: String) = new runtime.RichString(x)
....
}
由於預設import了scala.Predef(也就是每個程式都相當於有import scala.Predef._),所以上面的例子,相當於:
scala> stringWrapper("12345").foreach(print)
12345
scala> stringWrapper("12345").reverse
res4: scala.runtime.RichString = 54321
scala>
12345
scala> stringWrapper("12345").reverse
res4: scala.runtime.RichString = 54321
scala>
在這邊其實已看到幾個編譯器套用隱式轉換的規則,首先就是必須是被標示為implicit的函式或變數,才有機會被編譯器用於隱式轉換。再來是可套用的隱式轉換,必須在當時名稱空間範圍中,可以獨立識別(Single identifier)使用之函式或變數。舉例來說,下面的程式會編譯失敗:
object ImplicitUtil {
implicit def stringToBuilder(s: String) = new StringBuilder(s)
}
val old = "oz"
val young = old.insert(1, "r") // 編譯錯誤
println(young)
因為在這個程式中,無法以stringToBuilder()來呼叫函式,而必須使用ImplicitUtil.stringToBuilder(),這樣就不算是獨立識別,你可以使用import:
object ImplicitUtil {
implicit def stringToBuilder(s: String) = new StringBuilder(s)
}
import ImplicitUtil._
val old = "oz"
val young = old.insert(1, "r")
println(young) // orz
在 單 例物件 中看過伴侶物件(Companion object),編譯器也會搜尋轉換來源型態的伴侶物件,看看其中是否有定義符合的隱式轉換函式或變數,這是唯一不用符合獨立識別原則的情況。例如:
object Some {
implicit def someToOther(s: Some) = new Other(s)
}
class Some
class Other(s: Some) {
def doOther = "XD"
}
val s = new Some
println(s.doOther) // XD
如果在範圍內,你定義的隱式轉換來源型態與目的地型態,與另一個隱式轉換函式定義重複,那麼編譯器會不知道要套用哪個隱式轉換,因而發生編譯錯誤。例如:
implicit def stringToBuilder(s: String) = new runtime.RichString(s)
"12345".foreach(print) // 編譯錯誤
編譯這個程式時,會發生以下的錯誤:
error: type mismatch;
found : java.lang.String
required: ?{val foreach: ?}
Note that implicit conversions are not applicable because they are ambiguous: both method stringToRich of type (String)scala.runtime.RichString and method stringWrapper in object Predef of type (String)scala.runtime.RichString are possible conversion functions from java.lang.String to ?{val foreach: ?}
"12345".foreach(print)
^
found : java.lang.String
required: ?{val foreach: ?}
Note that implicit conversions are not applicable because they are ambiguous: both method stringToRich of type (String)scala.runtime.RichString and method stringWrapper in object Predef of type (String)scala.runtime.RichString are possible conversion functions from java.lang.String to ?{val foreach: ?}
"12345".foreach(print)
^
記得先前提過,scala.Predef中定義了stringWrapper()隱式函式,它是將String轉為RichString,而你又定義了一個stringToRich(),也是將String轉為RichString,編譯器沒辦法知道你要套用哪一個函式,直接回報錯誤!從這點也可以得知,隱式轉換根據的是來源與目的地型態,而不是隱式函式或變數名稱,而且目的地型態,不是指隱式轉換的傳回值型態,而是指目的物件的 結構型態(Structural typing)。例如上例中,來源型態是字串,目的型態是?{val foreach: ?}。
隱式轉換一次只會套用一次,也就是說,即使轉換後可以再套用另一個轉換,編譯器也不會對 x + y 作如 convert(convert1(x)) + y 連續套用的動作。為了避免改變既有程式的運作,只要既有程式可以運作,編譯器也不會作隱式轉換的動作。