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
      }
    }
    
    

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容