Scala 隐式转换陷阱

3

编辑
好的,@Drexin提出了一个关于使用隐式转换器时类型安全性丢失/结果令人惊讶的好观点。

那么如何进行一种不太常见的转换,避免与预定义的隐式类型发生冲突?例如,我在Scala中使用JodaTime(很棒的项目!)。在同一个控制器包对象中,我的隐式定义了一个类型别名:

type JodaTime = org.joda.time.DateTime

同时还有一个隐式转换,将JodaTime类型转换为Long类型(在基于ScalaQuery构建的数据访问层中,日期存储为Long类型)

implicit def joda2Long(d: JodaTime) = d.getMillis

在这里,PreDef和我的控制器包含的隐式参数之间不存在歧义,而且控制器的隐式参数不会进入DAL,因为它们处于不同的包作用域。因此,当我执行以下操作时:

dao.getHeadlines(articleType, Some(jodaDate))

对于我来说,将隐式转换为Long是安全的,鉴于基于日期的查询被大量使用,我可以节省一些样板代码。

同样地,对于str2Int转换,控制器层接收Servlet URI参数作为String -> String。有许多情况下URI包含数字字符串,因此当我过滤路由以确定该字符串是否为Int时,我不想每次都进行stringVal.toInt;相反,如果正则表达式通过,则让隐式将字符串值转换为Int。全部代码如下:

implicit def str2Int(s: String) = s.toInt
get( """/([0-9]+)""".r ) {
  show(captures(0)) // captures(0) is String
}
def show(id: Int) = {...}

在上下文中,这些隐式转换是有效的用例吗?还是说总是要显式声明?如果是后者,那么有哪些有效的隐式转换用例?
原文: 在一个包对象中,我定义了一些隐式转换,其中一个是将简单的字符串转换为整数:
implicit def str2Int(s: String) = s.toInt

通常情况下,方法接收 Int 参数但实际传入 String 时会将其转换为 Int,返回类型为 Int 但实际返回值为 String 的方法也是如此。但有些情况下,编译器会出现模糊的隐式错误,这是令人头疼的。我知道发生这种情况的情况是在尝试进行手动内联字符串到整数的转换时。例如,val i = "10".toInt。我的解决方法/技巧是在 package object 中创建一个 asInt 帮助程序以及隐式,使用 asInt("10")。那么,隐式最佳实践是什么(即通过失败学习),还是应该遵循一些指导方针,以避免陷入自己制造的陷阱?换句话说,是否应该避免简单、常见的隐式转换,只在要转换的类型是唯一的情况下使用?(即永远不会陷入歧义陷阱)谢谢反馈,当隐式按预期工作时,它们是很棒的。

为什么要调用"10".toInt,当你的作用域中有一个隐式转换呢?你应该使用"10": Int,让隐式转换来处理它。(但我同意drexin的观点,通常不应该在作用域中拥有这种隐式转换。) - Rex Kerr
我并没有调用"10".toInt,那只是一个例子。我被新的JodaTime().toString("yyyy").toInt咬了一口。我不知道关于"10": Int语法的事情,现在知道了也很好。我编辑了我的答案以展示隐式转换被使用的上下文。 - virtualeyes
您现在的问题并没有包含一个问题。您能否尝试编辑并说明您遇到了什么问题? - Rex Kerr
隐式转换还是显式转换,这是个问题。我的编辑展示了我使用隐式转换的一些背景。到目前为止,回复都是“不要麻烦,显式转换就好”,所以我想知道隐式转换的“有效”用例是什么?我编辑了我的问题以反映这一点。 - virtualeyes
1
@virtualeyes 隐式转换以添加方法是可以的(也是不可避免的)。隐式转换以转换类型最多是危险的,应该尽量避免。简而言之,JavaConversions 不好,JavaConverters 很好。它们也可以与类似于构建器的模式一起使用,例如在 ScalaQuery 中,但是,真正的问题是另一个问题。不要改变问题,提出一个新问题。这是免费的。 - Daniel C. Sobral
+1,很明显(因为每个回复都是这样):避免直接类型转换;相反,像Rex的JodaTime版本那样去 pimp-my-library(指使用第三方库)来实现隐式转换(虽然我想不到一个情况,在其中将JodaTime实例传递给期望Long的方法,而该方法不是ScalaQuery DAL的一部分)。无论如何,最好不要养成坏习惯,所以我会遵循惯例。谢谢... - virtualeyes
2个回答

7

我认为你在混淆两种不同的用例。

在第一种情况下,你使用隐式转换来隐藏不同类之间任意的区别(或者对你来说是任意的)。在功能相同的情况下,这是很合适的。将 JodaTime 转换为 Long 隐式转换就属于这种情况;这可能是安全的,也很可能是一个好主意。我可能会使用丰富我的库模式并编写:

class JodaGivesMS(jt: JodaTime) { def ms = jt.getMillis }
implicit def joda_can_give_ms(jt: JodaTime) = new JodaGivesMS(jt)

在每个调用中都使用.ms是为了更加明确。原因是这里单位很重要(毫秒不是微秒不是秒不是毫米,但都可以表示为整数),我宁愿在大多数情况下在接口留下一些有关单位的记录。键入getMillis有点麻烦,但键入ms还好。转换是合理的(如果为可能修改代码的人(包括您)提供了良好的文档)。

然而,在第二种情况下,您正在执行不可靠的转换,将一个非常常见的类型转换为另一个类型。确实,您只在有限的上下文中执行此操作,但该转换仍然可能会逸出并引起问题(无论是异常还是不符合您意图的类型)。相反,您应该编写那些您需要的方便程序来正确地处理转换,并在各处使用它们。例如,假设您有一个字段,您希望该字段为“是”,“否”或整数。您可能会有类似以下的内容:

val Rint = """(\d+)""".r
s match {
  case "yes" => println("Joy!")
  case "no" => println("Woe!")
  case Rint(i) => println("The magic number is "+i.toInt)
  case _ => println("I cannot begin to describe how calamitous this is")
}

但是这段代码是错误的,因为"12414321431243".toInt会抛出异常,而你真正想要表达的是情况很严重。相反,你应该编写正确匹配的代码:

case object Rint {
  val Reg = """([-]\d+)""".r
  def unapply(s: String): Option[Int] = s match {
    case Reg(si) =>
      try { Some(si.toInt) }
      catch { case nfe: NumberFormatException => None }
    case _ => None
  }
}

现在可以使用这个方法来代替之前的做法。现在,当您执行匹配操作时,不再需要进行冒险的、隐式的从String转换为Int的转换,而是会正确地处理所有内容,包括正则表达式匹配(以避免在错误解析时抛出和捕获大量异常)和即使正则表达式通过也要处理异常。

如果您有一个既有字符串表示又有整数表示的东西,请创建一个新类,然后分别对它们进行隐式转换,如果您不想让您知道可以安全地使用哪个对象的方法调用重复执行没有提供任何启示。


非常好的回答,Rex,谢谢,这有助于更清楚地澄清隐式转换领域。我给出的例子是初学者开始探索的例子,所以知道我大致上在哪里(JodaTime),以及哪些方面偏离了轨道(容易出错的str2Int)。我想一般的经验法则是:要明确,如果你要采取隐式路线,就要以不会让自己困惑(当你稍后返回代码时)和其他可能需要解密你的魔法王国的人为前提;-) - virtualeyes

5
我尽量不会隐式地进行类型转换,只有在使用pimp my library模式时才进行转换。当你将一个字符串传递给需要整数的函数时,这可能有点令人困惑。此外,存在很大的类型安全性损失。如果你错误地将一个字符串传递给需要整数的函数,编译器无法检测到它,因为它假定你想这样做。因此,总是明确地进行类型转换,并且只使用隐式转换来扩展类。
编辑:
回答您更新后的问题:出于可读性的考虑,请使用显式的getMillis。在我看来,隐式用例的有效用例包括 "pimp my library"、视图/上下文边界、类型类、清单、构建器...,但不要过于懒惰,不写方法的显式调用。

我会支持“安全第一”的方法,给它点赞。 - virtualeyes
这是真的,我给出的隐式实现示例都是懒惰的;getMillis和toInt并不会对手指造成过多负担,我知道。 - virtualeyes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接