0%

深度剖析scala中的隐式转换

“道常无为,而无不为。
侯王若能守之,万物将自化。
化而欲作,吾将镇之以无名之朴。
无名之朴,夫亦将不欲。
不欲以静,天下将自定。”1

对于scala的学习成本,大多会体现在对于隐式转换的理解应用上。隐式转换伴随着泛型,scala所有的神秘之处、优雅之处皆因于此。

隐式在scala中,应用场景会在如下四个方面:

  • 隐式参数
    1
    scala> def razor(implicit name:String) = name
    2
    razor: (implicit name: String)String
    直接调用razor方法
    1
    scala> razor
    2
    <console>:13: error: could not find implicit value for parameter name: String
    3
           razor
    4
           ^
    报错!编译器说无法为参数name找到一个隐式值
    定义一个隐式值后再调用razor方法
    1
    scala> implicit val iname = "Philips"
    2
    iname: String = Philips
    3
    4
    scala> razor
    5
    res12: String = Philips
    因为将iname变量标记为implicit,所以编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺少参数。
    但是如果此时你又重复定义一个隐式变量,再次调用方法时就会报错
    1
    scala> implicit val iname2 = "Gillette"
    2
    iname2: String = Gillette
    3
    4
    scala> razor
    5
    <console>:15: error: ambiguous implicit values:
    6
     both value iname of type => String
    7
     and value iname2 of type => String
    8
     match expected type String
    9
           razor
    10
           ^
    匹配失败,所以隐式转换必须满足无歧义规则,在声明隐式参数的类型是最好使用特别的或自定义的数据类型,不要使用Int,String这些常用类型,避免碰巧匹配。
  • 隐式函数
    1
    class Person(val name:String)
    2
    3
    class Engineer(val name:String,val salary:Double){
    4
      def mySalary(): Unit ={
    5
         println(name+",your monthly salary is is:"+salary)
    6
      }
    7
    }
    8
    9
    object Test {
    10
      def main(args: Array[String]): Unit = {
    11
        new Person("sutong").mySalary
    12
      }
    13
    }
    实例化一个person对象,然后调用mySalary方法时,此时编译是通不过的,因为person类没有mySalary方法。但是如果我们加入一个隐式函数就能时编译通过并能顺利运行,如下:
    1
    object Test {
    2
      implicit def person2Engineer(p:Person):Engineer = {
    3
        new Engineer(p.name,100000)
    4
      }
    5
      def main(args: Array[String]): Unit = {
    6
        new Person("sutong").mySalary
    7
      }
    8
    }
    输出:
    1
    sutong,your salary is:100000.0
    Scala会在报错前,去找隐式函数,例如我们定义的隐式函数,根据函数签名,主要是输入参数,找到了person2Engineer方法,并利用此方法将person对象转换为了Engineer对象,然后发现Engineer对象有mySalary方法,直接调用其mySalary方法,打印输出。运行后,Person对象和Engineer类型无任何关系。

约定:

虽然在定义implicit函数时scala并未要求要写明返回类型,但我们应写明返回类型,这样有助于代码阅读。

  • 隐式对象
    用implicit修饰的object就是隐式对象。
    1
    abstract class Template[T]{
    2
      def add(a:T,b:T):T
    3
    }
    4
    5
    abstract class SubTemplate[T] extends Template[T] {
    6
      def unit:T
    7
    }
    8
    9
    object Implicit_Object_Test {
    10
      def main(args: Array[String]): Unit = {
    11
        implicit object StringAdd extends SubTemplate[String]{
    12
          override def unit: String = ""
    13
          override def add(a:String,b:String):String = a concat b
    14
        }
    15
        implicit object IntAdd extends SubTemplate[Int]{
    16
          override def unit: Int = 0
    17
          override def add(a:Int,b:Int):Int = a+b
    18
        }
    19
    20
        def sum[T](x:List[T])(implicit m:SubTemplate[T]):T = {
    21
          if(x.isEmpty) m.unit
    22
          else m.add(x.head,sum(x.tail))
    23
        }
    24
    25
        println(sum(List(2,4,6,8,10)))
    26
        println(sum(List("a","b","c")))
    27
      }
    28
    }
    29
    输出:
    30
    30
    31
    abc
    首先定义了两个含有泛型的抽象类,Template是父类,而SubTempalte是子类,而这个子类有有两个子对象StringAdd和IntAdd,下面来看下主函数中定义的sum函数,它是一个含有泛型的函数,它的第一个参数是含有泛型的List类型的xs,第二个参数是含有泛型的SubTemplate类型的隐式参数m,函数返回值是一个泛型,首先,函数里先判断传入的第一个参数是否为空,若为空则调用隐式参数m,有由于scala可以自动进行类型推到,所以运行时,泛型T是一个确定类型,要么为Int要么为String,但是为空时,我们调用隐式参数m的unit是不同的,Int为0,而String为“”,所以我们定义了两个隐式对象对其进行处理,IntAdd隐式类复写了unit,使之为0,StringAdd隐式类复写了unit,使之为””,这样程序就可以正常执行了。同理,隐式对象方法对add方法进行复写,完成了sum操作。
  • 隐式类
    在scala2.10后提供了隐式类,可以使用implicit声明类,但是需要注意以下几点:

1.其所带的构造参数有且只能有一个
2.隐式类必须被定义在类,伴生对象和包对象里
3.隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
4.作用域内不能有与之相同名称的标示符

1
import scala.io.Source
2
import java.io.File
3
4
object Implicit_Class {
5
  import Context_Helper._
6
  def main(args: Array[String]) {
7
    println(1.add(2))  
8
    println(new File("I:\\aa.txt").read())
9
  }
10
}
11
12
object Context_Helper{
13
  implicit class ImpInt(tmp:Int){
14
    def add(tmp2: Int) = tmp + tmp2
15
  }
16
  implicit class FileEnhance(file:File){
17
    def read() = Source.fromFile(file.getPath).mkString
18
  }
19
}

其中1.add(2)这个可以这样理解:

当 1.add(2) 时,scala 编译器不会立马报错,而检查当前作用域有没有 用implicit 修饰的,同时可以将Int作为参数的构造器,并且具有方法add的类,经过查找 发现 ImpInt 符合要求,利用隐式类 ImpInt 执行add 方法。

归纳一下

  • 隐式转换的时机
    a.当方法中的参数的类型与目标类型不一致时
    b.当对象调用类中不存在的方法或成员时,编译器会自动将对象进行隐式转换
  • 隐式解析机制
    a.首先会在当前代码作用域下查找隐式实体(隐式方法 隐式类 隐式对象)
    b.如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找
  • 隐式转换的前提
    a.不存在二义性
    b.隐式操作不能嵌套使用
    c.代码能够在不使用隐式转换的前提下能编译通过,就不会进行隐式转换

1:老子《道德经》第三十七章,老子故里,中国鹿邑。