Scala 3 — 书籍

隐式转换

语言

隐式转换是 Scala 的一项强大功能,允许用户提供一个类型的参数,就像它是另一个类型一样,以避免样板。

请注意,在 Scala 2 中,隐式转换还用于向封闭类提供其他成员(请参见 隐式类)。在 Scala 3 中,我们建议通过定义 扩展方法来解决此用例,而不是隐式转换(尽管出于历史原因,标准库仍然依赖于隐式转换)。

示例

例如,考虑一个方法 findUserById,它采用一个类型为 Long 的参数

def findUserById(id: Long): Option[User]

为了简洁起见,我们省略了类型 User 的定义,它对我们的示例并不重要。

在 Scala 中,可以调用方法 findUserById,其参数类型为 Int,而不是预期的类型 Long,这是因为该参数将隐式转换为类型 Long

val id: Int = 42
findUserById(id) // OK

此代码不会因“类型不匹配:预期 Long,但找到 Int”之类的错误而编译失败,这是因为有一个隐式转换,将参数 id 转换为类型 Long 的值。

详细说明

本节介绍如何定义和使用隐式转换。

定义隐式转换

在 Scala 2 中,从类型 S 到类型 T 的隐式转换由 隐式类 T 定义,该类采用一个类型为 S 的构造函数参数,一个函数类型 S => T隐式值,或一个可转换为该类型值的隐式方法。

例如,以下代码定义了从 IntLong 的隐式转换

import scala.language.implicitConversions

implicit def int2long(x: Int): Long = x.toLong

这是一个可转换为类型 Int => Long 的值隐式方法。

请参阅下面的“小心隐式转换的力量”一节,了解开头部分的子句 import scala.language.implicitConversions 的说明。

在 Scala 3 中,从类型 S 到类型 T 的隐式转换由类型 scala.Conversion[S, T]given 实例 定义。为了与 Scala 2 兼容,也可以通过隐式方法来定义(在 Scala 2 选项卡中了解更多信息)。

例如,此代码定义了从 IntLong 的隐式转换

given int2long: Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

与其他 given 定义一样,隐式转换可以是匿名的

given Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

使用别名,可以更简洁地表示为

given Conversion[Int, Long] = (x: Int) => x.toLong

使用隐式转换

隐式转换在两种情况下应用

  1. 如果表达式 e 的类型为 S,并且 S 不符合表达式的预期类型 T
  2. 在选择 e.m 中,其中 e 的类型为 S,如果选择器 m 不表示 S 的成员(以支持 Scala-2 风格的 扩展方法)。

在第一种情况下,将搜索适用于 e 且其结果类型符合 T 的转换 c

在上面的示例中,当我们将类型为 Int 的参数 id 传递给方法 findUserById 时,将插入隐式转换 int2long(id)

在第二种情况下,将搜索适用于 e 且其结果包含名为 m 的成员的转换 c

一个示例是比较两个字符串 "foo" < "bar"。在这种情况下,String 没有成员 <,因此将插入隐式转换 Predef.augmentString("foo") < "bar"。(scala.Predef 会自动导入到所有 Scala 程序中。)

如何将隐式转换引入范围?

当编译器搜索适用的转换时

  • 首先,它会查看当前词法范围
    • 在当前范围或外部范围内定义的隐式转换
    • 导入的隐式转换
    • 通过通配符导入导入的隐式转换(仅限 Scala 2)
  • 然后,它会查看与参数类型 S 或预期类型 T 关联的 伴生对象。与类型 X 关联的伴生对象是
    • 伴生对象 X 本身
    • X 的任何继承类型关联的伴生对象
    • X 中的任何类型参数关联的伴生对象
    • 如果 X 是内部类,则为其嵌入其中的外部对象

例如,考虑在对象 Conversions 中定义的隐式转换 fromStringToUser

import scala.language.implicitConversions

object Conversions {
  implicit def fromStringToUser(name: String): User = (name: String) => User(name)
}
object Conversions:
  given fromStringToUser: Conversion[String, User] = (name: String) => User(name)

以下导入将等效地将转换引入范围

import Conversions.fromStringToUser
// or
import Conversions._
import Conversions.fromStringToUser
// or
import Conversions.given
// or
import Conversions.{given Conversion[String, User]}

请注意,在 Scala 3 中,通配符导入(即 import Conversions.*)不会导入给定定义。

在介绍性示例中,从 IntLong 的转换不需要导入,因为它是在对象 Int 中定义的,该对象是类型 Int 的伴生对象。

进一步阅读:Scala 在哪里查找隐式对象?(在 Stackoverflow 上)

小心隐式转换的力量

由于隐式转换在不加选择地使用时可能存在缺陷,因此编译器在编译隐式转换定义时会发出警告。

要关闭警告,请执行以下任一操作

  • scala.language.implicitConversions 导入隐式转换定义的范围
  • 使用 -language:implicitConversions 调用编译器

当编译器应用转换时,不会发出任何警告。

由于隐式转换在不加选择地使用时可能存在缺陷,因此编译器在两种情况下发出警告

  • 编译 Scala 2 样式的隐式转换定义时。
  • 在将 scala.Conversion 的给定实例作为转换插入的调用位置。

要关闭警告,请执行以下任一操作

  • scala.language.implicitConversions 导入以下范围
    • Scala 2 样式的隐式转换定义
    • scala.Conversion 的给定实例作为转换插入的调用位置。
  • 使用 -language:implicitConversions 调用编译器

此页面的贡献者