scala implicit关键字详解(隐式转换函数、隐式类、隐式参数、隐式值)

scala implicit关键字详解(隐式转换函数、隐式类、隐式参数、隐式值)

一、Overview

implicit是scala中的一个关键字,关于它有着丰富的用法,使得scala更灵活和容易扩展。截止目前,scala已经来到了2.12.3版本,本篇文章把目前scala支持的implicit用法作了较为全面的整理,以方便在阅读scala源码的时候稍作参考。文中全部的代码和测试结果均以scala2.12.3为准。

闲话不多讲,目前可以见到的implict用法有如下几种:

  1. 隐式转换函数
implicit def int2str(x:Int):String = x.toString
  1. 隐式类
implicit class Box(x: Int) {    
}
  1. 隐式参数
def compare[T](x:T,y:T)(implicit ordered: Ordering[T]):Int = {
  ordered.compare(x,y)
}
  1. 隐式值
implicit val x: Int = 0
  1. 隐式对象
implicit object obj {
}
  1. context bound
def compare2[T: Ordering](x: T, y: T) = {
  val ord = implicitly[Ordering[T]]
  ord.compare(x, y)
}

先不用着急理解这些代码的含义,下面会逐一讲解。

二、隐式转换函数

首先来看隐式转换函数。

implicit def int2str(x:Int):String = x.toString

这段代码声明了一个函数int2str,它与正常函数唯一的区别在于前面多出的implicit关键字。这里的implicit就是它字面的含义——隐式,它告诉编译器,这个函数是一个隐式转换函数,能够把Int类型的值转换成String类型的值。

这种隐式转换的意义在于,如果在进行一个对Int类型的操作时不合法,编译器会在当前作用域寻找合适的隐式转换,来尝试使这种操作合法。隐式转换发生在这两种情景:

  • e是一个S类型的表达式,而需要的却是T类型,编译器会寻找S=>T的隐式转换
  • e是一个S类型的表达式,使用点号访问e.m时,m不是类型S的成员,编译器会寻找合适的隐式转换使e.m合法

隐式转换最常用的用途就是扩展已有的类,在不修改原有类的基础上为其添加新的方法、成员。

例如上面的这个函数,在为Int类型定义好到String类型的隐式转换后,所有String类型支持的操作都可以直接在Int类型的值上使用:

10.concat("hello")
10.length

接受String类型的函数也可以接受Int类型:

def hi(x:String) = println("hi"+x)
hi(123)

需要注意:

  1. 对于隐式转换函数,编译器最关心的是它的类型签名,即它将哪一种类型转换到另一种类型,也就是说它应该接受只一个参数,对于接受多参数的隐式函数来说就没有隐式转换的功能了。
implicit def int2str(x:Int):String = x.toString // 正确

implicit def int2str(x:Int,y:Int):String = x.toString // 错误
  1. 不支持嵌套的隐式转换
class A{
 def hi = println("hi")
}

implicit def int2str(x:Int):String = x.toString 

implicit def str2A(x:Int,y:Int):A = new A

"str".hi  // 正确
1.hi      // 错误
  1. 不能存在二义性,即同一个作用域不能定义两个相同类型的隐式转换函数,这样编译器将无法决定使用哪个转换
/* 错误-- */
implicit def int2str(x:Int):String = x.toString 

implicit def anotherInt2str(x:Int):A = x.toString
/* --错误 */
  1. 代码能够在不使用隐式转换的前提下能编译通过,就不会进行隐式转换

三、隐式类

前面提到,隐式转换最重要的应用是扩展已存在的类,它的功能和c#中的扩展方法很类似。比如我们想对已有的Int类型添加一个sayhi的方法,可以这样做:

class SayhiImpl(ivalue:Int) {
  val value:Int = ivalue
  def sayhi = println(s"Hi $value!")
}
implicit def int2Sayhi(x:Int) = new SayhiImpl(x)

那么调用123.sayhi,将会输出:Hi 123!

即我们先实现一个支持sayhi方法的类,再写一个隐式转换函数,使得Int类也支持sayhi。但是这种写法过于啰嗦了,可以使用隐式类实现等价的功能:

implicit class SayhiImpl(ivalue:Int) {
  val value:Int = ivalue
  def sayhi = println(s"Hi $value!")
}

123.sayhi  //合法

隐式类就是在类定义前加一个implicit关键字,这表示它的构造函数是一个隐式转换函数,能够将参数的类型转换成自己的类型,在这里就是构造函数SayhiImpl(ivalue:Int)定义了IntSayhiImpl的隐式转换。

在使用隐式类时需要注意以下限制条件,这里直接搬运官网的文档

  1. 只能在别的trait/类/对象内部定义。
    object Helpers {
       implicit class RichInt(x: Int) // 正确!
    }
    implicit class RichDouble(x: Double) // 错误!
  1. 构造函数只能携带一个非隐式参数。
 implicit class RichDate(date: java.util.Date) // 正确!
 implicit class Indexer[T](collecton: Seq[T], index: Int) // 错误!
 implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 正确!

虽然我们可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。

  1. implict关键字不能用于case类
implicit case class Baz(x: Int) // 错误!

四、隐式参数

看最开始的例子:

def compare[T](x:T,y:T)(implicit ordered: Ordering[T]):Int = {
  ordered.compare(x,y)
}

在函数定义的时候,支持在最后一组参数使用 implicit,表明这是一组隐式参数。在调用该函数的时候,可以不用传递隐式参数,而编译器会自动寻找一个implict标记过的合适的值作为该参数。

例如上面的函数,调用compare时不需要显式提供ordered,而只需要直接compare(1,2)这样使用即可。

再举一个例子:

object Test{
    trait Adder[T] {
      def add(x:T,y:T):T
    }

    implicit val a = new Adder[Int] {
      override def add(x: Int, y: Int): Int = x+y
    }

    def addTest(x:Int,y:Int)(implicit adder: Adder[Int]) = {
      adder.add(x,y)
    }

   addTest(1,2)      // 正确, = 3
   addTest(1,2)(a)   // 正确, = 3
   addTest(1,2)(new Adder[Int] {
      override def add(x: Int, y: Int): Int = x-y
    })   // 同样正确, = -1
}

Adder是一个trait,它定义了add抽象方法要求子类必须实现。

addTest函数拥有一个Adder[Int]类型的隐式参数。

在当前作用域里存在一个Adder[Int]类型的隐式值implicit val a

在调用addTest时,编译器可以找到implicit标记过的a,所以我们不必传递隐式参数而是直接调用addTest(1,2)。而如果你想要传递隐式参数的话,你也可以自定义一个传给它,像后两个调用所做的一样。

五、隐式值和隐式对象

最开始的示例代码有,

隐式值:

implicit val x: Int = 0

隐式对象:

implicit object obj {
}

上面提到过,在调用含有隐式参数的函数时,编译器会自动寻找合适的隐式值当做隐式参数,而只有用implict标记过的值、对象、函数才能被找到。

例如自动寻找隐式对象:

    implicit object Obj {
      def hello(s:String) = println(s"Hello $s!")
    }

    def test2(s:String)(implicit o: Obj.type ) = {
      o.hello(s)
    }

   test2("world")   // 输出Hello world!

自动寻找隐式函数:

    implicit def int2str(x: Int): String = x.toString

    def test1(x: Int, func: String => Unit)
             (implicit helper: Int => String) = {
      func("\"" + helper(x) + "\"")
    }

    test1(12, println) // 打印出"12"

六、context bound

最后来看隐式作为泛型类型的限制:

def compare2[T: Ordering](x: T, y: T) = {
  val ord = implicitly[Ordering[T]]
  ord.compare(x, y)
}

上面compare2是一个泛型函数,其有一个类型参数T,在这里T:OrderingT类型做出了限制,要求必须存在一个Ordering[T]类型的隐式值,这种限制就叫做context bound。

这其实是隐式参数的语法糖,它等价于:

def compare2[T](x: T, y: T)(implicit ord:Ordering[T]) = {
  ord.compare(x, y)
}

注意到前面的函数体里用到了implicitly函数。这是因为在使用[T: Ordering]这样的类型限制时,我们没有能接触到具体的Ordering[T]类型的隐式值ord,这时候调用implicitly函数,就可以拿到这个隐式值,进而进行下一步操作了。没有这个函数的话,你需要这样写:

def compare2[T: Ordering](x: T, y: T) = {
  def helper(implicit ord:Ordering[T]) == ord.compare(x, y)
  helper
}

七、隐式解析机制

最后加一点比较深入一点的内容,看一下隐式值的寻找机制,引用自这篇博客

即编译器是如何查找到缺失信息的,解析具有以下两种规则:

1 .首先会在当前代码作用域下查找隐式实体(隐式方法 隐式类 隐式对象)
2.如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找

类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T它的查找范围如下:

(1)如果T被定义为T with A with B with C,那么A,B,C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索

(2)如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如List[String]的隐式搜索会搜索List的
伴生对象和String的伴生对象

(3) 如果T是一个单例类型p.T,即T是属于某个p对象内,那么这个p对象也会被搜索

(4) 如果T是个类型注入S#T,那么S和T都会被搜索

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

推荐阅读更多精彩内容