1. 概述
在学习或使用 Scala 编程语言时,我们迟早都会接触到 implicitly
这个函数。但真正深入理解它的人并不多。
因此,本文将分析 implicitly
函数的语义及其主要用途。
2. “Implicitly” 的含义
假设我们正在编写一个函数,用于计算一个物体的重量,参数是质量(mass)和某个星球上的重力加速度常数(gravitational constant):
def weight(mass: Double, gravitationalConstant: Double): Double =
mass * gravitationalConstant
当我们开始频繁使用这个 weight
函数后,发现每次都显式传递 gravitationalConstant
显得多余。
这不仅违反了 KISS 原则(Keep It Simple, Stupid),也降低了代码可读性。
不过,我们可以借助编译器自动填充这个参数。为此,Scala 提供了 隐式参数(implicit parameters) 机制:
def weightUsingImplicit(mass: Double)(implicit gravitationalConstant: Double): Double =
weight(mass, gravitationalConstant)
现在只需要在调用该函数的作用域内提供一个 implicit
标记的 Double
类型值即可:
implicit val G: Double = 9.81
但你可能觉得还不够优雅 —— 为什么还要显式声明那个 implicit
参数?能不能让编译器自己搞定?
这时候,Scala 2.8 引入了一个非常实用的工具:implicitly
函数。它位于 Predef
包中,无需导入即可使用:
def implicitly[T](implicit e: T) = e
✅ 简单粗暴地说,implicitly
就是“隐式值的编译器查询接口”。我们可以用它来验证当前作用域是否存在某个类型的隐式值。如果不存在,编译器会报错。
那么如何利用 implicitly
来优化我们的 weight
函数呢?
我们可以去掉那个显式的隐式参数,并改用 implicitly
获取隐式值:
def weightUsingImplicitly(mass: Double): Double = {
val gravitationalConstant = implicitly[Double]
weight(mass, gravitationalConstant)
}
这样做的本质是:**请求编译器查找一个类型为 Double
的隐式值并赋给变量 gravitationalConstant
**。
⚠️ 不过需要指出的是,上面这种直接对基本类型使用 implicitly
并不常见。它只是构建类型类(type class)模式的基础砖块之一。接下来我们就来聊聊类型类。
3. 类型类:释放隐式解析的真正力量
3.1. 类型类基础
众所周知,Scala 的很多设计思想来源于 Haskell,而类型类(Type Class)就是其中之一。
📌 类型类定义了一组行为或特性,泛型类型 T
可以实现这些行为。它的概念类似于接口(interface),在 Scala 中通常通过 trait
实现:
trait Searchable[T] {
def uri(obj: T): String
}
更多关于 trait
的内容可以参考:Introduction to Traits in Scala
以上定义了一个类型类,使得类型 T
具备“可搜索”的能力 —— 即拥有对应的 URI 地址。任何想参与 Searchable
类型类的类都需要实现 uri
方法。
💡 类型类正是 Scala(以及 Haskell)实现 特设多态(ad-hoc polymorphism) 的方式。也就是说,接收实现了 Searchable
trait 的类型的方法可以表现出不同的行为:
def searchWithImplicit[S](obj: S)(implicit searchable: Searchable[S]): String = searchable.uri(obj)
假设我们有两个类型 Customer
和 Policy
,我们希望它们都具备可搜索的能力:
case class Customer(taxCode: String, name: String, surname: String)
case class Policy(policyId: String, description: String)
我们需要分别为这两个类型实现 Searchable
trait,可以通过匿名类的方式:
implicit val searchableCustomer: Searchable[Customer] = new Searchable[Customer] {
override def uri(customer: Customer): String = s"/customers/${customer.taxCode}"
}
implicit val searchablePolicy: Searchable[Policy] = new Searchable[Policy] {
override def uri(policy: Policy): String = s"/policies/${policy.policyId}"
}
由于编译器的隐式解析机制,searchWithImplicit
方法的行为会根据传入的对象类型动态变化:
- 如果传入的是
Customer
,则使用searchableCustomer
- 如果是
Policy
,则使用searchablePolicy
更多信息请参见:Type Classes in Scala
3.2. 类型类与 implicitly 函数的结合
好了,类型类很强大,那 implicitly
在这里起什么作用呢?
还记得我们说过,implicitly
是用来“查找隐式值”的。但是这次我们不是要找 Customer
或 Policy
这样的具体类型,而是要找像 Searchable[Customer]
或 Searchable[Policy]
这种“包装类型”。
为了解决这个问题,Scala 从 2.8 版本开始支持 上下文界定(context bound)。语法如下:
def searchWithContextBound[S: Searchable](obj: S): String
📌 上下文界定的意思是:类型 S
必须存在一个对应的 Searchable[S]
实例。
既然我们知道 Searchable[S]
一定存在(因为有上下文界定),就可以使用 implicitly
来获取它:
def searchWithContextBound[S: Searchable](obj: S): String = {
val searchable = implicitly[Searchable[S]]
searchable.uri(obj)
}
✅ 这样一来,函数签名变得更干净了 —— 不再暴露隐式参数,只保留业务相关的参数。
举个例子,分别对 Customer
和 Policy
使用这个函数:
val customer = Customer("123456", "Will", "Smith")
val uri = searchWithContextBound(customer)
assert(uri == "/customers/123456")
val policy = Policy("09876", "A policy")
val uri = searchWithContextBound(policy)
assert(uri == "/policies/09876")
🎯 最后,如果我们想为一个新的类型添加支持,只需要新增一个 Searchable
trait 的匿名实现,剩下的交给编译器自动解析即可,非常方便!
4. 总结
在这篇文章中,我们介绍了自 Scala 2.8 起就存在的 implicitly
函数。
我们首先展示了它作为“隐式值查找器”的基本用法,然后重点讲解了它在类型类模式中的应用 —— 让代码更加简洁、清晰。
一如既往,完整的示例代码可以在 GitHub 上找到: 👉 Baeldung/scala-tutorials - scala-core-modules/scala-core-4