scala学习笔记 - 隐式转换

25 篇文章 0 订阅
15 篇文章 2 订阅
Scala的隐式转换允许在不匹配的类型之间进行转换,增强了类库的功能。通过声明implicit函数或隐式类,可以自动将一个类型转换为另一个。例如,将整数转换为分数以便进行数学运算。这种转换可以通过在伴生对象中定义,或在当前作用域引入来实现。然而,过度使用或不恰当的隐式转换可能导致代码难以理解和维护。理解何时和如何使用隐式转换是关键,以避免潜在问题。
摘要由CSDN通过智能技术生成

隐式转换

在Scala中,隐式转换函数(implicit conversion function)指的是那种以implicit关键字声明的带有单个参数的函数。正如它的名称所表达的那样,这样的函数将被自动应用,将值从一种类型转换为另一种类型。
如下,有一个*方法用来将两个分数相乘,我们想把整数n转换成分数n / 1:

implicit def int2Fraction(n: Int) = Fraction(n, 1)

这样我们就可以做如下表达式求值:

val result = 3 * Fraction(4, 5) // 将调用 int2Fraction(3)

隐式转换函数将整数3转换成了一个 Fraction对象,这个对象接着又被乘以Fraction(4, 5)。
说明:尽管Scala提供了微调隐式转换的工具,语言的设计者们也意识到隐式转换可能有潜在的问题,为了避免在使用隐式函数时出现警告,我们可以添加import scala.language.implicitConversions语句或编译器选项-language implicitConversions
说明:C++中,可以以单参数构造器或者名为operator Type()的成员函数来指定隐式转换。不过,在C++中,你无法有选择地允许或禁止这些函数,因而得到不想要的转换是常有的事。

利用隐式转换丰富现有类库的功能

当希望某个类有某个方法,而这个类的作者却没有提供?举例来说,如果java.io.File类能有个read方法来读取文件,这该多好:

val contents = new File("file").read

在Scala中,可以定义一个经过丰富的类型,提供想要的功能:

class RichFile(val from: File) { 
  def read = Source.fromFile(from.getPath).mkString
}

然后,再提供一个隐式转换来将原来的类型转换到这个新的类型:

implicit def file2RichFile(from: File) = new RichFile(from)

这样,就可以在File对象上调用read方法了,它被隐式地转换成了一个RichFile。
除了提 个转换函数,你还可以将RichFile声明为隐式类(implicit class):

implicit class RichFile(val from: File){ ... }

隐式类必须有一个单入参的主构造器。该构造器自动成为那个隐式的转换函数。
将这个经过丰富的类声明为值类(value class)是一个不错的主意:

implicit class RichFile(val from: File) extends AnyVal{ ... }

这样,不会有RichFile的对象被创建出来,file.read的调用将直接被编译成一个静态的方法调用RichFile$.read$extension(file)。
注意:隐式类不能是顶层的类,可以将它放在使用类型转换的类中,或者另一个对象或类中,然后引人。

引入隐式转换

Scala会考虑如下的隐式转换函数:

  1. 位于源或目标类型的伴生对象中的隐式函数或隐式类。
  2. 位于当前作用域中可以以单个标识符指代的隐式函数或隐式类。
    比如int2Fraction函数,可以将它放到Fraction伴生对象中,这样它就能够被用来将整数转换成分数了。
    或者,假定把它放到了FractionConversions对象当中,而这个对象位于com.demo.impatient包,如果想要使用这个转换,就需要引入FractionConversions对象,像这样:
import com.demo.impatient.FractionConversions._

为了让隐式转换在当前作用域可见,它必须是不带前缀的,例如,如果你引入了com.demo.impatient.FractionConersions或com.demo,那么,对于所有想要显示调用int2Fraction方法的人而言,在当前作用域内可以使用FractionConversions.int2Fraction或impatient.FractionConversions.
int2Fraction来调用它。不过如果该函数不是以int2Fraction可见, 不带任何前缀的话,编译器是不会隐式地使用它的。
在REPL中,键入:implicits以查看所有除Predef外被引入的隐式成员,或者键入:implicits -v以查看全部隐式成员。
可以将引人局部化以尽量避免不想要的转换发。例如:

object Main extends App { 
  import com.demo.impatient.FractionConversions._
  val result = 3 * Fraction(4, 5) // 使用 引入的转换
  println(result)
}

甚至可以选择自己想要的特定转换,假定你有另一个转换:

object FractionConversions { 
  ...
  implicit def fraction2Double(f: Fraction) = f.nurn * 1.0 / f.den
}

如果你想要的是这一个转换而不是int2Fraction,则可以直接引人它:

import com.demo.impatient.FractionConversions.fraction2Double
val result = 3 * Fraction(4, 5) // 结果是2.4

如果某个特定的隐式转换给你带来麻烦,也可以将它排除在外:

// 引入除fraction2Double外的所有成员
import com.demo.impatient.FractionConversions.{ fraction2Double => _, _}

提示:如果你想要搞清楚为什么编译器没有使用某个你认为它应该使用的隐式转换 ,可以试着将它显式加上,例如调用fraction2Double(3) * Fraction(4, 5),你可能就会得到显示问题所在的错误提示了。

隐式转换规则

在本节中,你将看到编译器何时会尝试隐式转换。为了说明规则,我们再次以Fraction类为例,假定int2Fractio和fraction2Double这两个隐式转换是可用的。
隐式转换在如下三种各不相同的情况下会被考虑:

  • 当表达式 类型与预期的类型不同时:
3 * Fraction(4, 5) // 将调用fraction2Double

Int类并没有一个*(Fraction)方法,不过它有一个*(Double)方法。

  • 当对象访问一个不存在的成员时:
3.den // 将调用int2Fraction

Int类没有den这个成员,但Fraction类有。

  • 与对象调用某个方法,而该方法的参数声明与传人参数不匹配时:
Fraction(4, 5) * 3 // 将调用 int2Fraction

Fraction的*方法并不接受一个Int ,但它接受Fraction。
另一方面,在以下三种情况下编译器不会尝试使用隐式转换:

  • 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换。举例来说,如果ab能够编译,那么编译器不会尝试aconvert(b)或者conver(a)*b。
  • 编译器不会尝试同时执行多个转换,比如convert1(convert(a))*b。
  • 存在二义性的转换是一个错误。举例来说,如果convert1(a)*b和convert2(a)*b都是合法的,编译器将会报错。
    注意,如下情况并不属于二义性:
3 * Fraction(4 , 5)
// 既可以被转换成
3 * fraction2Double(Fraction(4, 5))
// 也可以被转换成
int2Fraction(3) * Fraction(4, 5)

第一个转换将会胜出,因为它无须改变被应用*方法的那个对象。
提示: 如果你想要弄清楚编译器使用了那些隐式转换,可以用如下命令行参数来编译自己的程序:

scalac -Xprint:typer MyProg.scala // 会看到加入隐式转换后的源码

参考:快学scala(第二版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值