从JAVA到Scala(三):implicit的三种用法

时间:2022-02-28 13:16:48

对scala的隐式参数又爱又恨,既强大得让人欢呼雀跃,但繁琐的机制与晦涩的语法又让人望而却步,总结起来,implicit一共有三种用法,一种比一种强大,这里就不讲太深奥的理论,直接进入应用场景实战。

1. 默认参数值

首先,需要注意的是,在方法参数上添加implicit关键字对方法几乎没有任何影响,原来的调用方式依旧。

其次,implicit作为参数声明时,只能作为方法的唯一参数,是的,你没看错,唯一的参数,还有每个方法也只能声明一个参数!如果混在一起,那么它几乎没有作用,因为每一次调用时你都必须指定方法的所有参数。当然,scala提供了完美的解决办法,请看下面的例子:

test("隐式参数必须独立声明") {
    // 调用时无法省略参数
    def add(implicit a: Int, b: Int): Int = a + b
    // 一定要作为最后一个参数
    def plus(a: Int)(implicit b: Int): Int = a + b
    // 声明隐式参数,不需要名称相同,只能类型相同就可以了
    implicit val c: Int = 10
    // 单元测试
    assertResult(plus(5))(15)
}

从上面的例子可以得出如下结论:
1. 通过高阶函数的方法,隐式参数可以和其他参数一起合作,但受限于调用方式,必须作为函数的最后一个参数;
2. 声明隐式参数时,并不需要采用与函数参数一样的名称,只要类型相同即可;

2. 参数类型转换

在方法的调用中,实参的类型有时难以匹配形参的类型,此时需要可以利用隐式方法进行自动转换

 test("隐式方法转换类型") {
    // 调用的方法,并不需要声明为隐式参数
    def plus(a: Int, b: Int): Int = a + b
    // 定义转换函数
    implicit def stringToInt(s: String): Int = Integer.parseInt(s)
    assertResult(plus("3", "5"))(8)
  }

从上面的例子中,我们可以看出:
1. 调用的方法不需要携带隐式参数,适用于所有的方法;
2. 定义转换函数后,大大增强了方法参数的适配性;
3. 隐式函数查找的作用域跟隐式参数的作用域一模一样;

3. 动态添加属性与方法

除了可以批量赋值默认参数,以及动态转换函数外,implicit最强大的地方还在于能为每种类型动态添加属性与方法,如下:

test("动态添加属性与方法") {
    // 参数就是添加属性与方法的类型
   implicit def strFile(dir: String) = new {
      // 动态添加属性
      val isDir:Boolean = true
      // 动态添加方法
      def listDir(): List[File] = {
        val d = new File(dir)
        // _只有作为参数时才能省略,作为主体时不能省略
        d.listFiles().filter(_.isDirectory).toList
      }
    }
    assert("/".listDir().size > 0)
    assert("/".isDir)
}

这个功能简直强大到吓人,任意类型一律通杀,无视类型的声明是否是不变模型(final),无视类型是自定义还是系统内置。有了这样的机制,写出一万公里长的链式代码也是易如反掌。

4. 隐式作用域

为了找到相关的隐式参数与方法,scala会按如下优先级查找相关的隐式声明:
1. 在当前函数调用的作用域及父作用域内;
2. 在声明隐式参数类型的伴生作用域内;

结论

简简单单的implicit关键字,对Scala的功能增强几乎是天翻地覆,只有理解了implicit,尤其是第三种用法,才能理解如同天书一样的DSL代码(尤其是初次使用的JAVA程序员),强烈推荐《深入理解Scala》(这本书翻译得不太好,但内容很有深度)进一步阅读。