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]));
}
}