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)

假设我们有两个类型 CustomerPolicy,我们希望它们都具备可搜索的能力:

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 是用来“查找隐式值”的。但是这次我们不是要找 CustomerPolicy 这样的具体类型,而是要找像 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)
}

✅ 这样一来,函数签名变得更干净了 —— 不再暴露隐式参数,只保留业务相关的参数。

举个例子,分别对 CustomerPolicy 使用这个函数:

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


原始标题:Implicitly in Scala

« 上一篇: Alpakka 介绍
» 下一篇: Scala中的重复参数