scala的implicit关键字----不仅仅是隐式类型转换

implicit可用于隐式类型转换,该用途比较好理解,这里不会赘述。我们聊聊implicit的其他用法。

通过类型获得隐式值

implicit可用于修饰变量、参数、类、对象、方法等,下面是一个包含多种使用场景的例子:

class MyTest {

  //类型+无参隐式函数
  class Bar{}
  implicit def newBar = new Bar{
    override def toString: String = "implicit Bar"
  }

  //隐式类,等价于“普通类+隐式转换函数”
  //隐式类必须带一个类参数,类参数的类型就是隐式转换的“源类型”
  implicit class EString(val s: String){
    def compress = s.substring(0, s.length-1)
  }

  //隐式变量
  implicit val g_v = 1

  //隐式对象
  implicit object EString

  //隐式方法
  implicit def implFunc() = println("implicit func")

  //隐式参数
  def f(implicit i:Int) = i

  @Test
  def testImplicit: Unit ={
    //EString为隐式类,会自动生成String=>EString的隐式转换函数
    Assert.assertNotNull(implicitly[String => EString])

    //获得隐式对象,注意必须使用EString.type
    Assert.assertEquals(implicitly[EString.type ], EString)

    //调用带隐式参数的方法
    Assert.assertEquals(f, 1)

    //获得隐式变量g_v
    Assert.assertEquals(implicitly[Int], 1)

    //通过super type获得隐式变量g_v
    Assert.assertEquals(implicitly[AnyVal], 1)

    //获得隐式函数
    Assert.assertNotNull(implicitly[()=>Unit])

    //通过类型+无参隐式函数获得Bar实例,相当于变相的构造函数
    println(implicitly[Bar])

  }

注释很详细了,我们只是要强调一点,implicitly是通过类型来获得隐式值,所以上下文中如果有多个同类型的值,scala编译器会报错,比如我们这样写:

implicit val i=1
implicit val j=1
def f(implicit i:Int)=i
f

scala会报错:
error: ambiguous implicit values:
both value i of type => Int
and value j of type => Int
match expected type Int
这是因为在f的上下文里,同时有i和j两个Int型的隐式值,编译器无法选择。

我们特别讨论一下“类型+无参隐式函数获得隐式值”的用法,就是这段代码:

class MyTest {
  class Bar{}
  implicit def newBar = new Bar{
    override def toString: String = "implicit Bar"
  }

  @Test
  def testImplicit: Unit ={
    //通过类型+无参隐式函数获得Bar实例,相当于变相的构造函数
    println(implicitly[Bar])
  }
}  

这里有个疑问,我们并未在上下文里创建implicit Bar实例,为何通过implicitly[Bar]能得到呢?原来是scala编译器从中做的手脚,它发现我们定义了一个不带参数的implicit方法newBar,且返回Bar类型,就自动在println(implicitly[Bar])之前插入一些指令,这些指令会调用newBar临时创建出一个Bar实例来供implicitly[Bar]获得。所以,前面的注释说,此处的implicitly[Bar]有点像变相的构造函数。也由于Bar实例是临时创建的,所以如果再写一行
println(implicitly[Bar])
就会产生一个新的Bar实例。

隐式类型分派

implicitly表面上做的是“类型=>值”的映射,实质上它提供了一种隐式的类型分派技术。实际开发中,我们往往需要针对不同的类型做不同的分支处理,这些分支处理不过是实现细节,我们并不希望暴露给用户。这时,隐式的类型分派就能派上用场。
举个例子,我们要对List里的元素做自增操作,不同元素类型的自增做法不同,Int是t+1,String则是t+t。我们要对外提供一份统一的接口incr,能同时处理不同类型的List,则可以这样做:

trait Incrisable[T]{
  def incr(t:T):T
}

object Incrisable{
  //隐式对象供随后的implicitly[Incrisable[T]]获取,注意是通过super class获取
  implicit object IncrisableInt extends Incrisable[Int]{
    def incr(t:Int):Int = t + 1
  }

  //类型+无参隐式函数,如前所述,会触发scala为我们临时生成一个Incrisable[String]实例,供implicitly[Incrisable[T]]获取
  implicit def newIncrisableStr = new Incrisable[String] {
    override def incr(t: String): String = t + t
  }

  //【注意】T:Incrisable技法称为context-bound,它是implicit参数的语法糖,相当于
  //def incr[T](l:List[T])(implicit ev:Incrisable[T])
  def incr[T:Incrisable](l:List[T]) = {
    val ev = implicitly[Incrisable[T]]
    l.map(ev.incr)
  }
}

我们这样调用:

println(incr(List(1,2,3)))
println(incr(List("a", "b", "c")))

可以看到,由于省略了第二个真正做转换操作的参数,我们隐藏了不同类型List的实现细节,对外保持了统一、简洁的接口。

类型类

上述Incrisable例子所使用的技术也被称为类型类(type class),原本是haskell里的概念,被引入到scala里来。我对haskell了解不多,但从网上的文章和例子来看,类型类的出现应该是为了解决haskell的类型泛化问题,因为haskell里并无类和泛型的概念。
implicit关键字在scala的类型类技术中起着至关重要的作用,若无implicit,我们很难优雅的省略掉Incrisable[T]参数。我也尝试用java实现类似的效果,结果发现很难做到。
用C++的模板偏特化倒是可以模拟出类似的效果,因为如前所述,类型类要解决的是类型泛化的问题,类型泛化一般会遇到两种情形:一是不同类型的共性处理,二是不同类型的个性处理。前者用一般的泛型即可解决(C++、java皆可),后者则只能对应C++的模板偏特化(java因为类型擦除,显然无法做偏特化)。等价的C++代码如下(我们假设通用操作是t+1,string的t+t是针对类型的特殊处理):

template <typename T> class IncrBase
{
public:
    T incr(T t)
    {
        return t+1;
    }
}

template <> class IncrBase<std::string>
{
public:
    std::string incr(const std::string& t)
    {
        return t + t;
    }
}

template<typename T> void incr(const std::vector<T>& l, std::vector<T>& out)
{
    IncrBase<T> ev;
    out.reserve(l.size());
    for(size_t i=0; i<l.size();++i)
    {
        out.push_back(ev.incr(l[i]));
    }
}

猜你喜欢

转载自blog.csdn.net/tlxamulet/article/details/77971102