Scala中的Implicit详解
Scala中的implicit
关键字对于我们初学者像是一个谜一样的存在,一边惊讶于代码的简洁,
一边像在迷宫里打转一样地去找隐式的代码,因此我们团队结合目前的开发工作,将implicit
作为一个专题进行研究,了一些心得。
在研究的过程当中,我们注重三方面:
- 为什么需要
implicit
? implicit
包含什么,有什么内在规则?implicit
的应用模式有哪些?
为什么需要Implicit?
Scala在面对编译出现类型错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找
一个隐式implicit
的转换方法,转换出正确的类型,完成编译。这就是implicit
的意义。
我们正在做Guardian
系统的升级,Guardian
是公司内部的核心系统,提供统一权限管控、
操作审计、单点登录等服务。系统已经有4年多的历史了,已经难以满足目前的需要,比如:
当时仅提供了RESTFul的服务接口,而随着性能需求的提高,有些服务使用Tcp消息完成远程
调用;另外,在RESTFull接口的协议方面,我们也想做一些优化。
而现状是公司内部系统已经全部接入Guardian
,要接入新版,不可能一次全部迁移,甚至
要花很长一段时间才能完成迁移工作,因此新版接口必须同时支持新老两个版本接口协议。
因此我们必须解决两个问题:
- 兼容老版本协议, 以便能够平滑升级
- 支持多种协议,以满足不同业务系统的需求
我们希望对接口层提供一个稳定的Service
接口,以免业务的变动影响前端接口代码,常规
的做法是我们在Service
接口上定义多种版本的方法(重载),比如鉴权服务:
trait AuthService {
// 兼容老版本的鉴权业务方法
def auth(p: V1HttpAuthParam): Future[V1HttpAuthResult]
// 新版本的鉴权业务方法
def auth(p: V2HttpAuthParam): Future[V2HttpAuthResult]
// 新版本中支持的对Tcp消息鉴权的业务方法
def auth(p: V2TcpMsg): Future[V2TcpMsg]
}
这种做法的问题在于一旦业务发生变化,出现了新的参数,势必要修改AuthService
接口,
添加新的接口方法,接口不稳定。
假如有一个通用的auth
方法就好了:
trait AuthParam {}
trait StableAuthService{
// 稳定的鉴权接口
def auth(p: AuthParam)
}
这样,我们就可以按照下面的方式调用:
//在老版本的REST WS接口层:
val result = authService auth V1HttpAuthParam
response(result)
//在新版本的REST WS接口层:
val result = authService auth V2HttpAuthParam
response(result)
// .... 在更多的服务接口层,任意的传入参数,获得结果
很明显,这样的代码编译出错。 因为在authService中没有这样的方法签名。
再举个简单的例子, 我们想在打印字符串时,添加一些分隔符,下面是最自然的调用方式:
"hello,world" printWithSeperator "*"
很明显,这样的代码编译出错。 因为String 没有这样的方法。
Scala在面对编译出现类型错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找
一个隐式implicit
的转换方法,转换出正确的类型,完成编译。这就是implicit
的意义。
Implicit包含什么,有什么内在规则?
Scala 中的implicit
包含两个方面:
- 隐式参数(implicit parameters)
- 隐式转换(implicit conversion)
隐式参数(implicit parameters)
隐式参数同样是编译器在找不到函数需要某种类型的参数时的一种修复机制,我们可以采用显式的柯里化式
的隐式参数申明,也可以进一步省略,采用implicitly
方法来获取所需要的隐式变量。
隐式参数相对比较简单,Scala中的函数申明提供了隐式参数的语法,在函数的最后的柯里化参数
列表中可以添加隐式implicit
关键字进行标记, 标记为implicit
的参数在调用中可以省略,
Scala编译器会从当前作用域中寻找一个相同类型的隐式变量,作为调用参数。
在Scala的并发库中就大量使用了隐式参数,比如Future:
// Future 需要一个隐式的ExecutionContext
// 引入一个默认的隐式ExecutionContext, 否则编译不通过
import scala.concurrent.ExecutionContext.Implicits.default
Future {
sleep(1000)
println("I'm in future")
}
对于一些常量类的,可共用的一些对象,我们可以用隐式参数来简化我们的代码,比如,我们的应用
一般都需要一个配置对象:
object SomeApp extends App {
//这是我们的全局配置类
class Setting(config: Config) {
def host: String = config.getString("app.host")
}
// 申明一个隐式的配置对象
implicit val setting = new Setting(ConfigFactory.load)
// 申明隐式参数
def startServer()(implicit setting: Setting): Unit = {
val host = setting.host
println(s"server listening on $host")
}
// 无需传入隐式参数
startServer()
}
甚至,Scala为了更进一步减少隐式参数的申明代码,我们都可以不需要再函数参数上显示的申明,在scala.Predef
包中,提供了一个implicitly
的函数,帮助我们找到当前上下文中所需要类型的
隐式变量:
@inline def implicitly[T](implicit e: T) = e // for summoning implicit values from the nether world
因此上面的startServer
函数我们可以简化为:
// 省略隐式参数申明
def startServer(): Unit = {
val host = implicitly[Setting].host
println(s"server listening on $host")
}
需要注意的是,进一步简化之后,代码的可读性有所损失,调用方并不知道startServer
需要一个隐式的
配置对象,要么加强文档说明,要么选用显式的申明,这种权衡需要团队达成一致。
隐式转换(implicit conversion)
回顾一下前面说到的小例子,让字符串能够带分隔符打印:
"hello,world" printWithSeperator "*"
此时,Scala编译器尝试从当前的表达式作用域范围中寻找能够将String
转换成一个具有printWithSeperator
函数的对象。
为此,我们提供一个PrintOps
的trait
,有一个printWithSeperator
函数:
trait PrintOps {
val value: String
def printWithSepeator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
此时,编译仍然不通过,因为Scala编译器并没有找到一个可以将String
转换为PrintOps
的方法!那我们申明一个:
def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
OK, 我们可以显示地调用stringToPrintOps
了:
stringToPrintOps("hello,world") printWithSepeator "*"
离我们的最终目标只有一步之遥了,只需要将stringToPrintOps
方法标记为implicit
即可,除了为String
添加stringToPrintOps
的能力,还可以为其他类型添加,完整代码如下:
object StringOpsTest extends App {
// 定义打印操作Trait
trait PrintOps {
val value: String
def printWithSeperator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
// 定义针对String的隐式转换方法
implicit def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
// 定义针对Int的隐式转换方法
implicit def intToPrintOps(i: Int): PrintOps = new PrintOps {
override val value: String = i.toString
}
// String 和 Int 都拥有 printWithSeperator 函数
"hello,world" printWithSeperator "*"
1234 printWithSeperator "*"
}
隐式转换的规则 -- 如何寻找隐式转换方法
Scala编译器是按照怎样的套路来寻找一个可以应用的隐式转换方法呢? 在Martin Odersky的Programming in Scala, First Edition中总结了以下几条原则:
- 标记规则:只会去寻找带有
implicit
标记的方法,这点很好理解,在上面的代码也有演示,如果不申明为implicit
只能手工去调用。 - 作用域范围规则:
- 只会在当前表达式的作用范围之内查找,而且只会查找单一标识符的函数,上述代码中,
如果stringToPrintOps
方法封装在其他对象(加入叫Test)中,虽然Test
对象也在作用域范围之内,但编译器不会尝试使用Test.stringToPrintOps
进行转换,这就是单一标识符的概念。 - 单一标识符有一个例外,如果
stringToPrintOps
方法在PrintOps
的伴生对象中申明也是有效的,Scala
编译器也会在源类型或目标类型的伴生对象内查找隐式转换方法,本规则只会在转型有效。而一般的惯例,会将隐式转换方法封装在伴生对象中。 - 当前作用域上下文的隐式转换方法优先级高于伴生对象内的隐式方法
- 只会在当前表达式的作用范围之内查找,而且只会查找单一标识符的函数,上述代码中,
- 不能有歧义原则:在相同优先级的位置只能有一个隐式的转型方法,否则Scala编译器无法选择适当的进行转型,编译出错。
- 只应用转型方法一次原则:Scala编译器不会进行多次隐式方法的调用,比如需要
C
类型参数,而实际类型为A
,作用域内
存在A => B
,B => C
的隐式方法,Scala编译器不会尝试先调用A => B
,再调用B => C
。 - 显示方法优先原则:如果方法被重载,可以接受多种类型,而作用域中存在转型为另一个可接受的参数类型的隐式方法,则不会
被调用,Scala编译器优先选择无需转型的显式方法,例如:def m(a: A): Unit = ???
def m(b: B): Unit = ??? val b: B = new B //存在一个隐式的转换方法 B => A
implicit def b2a(b: B): A = ??? m(b) //隐式方法不会被调用,优先使用显式的 m(b: B): Unit
Implicit的应用模式有哪些?
隐式转换的核心在于将错误的类型通过查找隐式方法,转换为正确的类型。基于Scala编译器的这种隐式转换机制,通常有两种应用
模式:Magnet Pattern
和Method Injection
。
Magnet Pattern
Magnet Pattern
模式暂且翻译为磁铁模式
, 解决的是方法参数类型的不匹配问题,能够优雅地解决本文开头所提出的问题,
用一个通用的Service
方法签名来屏蔽不同版本、不同类型服务的差异。
磁铁模式的核心在于,将函数的调用参数和返回结果封装为一个磁铁参数,这样方法的签名就统一为一个了,不需要函数重载;再
定义不同参数到磁铁参数的隐式转换函数,利用Scala的隐式转换机制,达到类似于函数重载的效果。
磁铁模式广泛运用于Spray Http 框架,该框架已经迁移到Akka Http中。
下面,我们一步步来实现一个磁铁模式,来解决本文开头提出的问题。
定义
Magnet
参数和使用Magnet
参数的通用鉴权服务方法// Auth Magnet参数
trait AuthMagnet {
type Result
def apply(): Result
} // Auth Service 方法
trait AuthService {
def auth(am: AuthMagnet): am.Result = am()
}
实现不同版本的
AuthService
//v1 auth service
trait V1AuthService extends AuthService
//v2 auth service
trait V2AuthService extends AuthService
实现不同版本
AuthService
的伴生对象,添加适当的隐式转换方法//V1 版本的服务实现
object V1AuthService {
case class V1AuthRequest()
case class V1AuthResponse() implicit def toAuthMagnet(p: V1AuthRequest): AuthMagnet {type Result = V1AuthResponse} = new AuthMagnet {
override def apply(): Result = {
// v1 版本的auth 业务委托到magnet的apply中实现
println("这是V1 Auth Service")
V1AuthResponse()
}
override type Result = V1AuthResponse
}
} //V2 版本的服务实现
object V2AuthService {
case class V2AuthRequest()
case class V2AuthResponse() implicit def toAuthMagnet(p: V2AuthRequest): AuthMagnet {type Result = V2AuthResponse} = new AuthMagnet {
override def apply(): Result = {
// v2 版本的auth 业务委托到magnet的apply中实现
println("这是V2 Auth Service")
V2AuthResponse()
}
override type Result = V2AuthResponse
}
}
编写两个版本的资源接口(demo)
trait V1Resource extends V1AuthService {
def serv(): Unit = {
val p = V1AuthRequest()
val response = auth(p)
println(s"v1 resource response: $response")
}
} trait V2Resource extends V2AuthService {
def serv(): Unit = {
val p = V2AuthRequest()
val response = auth(p)
println(s"v2 resource response: $response")
}
} val res1 = new V1Resource {}
val res2 = new V2Resource {} res1.serv()
res2.serv()
控制台输出结果为:
这是V1 Auth Service
v1 resource response: V1AuthResponse()
这是V2 Auth Service
v2 resource response: V2AuthResponse()
Method Injection
Method Injection 暂且翻译为方法注入,意思是给一个类型添加没有定义的方法,实际上也是通过隐式转换来实现的,
这种技术在Scalaz中广泛使用,Scalaz为我们提供了和Haskell类似的函数式编程库。
本文中的关于printWithSeperator
方法的例子其实就是Method Injection
的应用,从表面上看,即是给String
和Int
类型添加了printWithSeperator
方法。
与Magnet Pattern
不同的是转型所针对的对象,Magnet Pattern
是针对方法参数进行转型,Method Injection
是针对调用对象进行转型。
举个简单的例子,Scala
中的集合都是一个Functor
,都可以进行map
操作,但是Java
的集合框架却没有,
如果需要对java.util.ArrayList
等进行map
操作则需要先转换为Scala
对应的类型,非常麻烦,借助Method Injection
,我们可以提供这样的辅助工具,让Java
的集合框架也成为一种Functor
,具备map
能力:
- 首先定义一个Functor
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A ⇒ B): F[B]
}
- 再定义一个FunctorOps
final class FunctorOps[F[_], A](l: F[A])(implicit functor: Functor[F]) {
def map[A, B](f: A ⇒ B): F[B] = functor.map(l)(f)
}
- 在FunctorOps的伴生对象中定义针对java.util.List[E]的隐式Funcotr实例和针对java.util.List[E]到
FunctorOps的隐式转换方法object FunctorOps {
// 针对List[E]的functor
implicit val jlistFunctor: Functor[JList] = new Functor[JList] {
override def map[A, B](fa: JList[A])(f: (A) => B): JList[B] = {
val fb = new JLinkList[B]()
val it = fa.iterator()
while(it.hasNext) fb.add(f(it.next))
fb
}
} // 将List[E]转换为FunctorOps的隐式转换方法
implicit def jlistToFunctorOps[E](jl: JList[E]): FunctorOps[JList, E] = new FunctorOps[JList, E](jl)
}
- 愉快滴使用map啦
val jlist = new util.ArrayList[Int]()
jlist.add(1)
jlist.add(2)
jlist.add(3)
jlist.add(4) import FunctorOps._
val jlist2 = jlist map (_ * 3)
println(jlist2)
// [3, 6, 9, 12]
总结
Implicit 是Scala语言中处理编译类型错误的一种修复机制,利用该机制,我们可以编写出任意参数和返回值的多态方法(这种多
态也被称为Ad-hoc polymorphism
-- 任意多态),实现任意多态,我们通常使用Magnet Pattern
磁铁模式;同时还可以
给其他类库的类型添加方法来对其他类库进行扩展,通常将这种技术称之为Method Injection
。
参考资料
- 《Programming in Scala》中关于隐式转换和隐式参数章节: http://www.artima.com/pins1ed/implicit-conversions-and-parameters.html
- 《The Magnet Pattern》http://spray.io/blog/2012-12-13-the-magnet-pattern/
Scala中的Implicit详解的更多相关文章
- Scala进阶之路-Scala高级语法之隐式(implicit)详解
Scala进阶之路-Scala高级语法之隐式(implicit)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 我们调用别人的框架,发现少了一些方法,需要添加,但是让别人为你一 ...
- php中关于引用(&)详解
php中关于引用(&)详解 php的引用(就是在变量或者函数.对象等前面加上&符号) 在PHP 中引用的意思是:不同的变量名访问同一个变量内容. 与C语言中的指针是有差别的.C语言中的 ...
- JavaScript正则表达式详解(二)JavaScript中正则表达式函数详解
二.JavaScript中正则表达式函数详解(exec, test, match, replace, search, split) 1.使用正则表达式的方法去匹配查找字符串 1.1. exec方法详解 ...
- AngularJS select中ngOptions用法详解
AngularJS select中ngOptions用法详解 一.用法 ngOption针对不同类型的数据源有不同的用法,主要体现在数组和对象上. 数组: label for value in a ...
- 【转载】C/C++中extern关键字详解
1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...
- oracle中imp命令详解 .
转自http://www.cnblogs.com/songdavid/articles/2435439.html oracle中imp命令详解 Oracle的导入实用程序(Import utility ...
- Android中Service(服务)详解
http://blog.csdn.net/ryantang03/article/details/7770939 Android中Service(服务)详解 标签: serviceandroidappl ...
- python中threading模块详解(一)
python中threading模块详解(一) 来源 http://blog.chinaunix.net/uid-27571599-id-3484048.html threading提供了一个比thr ...
- Android中mesure过程详解
我们在编写layout的xml文件时会碰到layout_width和layout_height两个属性,对于这两个属性我们有三种选择:赋值成具体的数值,match_parent或者wrap_conte ...
随机推荐
- .netframe初识
转发自:https://blog.csdn.net/bingshan5haoao/article/details/32966581 https://www.cnblogs.com/liuxx/p/35 ...
- JVM入门
面试过程中,问到JVM一脸懵逼,在github看了一些文章,感觉质量不错,整理了一下希望大家不吝赐教. 目前主流的jdk采用解释与编译混合执行的模式,其JIT技术采用分层编译,极大地提升了Java的执 ...
- java将一个List赋值给另一个List
声明和实例化: 声明:ArrayList a, 仅仅只是声明了一个list变量,其未来作用相当于C++中的引用变量,亦或者相当于一个对象块的索引,但并未为其分配具体的完整的对象所需要的内存空间,其所分 ...
- 《CSAPP》读书笔记
第一章 第二章 第三章 第四章 第五章 第六章 第七章 链接 可重定位目标文件 符号和符号表 符号解析 第八章 第九章 虚拟存储器 虚拟存储器 页表.页命中.缺页 地址翻译 第十章 第十一章 第十二章 ...
- CodeWarrior 10 自定义关键字模版
==============================================版本信息开始============================================ 相关作 ...
- POJ-3159.Candies.(差分约束 + Spfa)
Candies Time Limit: 1500MS Memory Limit: 131072K Total Submissions: 40407 Accepted: 11367 Descri ...
- React forwardRef:跳转引用
一 在DOM组件中使用 import React, { Component } from 'react'; // 跳转引用对象本身并不关心ref,而是由渲染函数转发ref const FancyBut ...
- 解决Jenkins用shell脚本部署后,Jenkins自动杀掉启衍生出来的守护进程
Jenkins部署java项目遇到的问题: 1.Jenkins执行构建后,需要手动执行startup.sh,站点才能正常访问 产生原因: shell脚本发布时,会衍生进程,Jenkins默认会自动杀掉 ...
- docker 删除所有none的镜像
docker images|grep none|awk '{print $3}'|xargs docker rmi
- centos中病毒
嗯 很开中了病毒,,,而且这是第二次了.... 然后大佬说让我 crontab -l 一下 然后试了下 然后出来这个东东 执行下 crontab -r 这个 然后就crontab -l 就 ...