scala 隐式转换

Scala的隐式转换和参数使得代码更加简洁,允许库函数扩展。隐式转换在类型不匹配时自动进行,如从Int到Double。规则包括标记、范围、一次、优先和命名。隐式参数类似于默认参数,当未提供时,编译器寻找合适的隐式对象。实验展示了如何通过隐式转换支持新的类型和模拟新语法结构,例如将整数转换为Rational类型。隐式转换和参数是Scala中增强代码灵活性的关键工具。

作用

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.d

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大怀特

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值