大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE稳定放心使用
文章目录
作用
Scala 中可以让函数库调用变得更加方便的隐式变换和隐式参数,以及如何通过它们来避免一些繁琐和显而易见的细节问题。 内容主要包括 implicits 的使用规则、隐含类型转换、转换被方法调用的对象等
解决什么问题
应用中自己写的代码和调用的第三方函数库有着一个基本的区别:也就是你可以任意修改和扩展自己写的代码。而一般来说,在没有源码的情况下,你很难扩展第三方函数库,函数库提供了什么就只能利用什么。
C 3.0 支持静态扩展方法,可以为已经定义的库、类进行扩展。
在 Scala 中,解决这个问题的途径是使用隐含类型变换和隐式参数。它们可以让函数库的调用变得更加方便,并避免一些繁琐和显而易见的细节问题。
Scala 的 implicit 可以有 implicit 类、方法和参数。
本项目课的几个实验将为你介绍 Scala 的隐式变换和隐式参数的用途。
使用 implicits 的一些规则
在 Scala 中的 implicit 定义,是指编译器在需要修复类型匹配时,可以用来自动插入的定义。比如说,如果 x + y 类型不匹配,那么编译器可能尝试使用 convert(x) + y, 其中 convert 就是由某个 implicit 定义的。
这有点类似于一个整数和一个浮点数相加,编译器可以自动把整数转换为浮点数。Scala 的 implicit 定义是对这种情况的一个推广,你可以定义一个类型,使其在需要时自动转换成另外一种类型。
Scala 的 implicit 定义符合下面一些规则:
3.1.1 标记规则
只有那些使用 implicit 关键字的定义才是可以使用的隐式定义。关键字 implicit 用来标记一个隐式定义。编译器随之可以选择它作为隐式变化的候选项。你可以使用 implicit 来标记任意变量,函数或是对象。
例如下面为一个隐式函数定义,尝试在Shell中输入这个语句:
implicit def intToString(x:Int) : x.toString
编译器只有在 convert 被标记成 implicit 后,才会将 x + y 改成 convert(x) + y。当然这是在 x + y 类型不匹配时。
3.1.2 范围规则
编译器在选择备选 implicit 定义时,只会选取当前作用域的定义。比如说,编译器不会去调用 someVariable.convert 。如果你需要使用 someVariable.convert ,你必须把 someVarible 引入到当前作用域。也就是说,编译器在选择备选 implicit 时,只有当 convert 是当前作用域下的单个标志符时,它才会作为备选 implicit 。举个例子,对于一个函数库而言,在一个 Preamble 对象中定义一些常用的隐式类型转换非常常见,因此需要使用 Preamble 的代码时,可以通过 “import Preamble._`” ,从而把这些 implicit 定义引入到当前作用域。
这个规则有一个例外,编译器也会在类的伙伴对象定义中查找所需的 implicit 定义。例如下面的定义:
object Dollar {
implicit def dollarToEuro(x:Dollar):Euro = ...
...
}
class Dollar {
...
}
你可以尝试补充上述代码中的细节,并在 Shell 中验证一下。
如果在 class Dollar 的方法有需要 Euro 类型,但输入数据使用的是 Dollar ,编译器会在其伙伴对象 object Dollar 查找所需的隐式类型转换,本例定义了一个从 Dollar 到 Euro 的 implicit 定义以供使用。
3.1.3 一次规则
编译器在需要使用 implicit 定义时,只会试图转换一次,也就是编译器永远不会把 x + y 改写成 convert1(convert2(x)) + y 。
3.1.4 优先规则
编译器不会在 x+y 已经是合法的情况下去调用 implicit 规则。
3.1.5 命名规则
你可以为 implicit 定义任意的名称。通常情况下,你可以任意命名, implicit 的名称只在两种情况下有用:一是你想在一个方法中明确指明;另外一个是想把那一个引入到当前作用域。
比如,我们定义一个对象,包含两个 implicit 定义:
object MyConversions {
implicit def stringWrapper(s:String):IndexedSeq[Char] = ...
implicit def intToString(x:Int):String = ...
}
请尝试补充代码中的细节部分,并在 Shell 中输入后验证一下。
在你的应用中,你想使用 stringWrapper 变换,而不想把整数自动转换成字符串,你可以只引入 stringWrapper 。
用法如下:
import MyConversions.stringWrapper
3.1.6 编译器使用 implicit 的几种情况
有三种情况使用 implicit :
转换成预期的数据类型:比如你有一个方法参数类型是 IndexedSeq[Char] ,在你传入 String 时,编译器发现类型不匹配,就会检查当前作用域是否有从 String 到 IndexedSeq 隐式转换。
转换 selection 的 receiver :这种情况下,允许你适应某些方法调用。比如 “abc”.exist ,“abc” 的类型为 String ,它本身没有定义 exist 方法,这时编辑器就检查当前作用域内 String 的隐式转换后的类型是否有 exist 方法。若发现 stringWrapper 转换后成 IndexedSeq 类型后,就会有 exist 方法,这个与 C 静态扩展方法功能类似。
隐含参数:隐含参数有点类似于缺省参数。如果在调用方法时没有提供某个参数,编译器会查找当前作用域是否有符合条件的 implicit 对象作为参数传入(有点类似 dependency injection )。
3.2 隐含类型转换
使用隐含转换将变量转换成预期的类型,是编译器最先使用 implicit 的地方。这个规则非常简单,当编译器看到类型 X 而却需要类型 Y ,它就在当前作用域查找是否定义了从类型 X 到类型 Y 的 隐式定义 。
比如,通常情况下,双精度实数不能直接当整数使用,因为会损失精度。你可以尝试在 Shell 中输入下面的语句来验证:
scala> val i:Int = 3.5
:7: error: type mismatch;
found : Double(3.5)
required: Int
val i:Int = 3.5
^
当然,你可以直接调用 3.5.toInt 。
这里我们定义一个从 Double 到 Int 的隐含类型转换的定义,然后再把 3.5 赋值给整数,这样就不会报错了。
尝试在 Shell 中输入下面的语句,然后再次验证一下:
scala> implicit def doubleToInt(x:Double) = x toInt
doubleToInt: (x: Double)Int
scala> val i:Int = 3.5
i: Int = 3
此时编译器看到一个浮点数 3.5 ,而当前赋值语句需要一个整数。此时,按照一般情况,编译器会报错。但在报错之前,编译器会搜寻是否定义了从 Double 到 Int 的隐含类型转换。在本例中,它找到了一个 doubleToInt 。因此编译器将把 val i:Int = 3.5
转换成 val i:Int = doubleToInt(3.5) 。
这就是一个隐含转换的例子。但是从浮点数自动转换成整数并不是一个好的例子,因为会损失精度。 Scala 在需要时会自动把整数转换成双精度实数,这是因为在 Scala.Predef 对象中定义了一个隐式转换:
implicit def int2double(x:Int) :Double = x.toDouble
而 Scala.Predef 是自动引入到当前作用域的,因此编译器在需要时,会自动把整数转换成 Double 类型。
3.3 转换被方法调用的对象
隐式变换也可以转换调用方法的对象。若编译器看到 X.method ,而类型 X 没有定义 method (包括基类)方法,那么编译器就查找作用域内定义的从 X 到其它对象的类型转换。而对于类型 Y ,若它定义了 method 方法,编译器就首先使用隐含类型转换把 X 转换成 Y ,然后调用 Y 的 method 。
下面我们看看这种用法的两个典型用法:
3.3.1 支持新的类型
这里我们使用课程《 Scala 开发教程》中讲解 Ordered Trait 时,定义的 Rational 类型为例。先来回顾一下其定义:
class Rational (n:Int, d:Int) {
require(d!=0)
private val g =gcd (n.abs,d.abs)
val numer =n/g
val denom =d/g
override def toString = numer + “/” +denom
def +(that:Rational) =
new Rational(
numer * that.denom + that.numer* denom,
denom * that.denom
)
def +(i:Int) :Rational =
new Rational(numer +1*denom,denom)
def * (that:Rational) =
new Rational( numer * that.numer, denom * that.denom)
def this(n:Int) = this(n,1)
private def gcd(a:Int,b:Int):Int =
if(b==0) a else gcd(b, a % b)
}
类 Rational 重载了两个 + 运算,参数类型分别为 Rational 和 Int。因此你可以把 Rational 和 Rational 相加,也可以把 Rational 和整数相加。
在 Shell 中输入下列语句测试一下,在这之前别忘了定义好 Rational 类。
scala> val oneHalf = new Rational(1,2)
oneHalf: Rational = 1/2
scala> oneHalf + oneHalf
res0: Rational = 1/1
scala> oneHalf + 1
res1: Rational = 3/2
但是我们如果使用 1 + oneHalf,会出现什么问题呢?测试一下:
scala> 1 + oneHalf
:10: error: overloaded method value + with alternatives:
(x: Double)Double
(x: Float)Float
(x: Long)Long
(x: Int)Int
(x: Char)Int
(x: Short)Int
(x: Byte)Int
(x: String)String
cannot be applied to (Rational)
1 + oneHalf
^
整数及其相关类型都没有定义与 Rational 类型相加的操作,因此编译器会报错。此时编译器在 1 能够转换成 Rational 类型才可以编译通过。因此,我们可以定义一个从整数到 Rational 的隐含类型变换。在 Shell 中输入下面这些语句:
scala> implicit def int2Rational(x:Int) = new Rational(x)
int2Rational: (x: Int)Rational
现在再执行 1+oneHalf:
scala> 1 + oneHalf
res3: Rational = 3/2
在定义了 int2Rational 之后,编译器看到 1 + oneHalf,发现 1 没有定义与 Rational 相加的操作,通常需要报错。但编译器在报错之前,查找了当前作用域里面从 Int 到其他类型的定义,而这个转换定义了支持与 Rational 相加的操作。例如在本例中,就发现了 int2Rational ,因此编译器将 1 + oneHalf 转换为:
int2Rational(1)+oneHalf
3.3.2 模拟新的语法结构
隐式转换可以用来扩展 Scala 语言,定义新的语法结构。比如,我们在定义一个 Map 对象时,可以使用如下语法:
Map(1 -> “One”, 2->“Two”,3->“Three”)
你有没有想过,在 -> 内部,是如何实现的? -> 不是 Scala 本身的语法,而是类型 ArrowAssoc 的一个方法。这个类型定义在包 Scala.Predef 对象中。
Scala.Predef 自动引入到当前作用域。在这个对象中,同时定义了一个从类型 Any 到 ArrowAssoc 的隐含转换。因此当使用 1 -> “One” 时,编译器自动插入从 1 到 ArrowAssoc 的转换。具体定义可以参考 Scala 源码。
利用这种特性,你可以定义新的语法结构,比如行业特定语言(DSL)。
实验总结
若要为现有的类库增加功能,用 java 的话就只能用工具类或者继承的方式来实现,而在 Scala 则采用隐式转化的方式来实现。这就是隐式变换带来的好处。当然了,具体问题需要具体分析,在哪些情况下需要用到隐式变换,就需要在今后的开发工作中勤加练习。
implicit基本含义
你的代码与其他人的函数库之间有根本差别:你可以随意或扩展自己的代码,但哪颗使用它人的,通常只能原封不动.
为缓解这一问题,scala引用隐式转换和隐式参数.它们可以通过忽略那些掩盖了代码中有趣部分的冗长的/过去细节的内容,让现在库函数处理起来更为得心应手.只要技巧性使用,代码会更专注程序的重点,而非细枝末节.
隐式转换
scala中一个核心集合特质 IndexedSeq[+A] ,它描述了建立在类型A的元素上随机访问序列.它具有数组或列表的实用方法. take/drop/map/filter/exists以及mkString是其中一部分例子.只要定义新的随机访问序列,唯一的工作就是扩展 IndexedSeq 特质.只须完成特质中两个抽象方法的定义:length 和 apply,于是特质中所有方法都可以”免费”获得了.
implicit def stringWrapper(s: String) =
new IndexedSeq[Char] {
override def length: Int = s.length
override def apply(i: Int): Char = s.charAt(i)
}
//调用
//println(stringWrapper("abc123") exists (_.isDigit))
println("abc123" exists (_.isDigit))
隐藏在背后是scala编译器已经为你加入了StringWrapper转换. 因此他实际上它把表达式改写为 stringWrapper("abc123") exists (_.isDigit)
,但表面上感觉好像是java的String已经获得了所有IndexedSeq特质的所有方法.
隐式转换的另一个优点是它们支持目标类型转换.
所谓目标类型,是指在代码某个占需要的类型.
eg.编写方法printWithSpaces,它可以打印指定随机访问序列中的字符并用空格作为字符的分隔.
def printWithSpaces(seq: IndexedSeq[Char])= seq mkString " "
implicit def stringWrapper(s: String) =
new IndexedSeq[Char] {
override def length: Int = s.length
override def apply(i: Int): Char = s.charAt(i)
}
println(printWithSpaces("xyz"))
//输出 x y z
由于String对象可以隐式转换为 IndexedSeq, 因此可以把字符串传递给 printWithSpaces
表达式等同于
println(printWithSpaces(stringWrapper("xyz")))
//输出 x y z
demo演示出了隐式转换如何用他们”装扮”现在库
隐式转换操作规则
隐式定义是指编译器为了修改类型错误而允许插入到程序中的定义.
eg. x + y
不能通过类型检查,那么编译器可能会把它改为 convert(x) + y
, 这里的convert是某个可用的隐式转换.
如果 convert 可以把x 改变为某种带了 + 方法的东西,那么这种转变可能修复程序以便让它通过类型检查并正确地执行.
如果 convert只是一个非常简单的转换函数,那么从源代码上省略将有助于净化代码.
隐式转换由以下能用规则掌控:
-
标记规则:只有标记为 implicit的定义才可用.
implicit关键字用来标记编译器可以用于隐式操作的声明.可以使用它的标记任何变量,函数 或 对象定义.
eg.隐式函数定义
implicit def int2String(x: Int) = x.toString
编译器将仅在 convert被标记为 implicit时把 x + y 转换为 convert(x) + y. -
作用域规则:插入的隐式转换必须以单一标识符的形式处理作用域中,或与转换的源或目标类型关联一起.
scala 编译器将仅考虑处于作用域之内的隐式转换.从而,为了使用隐式转换可用,必须以某种方式把它带入作用域之内.
此外,隐式转换主必须以单一标识符的形式(除了一种情况以外)进入作用域.编译器将不能插入形式为someVariable.convert的转换.
eg.不能 x + y someVariable.convert(x) + y.
因此想让someVariable.convert可以作用于隐式操作就需要把它引用进来,以作为单一标识符使它可用.一旦引用进来,编译器将可以自由使用convert(x) + y 这种方式.
“单一标识符”规则有一个例外.编译器还将在源类型或转换的期望目标类的伴生对象中寻找隐式定义.
eg.如果尝试传递 Dollar 对象给入参为 Euro的方法,源类型是 Dollar, 而目标类型是Euro. 因此你可以把从Dollar 到 Euro的隐式转换打包到Dollar或Euro类的伴生对象中.
object Dollar{
implicit def dollar2Euro(x: Dollar): Euro = ..
}
class Dollar {
...}
这里,dollar2Euro转换被称为是类型Dollar相关联的.编译器将在每次需要转换Dollar类型实例的时候实现这个关联转换,这样就不需要在程序中单独引用转换了.
隐式参数和spring 的依赖注入之前关系与区别
在Scala中有一个关键字是implicit, 之前一直不知道这个货是干什么的,今天整理了一下。
我们先来看一个例子:
def display(input:String):Unit = println(input)
我们可以看到,display函数的定义只是接受String类型的入参,因此调用display(“any string”)这样的函数是没问题的。但是如果调用display(1)这样的入参不是String类型的话,编译会出错的。
如果我们想让display函数也能够支持Int类型的入参的话,除了我们重新定一个def display(input:Int):Unit = println(input)这样的函数以外,我们还可以在相同的作用域内用implicit关键字定义一个隐式转换函数,示例代码如下:
object ImplicitDemo {
def display(input:String):Unit = println(input)
implicit def typeConvertor(input:Int):String = input.toString
implicit def typeConvertor(input:Boolean):String = if(input) "true" else "false"
// implicit def booleanTypeConvertor(input:Boolean):String = if(input) "true" else "false"
def main(args: Array[String]): Unit = {
display("1212")
display(12)
display(true)
}
}
我们定义了2个隐式转换函数:
implicit def typeConvertor(input:Int):String = input.toString
implicit def typeConvertor(input:Boolean):String = if(input) "true" else "false"
这样display函数就可以接受String、Int、Boolean类型的入参了。注意到上面我们的例子中注释的那一行,如果去掉注释的那一行的话,会在运行的时候出现二义性:
Error:(18, 13) type mismatch;
found : Boolean(true)
required: String
Note that implicit conversions are not applicable because they are ambiguous:
both method typeConvertor in object ImplicitDemo of type (input: Boolean)String
and method booleanTypeConvertor in object ImplicitDemo of type (input: Boolean)String
are possible conversion functions from Boolean(true) to String
display(true)
^
得出的结论是:
隐式转换函数是指在同一个作用域下面,一个给定输入类型并自动转换为指定返回类型的函数,这个函数和函数名字无关,和入参名字无关,只和入参类型以及返回类型有关。注意是同一个作用域。
隐式转换类(Implicit Classes)
Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。
隐式类有以下限制条件:
- 只能在别的trait/类/对象内部定义
用法
创建隐式类时,只需要在对应的类前加上implicit关键字。比如:
object Helpers {
implicit class IntWithTimes(x: Int) {
def times[A](f: => A): Unit = {
def loop(current: Int): Unit =
if(current > 0) {
f
loop(current - 1)
}
loop(x)
}
}
}
这个例子创建了一个名为IntWithTimes的隐式类。这个类包含一个int值和一个名为times的方法。要使用这个类,只需将其导入作用域内并调用times方法。比如:
scala> import Helpers._
import Helpers._
scala> 5 times println("HI")
HI
HI
HI
HI
HI
不一样的写法,但这里的作用是否一样呢?
implicit class RichDate(date: java.util.Date) // 正确!
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 正确!
implicit的应用
我们可以随便的打开scala函数的一些内置定义,比如我们最常用的map函数中->符号,看起来很像php等语言。
但实际上->确实是一个ArrowAssoc类的方法,它位于scala源码中的Predef.scala中。下面是这个类的定义:
final class ArrowAssoc[A](val __leftOfArrow: A) extends AnyVal {
// `__leftOfArrow` must be a public val to allow inlining. The val
// used to be called `x`, but now goes by `__leftOfArrow`, as that
// reduces the chances of a user's writing `foo.__leftOfArrow` and
// being confused why they get an ambiguous implicit conversion
// error. (`foo.x` used to produce this error since both
// any2Ensuring and any2ArrowAssoc pimped an `x` onto everything)
@deprecated("Use `__leftOfArrow` instead", "2.10.0")
def x = __leftOfArrow
@inline def -> [B](y: B): Tuple2[A, B] = Tuple2(__leftOfArrow, y)
def →[B](y: B): Tuple2[A, B] = ->(y)
}
@inline implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x)
我们看到 def ->[B] (y :B) 返回的其实是一个 Tuple2[A,B] 类型。
我们定义一个Map:
scala> val mp = Map(1->"game1",2->"game_2")
mp: scala.collection.immutable.Map[Int,String] = Map(1 -> game1, 2 -> game_2)
这里 1->”game1″其实是1.->(“game_1”)的简写。
这里怎么能让整数类型1能有->方法呢。
这里其实any2ArrowAssoc隐式函数起作用了,这里接受的参数[A]是泛型的,所以int也不例外。
调用的是:将整型的1 implicit转换为 ArrowAssoc(1)
看下构造方法,将1当作__leftOfArrow传入。
->方法的真正实现是生产一个Tuple2类型的对象(__leftOfArrow,y) 等价于(1, “game_id”)
这就是一个典型的隐式转换应用。
其它还有很多类似的隐式转换,都在Predef.scala中:
例如:Int,Long,Double都是AnyVal的子类,这三个类型之间没有继承的关系,不能直接相互转换。
在Java里,我们声明Long的时候要在末尾加上一个L,来声明它是long。
但在scala里,我们不需要考虑那么多,只需要:
scala> val l:Long = 10
l: Long = 10
这就是implicit函数做到的,这也是scala类型推断的一部分,灵活,简洁。
其实这里调用是:
val l : Long = int2long(10)
更牛的功能
为现有的类库增加功能的一种方式,用java的话,只能用工具类或者继承的方式来实现,而在scala则还可以采用隐式转化的方式来实现。
隐式参数
看一个例子再说:
object ImplictDemo {
object Context{
implicit val ccc:String = "implicit"
}
object Param{
def print(content:String)(implicit prefix:String){
println(prefix+":"+content)
}
}
def main(args: Array[String]) {
Param.print("jack")("hello")
import Context._
Param.print("jack")
}
}
程序运行结果为:
hello:jack
implicit:jack
隐式转换扩展
import java.io.File
import scala.io.Source
class RichFile(val file:File){
def read = Source.fromFile(file.getPath()).mkString
}
object Context{
implicit def file2RichFile(f:File)= new RichFile(f)
}
object ImplictDemo {
def main(args: Array[String]) {
import Context.file2RichFile
println(new File("f:\\create.sql").read)
}
}
上面的代码中调用的read方法其实就是RichFile中定义的read方法。
最后的总结:
- 记住隐式转换函数的同一个scop中不能存在参数和返回值完全相同的2个implicit函数。
- 隐式转换函数只在意 输入类型,返回类型。
- 隐式转换是scala的语法灵活和简洁的重要组成部分
Scala 不仅重用了Java 的类型,还把它们“打扮”得更漂亮。
例如,Scala的字串支持类似于 toInt 和 toFloat 的方法,可以把字串转换成整数或者浮点数。因此你可以写str.toInt 替代 Integer.parseInt(str)。
如何在不打破互操作性的基础上做到这点呢?
Java 的 String 类当然不会有 toInt方法。 实际上,Scala 有一个解决这种高级库设计和互操作性不相和谐的通用方案。Scala可以让你定义隐式转换:implicit conversion,这常常用在类型失配,或者选用不存在的方法时。
在上面的例子里,当在字串中寻找toInt方法时,Scala 编译器会发现String类里没有这种方法,但它会发现一个把Java的String转换为Scala 的 RichString 类的一个实例的隐式转换,里面定义了这么个方法。于是在执行toInt操作之前,转换被隐式应用。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/183110.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...