谈谈scala的那些Implicit

作者:liuguobing   阅读 (4767)  |  收藏 (0)  |  点赞 (0)

摘要

scala有很多奇妙的语法糖,这里讲的是Implicit Parameter和Implicit Convert。 ----补充下,scala还有个Implicit Class


原文链接:谈谈scala的那些Implicit

这两个其实没什么关联,但都有个Implicit。

首先谈谈隐式转换,隐式转换是对java隐式转换的一个拓展,我们都知道java中基础类型中字节数少的可以自动转成字节数长的,反之就要强制转换了,子类也可以转成父类或者继承的接口类型对象(这个也可以说不算是转换吧,毕竟子类对象本身也instanceof 了父类),scala对之进行了扩展。scala这个实现起来很简单,只需要声明一个implicit 方法,接受原类型对象返回一个转换的类型对象,举个栗子:

class MyInt(value:Int) {

  def **(times: Int): Double = times match {
    case a if a < 0 => if (value == 0) throw new Exception("0没有非负指数") else 1.0 / (this ** (-times))
    case 0 => if (value == 0) throw new Exception("0没有非负指数") else 1
    case a => (1 to a).map(_ => value).product.toDouble
  }

}

定义了一个类MyInt,然后给他添加一个方法**表示乘方,这个没什么好说,就是注意一点scala的方法标识符不局限于拉丁字母和下划线,像这些*+-等符号也是可以用的。

然后就是神奇的地方:

来看main方法的代码:

implicit def IntConvertMyInt(value:Int):MyInt = new MyInt(value)

println(2 ** 3) // 8.0
println(-2 ** 3) // -8.0
println(0 ** 3) // 0.0
println(2 ** -3) // 0.125

首先我们定义一个implicit函数,接受一个Int类型的value作为参数,返回一个MyInt类型,这个方法除了这个implicit修饰符也没什么可说的,就是scala的方法中也可以定义函数(方法),神奇的在于几个println的括号内的东西,scala的Int就是java的Integer,而java的Integer明明就没有**方法的,这里为何能编译通过并且按我们的预料得到值呢,那就是隐式转换了,scala编译器发现Int并没有**方法,但发现当前有一个隐式转换方法,而这个方法返回的对象有**方法,于是scala偷偷的调用IntConverMyInt方法把这个Int变成了MyInt,如果这个转换还不是很明显,那就看下面的栗子:

def square(value:MyInt):Double =  {
  value ** 2
}
println(square(2)) //4.0
println(square(7)) //49.0

这明确的定义了一个方法,接受的是一个MyInt类型的参数,返回它的value的平方,而下面两个println中调用了这个方法,传入的是Int类型,自动隐式的转换成了MyInt。


#################分隔符##########################################


再来谈谈隐式参数。隐式参数就是在一个方法的参数声明中加了implicit的参数,在调用这个方法是可以不显示的传入该参数,它会根据上下文来自动添加该参数,先来看看栗子:

def sum[T](seq:Seq[T])(implicit monoid:Monoid[T]):T ={
  if (seq.isEmpty) monoid.unit
  else monoid.add(seq.head,sum(seq.tail))
}

abstract class SemiGroup[A] {
  def add(x:A,y:A): A
}

abstract class Monoid[A] extends SemiGroup[A] {
  def unit:A
}

这是scala官方doc的一个栗子,我借来用,定义了两个抽象类,主要用的是Monoid这个类,它有两个抽象方法,一个是add,一个是unit,这是泛型类,然后定义了一个sum函数,这个接受一个seq对象和一个隐式参数,是Monoid对象,如果seq是空,那么就返回monoid的unit,否则就对所有元素进行add。这些都很普通,接下来的使用就是语法糖了:

implicit object IntMonoid extends Monoid[Int] {
  val unit = 0
  override def add(x: Int, y: Int): Int = x + y
}

implicit object StringMonnit extends Monoid[String] {
  val unit = ""

  override def add(x: String, y: String): String = x concat y
}

println(sum(List(2,3,4,8,9))) // 26
println(sum(List("a","b","c"))) // abc

定义了两个隐式的object(单例对象),然后调用两次sum,第一个接受的是List[Int],第二个是List[String],都没有显示的传入monoid这个参数,编译通过,并自动的分别传入的IntMonoid和StringMonoid。再看一个错误的栗子:

println(sum(List(1.2,2.3,3.4))) // 无法编译,报错是找不到implicit value给参数monoid,没有足够的参数

编译报的错是:

Error:(34, 16) could not find implicit value for parameter monoid: lew.bing.scala.ImplicitTest.Monoid[Double]
    println(sum(List(1.2,2.3,3.4)))  
Error:(34, 16) not enough arguments for method sum: (implicit monoid: lew.bing.scala.ImplicitTest.Monoid[Double])Double.
Unspecified value parameter monoid.
    println(sum(List(1.2,2.3,3.4)))

我再修改下这个,改成:

println(sum(List(1.2,2.3,3.4,-2.3))(new Monoid[Double] {
  val unit = 0.0
  override def add(x: Double, y: Double): Double = {
    if (y < 0) x else x + y
  }
}))//6.9

这里我显示的传入了一个monoid进去,编译通过运行通过,并得到我们想要的结果,说明隐式参数是可以显示传入的。官方文档有两句话:

1.First, eligible are all identifiers x that can be accessed at the point of the method call without a prefix and that denote an implicit definition or an implicit parameter.
2.Second, eligible are also all members of companion modules of the implicit parameter’s type that are labeled implicit.

首先这个隐式参数合格的对象是那些所有那个方法调用所在的位置可以不需要前缀符号直接访问并能指向隐式参数的的对象,其次也可以是标明了implicit的所有的该隐式参数的类型的companion modules(英文水平有限翻译的不太准)。简单的理解就是假如没显示的传入该隐式参数,那么先后从以下两个地方寻找,第一是从调用这个方法的作用域中寻找那些不需要前缀可以直接访问并标明了implicit的该implicit参数的类型的对象,第二是该implicit参数的伴随模块中标明了implicit的定义的对象(定义必须明确标明类型)。

第一个的栗子就不举了,上面的都是,第二个的栗子如下:

object Monoid {
  implicit val monoid:Monoid[Double] = new Monoid[Double] {
    override def unit: Double = 0.0

    override def add(x: Double, y: Double): Double = {
      x + y
    }
  }
}

这样对于上面那个报错了的栗子:

println(sum(List(1.2,2.3,3.4)))

可以编译过了。


隐式转换与隐式参数是scala中比较不错的特性,也是很容易让人迷惑的地方,特别是如果我们使用别人提供的库时,一不小心就掉入这里面的陷阱了,把这两个特性都了解清楚才能更好的调试,知道我们哪个地方有错误并修正,看源码是也能知道那些隐式转换的对象调用的方法来自哪里。



----------------------------------分隔符-----------------------------------------------------


之前忘记说了,scala还有个implicate class,这个是给既有的类添加新的方法的,语法是:

implicit class AClass(val b:BClass){ defs...}

这样一个BClass对象就可以使用了AClass里面定义的方法,注意假如说有其他的隐式类扩展了BClass,并且有跟AClass的定义的方法相同,那这个方法无法使用,因为编译器无法判断该用哪个,但如果跟BClass原有的方法冲突,则没什么关系,它只会只想BClass的该方法。栗子来了:

implicit class NewString(val string: String) {

  def isNumber:Boolean = {
    string.matches("\\d+")
  }

  def trim:String = string.trim+"a"

}

println("123".isNumber)//true
println("123".trim)//123

它只是语法糖,实际上编译器会做如下几件事,首先隐式类变成普通的类声明,然后创建一个隐式方法:

implicate final def  NewString(val string:String):NewString = new NewString(string)

这里大家就应该猜到是利用隐式方法来对String到NewString进行隐式转换,然后String就可以使用NewString中的方法了。隐式类有几个局限的地方,它只能在能声明方法的地方声明,然后他的构造器的第一层参数列表只能有一个参数,但可以有额外的的隐式参数(不能是第一层参数)。然后假如有@annotation修饰这个类,那么在类声明和隐式方法声明中都加上这个annotation,对于target只有class的就只修饰到类(没class的自然会报错)

分类   默认分组
字数   4778

博客标签    scala   Implicit  

评论