7.5 隐式类型转换

假设我们要创建一个应用,其中包含了几种日期和时间的操作。如果代码可以写成下面这样,就会相当方便,更加可读:

  1. 2 days ago
  2. 5 days from_now

上面的代码看起来不像代码,更像是数据输入——这是DSL的特征之一。可选的点和括号在这里起到了作用。在第一个语句里,我们调用了2days()方法,传入了一个变量ago。在第二个语句里,调用了5的方法,传的变量是from_now

如果编译上面的代码,Scala会提示days()不是Int的方法。是的,Int没有提供这个方法,但是这并不能阻止我们写出这样的代码。就让Scala安静地把Int转换成什么东西,帮我们完成上面这个操作——进入隐式类型转换的世界吧!

隐式类型转换可以帮助我们扩展语言,创建“专用于特定应用和领域”的词汇或语法,也可以帮助我们创建属于自己的领域专用语言。

为了先理解这些概念,我们从一个恶心的代码开始,然后把它重构成一个漂亮的类。

我们需要定义变量agofrom_now,让Scala接收days()方法。定义变量很简单,接收方法却不容易。我们创建一个类DateHelper,其构造函数可以以一个Int为参数:

  1. import java.util._
  2. class DateHelper(number: Int) {
  3. def days(when: String) : Date = {
  4. var date = Calendar.getInstance()
  5. when match {
  6. case "ago" => date.add(Calendar.DAY_OF_MONTH, -number)
  7. case "from_now" => date.add(Calendar.DAY_OF_MONTH, number)
  8. case _ => date
  9. }
  10. date.getTime()
  11. }
  12. }

DateHelper类提供我们想要的days()方法②。现在,我们所需做的就是把Int转化为DateHelper。可以用一个方法来做这件事,接收一个Int,返回一个DataHelper的实例。简单的把方法标记为implicit,只要它在当前范围内存在(通过当前import可见,或是位于当前文件),Scala就会自动调用它。

days()方法里用到的match()方法是Scala的模式匹配功能的一部分,在第9章“模式匹配和正则表达式”中会有讲解。

代码如下:

  1. implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)
  2. val ago = "ago"
  3. val from_now = "from_now"
  4. val past = 2 days ago
  5. val appointment = 5 days from_now
  6. println(past)
  7. println(appointment)

如果把上面的代码同DateHelper的定义一起运行,Scala就会自动把给定的数字转换为一个DateHelper实例,然后,调用days()方法。

现在,代码已经可以工作了,是时候稍做清理了。我们并不想在每次需要转换时都去写隐式转换器。把这个转换器放到一个单独的单例对象里,可以获得更好的重用性,也更加易用。可以把转换器挪到DateHelper的伴生对象里:

TraitsAndTypeConversions/DateHelper.scala

  1. import java.util._
  2. class DateHelper(number: Int) {
  3. def days(when: String) : Date = {
  4. var date = Calendar.getInstance()
  5. when match {
  6. case DateHelper.ago => date.add(Calendar.DAY_OF_MONTH, -number)
  7. case DateHelper.from_now => date.add(Calendar.DAY_OF_MONTH, number)
  8. case _ => date
  9. }
  10. date.getTime()
  11. }
  12. }
  13. object DateHelper {
  14. val ago = "ago"
  15. val from_now = "from_now"
  16. implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)
  17. }

导入DateHelper时,Scala会自动的找到转换器。这是因为Scala会在当前范围和导入的范围内进行转换。

下面是一个例子,用到了在DateHelper里写的隐式转换:

TraitsAndTypeConversions/DaysDSL.scala

  1. import DateHelper._
  2. val past = 2 days ago
  3. val appointment = 5 days from_now
  4. println(past)
  5. println(appointment)

结果如下:

  1. Sun Dec 07 13:11:06 MST 2008
  2. Sun Dec 14 13:11:06 MST 2008

在Predef对象里,Scala已经定义了一些隐式转换,Scala会默认导入它们。这样的话,比如说,当我们写1 to 3时,Scala就会隐式的将1Int转换为其富封装器RichInt,然后,调用to()方法。

Scala一次至多应用一个隐式转换。在当前范围内,如果发现通过类型转换有助于操作、方法调用或类型转换的成功完成,就会进行转换。

在本章里,我们学到了Scala两个有趣的特性:trait和隐式转换。这两个概念有助于以动态行为创建出可扩展的代码,超越单独一个类所能提供的范畴。在下一章里,我们会看到Scala对对象容器的支持。