scala學習 - 隱式轉換和隱式參數

本文來自《Programming in Scala》一書

scala學習之隱式轉換和隱式參數

1 隱式類型轉換

scala編譯器在對語言做類型檢查時,發現參與運行的表達式類型A不符合類型語義要求,在拋出錯誤之前,會在當前作用域中查找是否存在A 到B類型的轉換,是的轉換之后通過類型檢查,這種轉換就是隱式類型轉換:用戶無需在代碼中明確的調用函數將A轉換成B,編譯器幫你做了這件事。

比如:

val list1 = List(1,2)
val i = 3
list1 ::: i

上面代碼無法通過scala編譯器的類型檢查,拋出如下錯誤:

scala> val list1 = List(1,2)
list1: List[Int] = List(1, 2)

scala> val i = 3
i: Int = 3

scala> list1 ::: i
<console>:28: error: value ::: is not a member of Int
       list1 ::: i
             ^

原因在于list1:::i被轉換成list1.:::(i)的方法調用,這個方法要求參數必須是集合類型,而i是Int型。一種解決辦法是像這樣list1:::List(i)在代碼中顯式的將 i 轉換成List。另外一種方式就是像下面這樣:

object IntWrapper{
  implicit def intToList(i : Int): List[Int] = {
    List(i)
  }

  def main(args : Array[String]) :Unit = {
    val l1 = List(1,2);
    val l2 = l1 ::: 3;
    print(l2)
    //輸出List(1, 2, 3)
  }
}

定義隱式轉換方法intToList(必須使用implicit關鍵字),接收Int類型參數,轉換成List[Int]。 上面代碼編譯器再碰到l1:::3時,發現類型不匹配,會在當前作用域尋找一種隱式轉換使得 3 轉換成l1.:::()調用能夠接收的類型。

還有一種會編譯器會嘗試將源類型隱式轉換成目標類型的地方就是:在源類型上調用方法,可是該方法卻不存在于源類型的class定義中,比如:

class IntWrapper(i : Int) {
  private[IntWrapper] var l = List[Int](i)
  
  def printSelf(): Unit ={
    println("In IntWraper: " + l)
  }
}

object IntWrapper{
  implicit def intToIntWraper(i : Int):IntWrapper = {
    new IntWrapper(i)
  }

  def main(args : Array[String]) :Unit = {
    val i = 11;
    i.printSelf()
    //輸出 In IntWraper: List(11)
  }
}

main方法里定義了Int類型 i ,Int類型不存在方法printSelf,此時編譯器會嘗試尋找Int上的類型轉換,使的轉換后的目標的類型有方法printSelf,因此會使用implicit def intToIntWraper作為轉換函數。

上面這兩種情形實際上可以歸為一類,就是都需要將源類型轉換成目標類型才能夠通過類型檢查。

2 隱式參數

scala編譯器另一個會替你完成隱式操作的地方就是參數列表,比如定義了下面這樣一個方法:

class IntWrapper (i : Int){
  ...
  private[IntWrapper] var l = List(i)

  def map(implicit m : Int => List[Int]):List[List[Int]] = {
    l.map(m)
  }
  ...
}

object IntWrapper{
  implicit val f =(i : Int) => List(i + 1)
  def main(args : Array[String]) :Unit = {
    val intWraper = new IntWrapper(1)
    //map使用隱式參數,編譯器在當前可見作用域中查找到 f 作為map的參數
    print(intWraper.map)
  }
}

方法map的參數列表使用了implicit關鍵字,表示你在使用map時,可以不提供參數,讓編譯器去替你完成在方法調用的作用域中去查找滿足參數類型的隱式轉換。

上面實例代碼中表,使用隱式參數時,不僅要求map的形式參數列表使用implicit,提供的隱式實際參數 f 也需要使用implicit。

3 何時發生隱式操作

其實1,2已經說明了scala何時會嘗試使用隱式操作:

  1. 需要轉換成目標類型

    比如函數 f 的類型A => B,使用類型為C(假設C不是A的子類)的變量c調用 f(c), 編譯器就會需要嘗試尋找將 C => A的隱式類型轉換。

  2. 在原類型上調用不存在的方法

    比如class A存在方法def fA()…,在class B的實例b上調用b.fA(),假設class B沒有成員方法fA,此時會嘗試 B => A的隱式轉換.

  3. 隱式參數

4 隱式操作的一些規則或者約束

4.1 作用域規則

隱式轉換必須以單一標識符的形式出現在需要轉換的地方所處的作用域中,或者與轉換的源類型或目標類型關聯在一起。分別解釋一下:

  1. 單一標識符是指不能以xxx.f這種形式才能使A轉換成B類型

    假設有隱式轉換f使得A轉換成B,編譯器在插入代碼時必須是f(A),而不需要在f前面插入包或者類的前綴,比如下面這種就是不行的:

    package me.eric.learn.`implicit`.convertutils
    object ConvertUtils{
      //Int 到 List的隱式轉換
      implicit def intToList(i : Int): List[Int] = {
        List(i)
      }
    }
    
    -------------------------------------------------------------------
    package me.eric.learn.`implicit`
    //引入ConvertUtils
    import convertutils.ConvertUtils
    
    class IntWrapper{}
    object IntWrapper{
      def main(args : Array[String]) :Unit = {
        val l1 = List(1,2)
        //此處需要將 3 隱式轉換成List,編譯器無法直接應用 l1:::intToList(3),當在代碼中顯式調用時也得這樣: l1:::ConvertUtils.intToList(3)。不符合單一標識符的原則
        val l2 = l1 ::: 3
      }
    }
    
    //上面代碼要想可以應用隱式轉換,可以使用“import convertutils.ConvertUtils.intToList“引入
    
    
  2. 與轉換的源類型關聯

    和后面介紹的與轉換的目標類型關聯一樣,這是單一標志符原則的例外。原類型關聯是指:假設需要A - > B類型轉換,那么轉換操作可以以implicit def ...這種形式定義在object A里,如下:

    package me.eric.learn.`implicit`
    
    //StringWrapper是源類型,包裝一下String
    class StringWrapper(s : String) {
      val s1 = s
    }
    
    object StringWrapper{
      //隱式操作和源類型關聯,StringWrapper -> String轉換
      implicit def stringWrapperToString(sw : StringWrapper):String = {
        sw.s1
      }
      
     //隱式操作, String -> StringWrapper的轉換
     implicit def stringToStringWrapper(s : String):StringWrapper={
        new StringWrapper(s)
      }
    }
    ---------------------------------------------------------------------------
    
    package me.eric.learn.`implicit`.convertutils
    //這種引入方式不符合單一標識符原則
    import me.eric.learn.`implicit`.StringWrapper
    
    object StringWrapperUtils {
      def split(sw : StringWrapper):String[]={
        //StringWrapper關聯的stringWrapperToString,sw可以隱式轉換成String,調用split方法
        sw.split(" ")
      }
    }
    
  3. 與目標類型關聯

    即A -> B的轉換可以定義在object B中,以2中代碼為例: StringWrapperUtils#split接收參數類型為StringWrapper, 此時傳一個String類型是非法的,顯然無法在Object String里增加String -> StringWrapper的轉換,但是可以在StringWrapper里增加,如同例子中 implicit def stringToStringWrapper...那樣。

4.2 無歧義規則

無歧義規則是指不可以有兩個隱式轉換 convert1 和convert2 使得轉換應用在A上都可以完成類型檢查。比如下面的代碼就通不過編譯;

object IntWrapper{
  implicit def intToList(i : Int): List[Int] = {
    List(i)
  }

  implicit def intToList1(i : Int): List[Int] = {
    List(i + 1)
  }
  // implicit val f =(i : Int) => List(i + 2)           
  implicit def intToIntWraper(i : Int):IntWrapper = {
    new IntWrapper(i)
  }


  def main(args : Array[String]) :Unit = {
    val l1 = List(1,2)
    val l2 = l1 ::: 3
    print(l2)
  }
}

忽略上面代碼注釋部分,有兩個Int到List的轉換:intToList和intToList1,存在歧義。

但是奇怪的是,去掉上面代碼的注釋,卻又能通過編譯,且運行結果顯示使用了implicit val f =(i : Int) => List(i + 2)這個隱式轉換。不知道為什么。

4.3 單一調用規則

如果需要對類型A轉換,只會進行一次轉換,比如方法f(c : C)接收C類型,此時存在 A -> B的轉換fAB,B -> C的轉換fBC,f(a)的調用不會成功,不會編譯器不會應用f(fBC(fAB(a))) 把a轉換成C類型

4.4 顯式操作優先

其實很簡單,能不進行隱式轉換就能通過類型檢查的話,就不使用隱式轉換。比如方法def f(a : A),變量b(類型為B,繼承A),調用f(b),即便此時有B -> A的轉換也不會應用。

5. 隱式參數

4中的規則同樣適用于隱式參數,隱式參數適用方式如下:

package me.eric.learn.`implicit`

class ImplicitParam {
  // implicit 聲明隱式參數
  def echo(i : Int)(implicit s:String, l:Long) ={
    println((i,s,l))
  }
}

object ImplicitParam{
  // 作為隱式值應用于ImplicitParam#echo方法
  implicit val s = "hello"
  implicit val l = 100L
  def main(args:Array[String]): Unit ={
    val ip = new ImplicitParam()
    //只需要指定參數 i的值,編譯器將應用 s,l作為隱式參數的實參,相當于調用ip.echo(i)(s,l)
    ip.echo(1)
  }
}
  1. 對于柯里化函數,隱式參數只能出現最后一組參數列表上,只能是形如def f(...)(...)(implicit ...)這樣.

  2. 隱式值不僅可以用于補足隱式參數,這個隱式值后續還能作為一種可行的隱式轉換作用于方法體中的隱式類型轉換,比如下面:

    package me.eric.learn.`implicit`
    
    class ImplicitParam {
      //需要 Int => List[Int]類型的隱式參數
      def echo(implicit  intToList : Int => List[Int]) = {
        val list = List(1,2)
        val i = 3
        //此處需要隱式轉換,將i轉換成合適的目標類型,此處 intToList將作為一種可行的隱式轉換作用于i上
        println(i ::: list)
      }
    }
    
    object ImplicitParam{
      //定義 Int => List[Int]的隱式值
      implicit  val f = (i : Int) => List(i)
    
      def main(args:Array[String]): Unit ={
        val ip = new ImplicitParam()
        // f 作為隱式值被應用
        ip.echo
      }
    }
    
    

    ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容