1. 概述
领域特定语言(Domain-Specific Language, DSL)是一种专门用于解决特定领域问题的编程语言。
内部 DSL(Internal DSL)是构建在宿主语言之上的,不需要我们自己开发解析器。但它的能力受限于宿主语言本身的灵活性。对于像 Java 或 C# 这样语法较为严格、扩展性有限的语言来说,最多只能做到提供流畅接口(Fluent Interface)风格的类库。
✅ Scala 凭借其灵活的语法特性,是编写内部 DSL 的理想选择。
在本教程中,我们将构建一个简单的 DSL,使得我们可以直接写:
20 seconds
而不是:
new FiniteDuration(20, SECONDS)
2. 一点提醒
Scala 并发包中的 scala.concurrent.duration
实现在 2.10 版本之前是基于 隐式转换 实现的。而我们现在将使用 隐式类(implicit class) 来替代。
这两种方式功能上等价,但隐式类更简洁且安全 ✅。
3. 它是怎么工作的?
为了实现这种“语法糖”,我们需要做两件事:
- 创建一个包装类
- 将该类标记为隐式类
之后,Scala 编译器会自动帮我们完成很多语法层面的优化。
3.1. 包装类的设计
我们先来创建一个包装类 class:
package com.baeldung.scala
import scala.concurrent.duration.{FiniteDuration, MILLISECONDS, MINUTES, SECONDS}
class DurationSugar(time: Long) {
def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)
def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)
def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
}
现在你可以这样调用:
(new DurationSugar(20)).seconds
看起来并没有简化多少 😅。但我们已经有了一个可以扩展的方法模块,后续可以轻松添加如 hours
等单位的支持。
⚠️ 注意:这里必须显式导入 scala.concurrent.duration
中的具体成员,而不是通配符导入。
3.2. 使用隐式类提升体验
由于 DurationSugar
只接受一个参数,我们可以将其标记为隐式类。但要注意:
❌ 隐式类不能作为顶级定义(top-level definition),否则编译器会报错。
我们的目标是只需一行导入,就能在整个作用域内使用这个语法糖:
import com.baeldung.scala.durationsugar._
object Main {
println(20 seconds)
}
要实现这一点,只需把类放到 package object 中并声明为 implicit class:
package object durationsugar {
implicit class DurationSugar(time: Long) {
def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)
def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)
def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
}
}
✅ 命名建议:隐式类的名字尽量长一些,避免与用户代码中的变量或方法冲突。
⚠️ 隐式类的构造函数不能超过一个参数。虽然 Scala 允许定义多个参数的隐式类,但它不会参与隐式查找过程,就失去了意义。
3.3. 其余部分由 Scala 自动处理
剩下的语法支持都是免费的 🎁。
比如:
可以省略无参方法后面的括号:
20.seconds
在大多数情况下,也可以省略点号
.
, 写成:20 seconds
举个例子:
"20.seconds" should "equal the object created with the native scala sugar" in {
20.seconds shouldBe new FiniteDuration(20, SECONDS)
}
⚠️ 但要注意:点号省略语法在某些复杂上下文中可能会让编译器混淆,特别是在后面还有链式调用时。此时最好保留点号,否则可能遇到令人困惑的错误提示:
4. 让它更甜一点
隐式类不仅能用来构造对象,还可以用来给已有的类扩展新方法。
比如我们可以给 FiniteDuration
添加一个新的操作符方法 ++
:
implicit class DurationOps(duration: FiniteDuration) {
def ++(other: FiniteDuration): FiniteDuration =
(duration.unit, other.unit) match {
case (a, b) if a == b =>
new FiniteDuration(duration.length + other.length, duration.unit)
case (MILLISECONDS, _) =>
new FiniteDuration(duration.length + other.toMillis, MILLISECONDS)
case (_, MILLISECONDS) =>
new FiniteDuration(duration.toMillis + other.length, MILLISECONDS)
case (SECONDS, _) =>
new FiniteDuration(duration.length + other.toSeconds, SECONDS)
case (_, SECONDS) =>
new FiniteDuration(duration.toSeconds + other.length, SECONDS)
}
}
因为原生类已经有 +
方法了,所以我们使用 ++
来避免冲突。
通过 模式匹配,我们可以判断单位是否一致,并统一转换为较小单位后再相加。
最终效果如下:
"20.seconds ++ 30.seconds" should "be equal to 50.seconds" in {
20.seconds ++ 30.seconds shouldBe 50.seconds
}
"20.seconds ++ 1.minutes" should "be equal to 80.seconds" in {
20.seconds ++ 1.minutes shouldBe 80.seconds
}
✅ 记住:Scala 中没有方法和操作符的区别,只是允许你在方法名中使用特殊字符,并可以用类似操作符的方式调用它们。
5. 总结
✅ Scala 的隐式类是一种非常高效的手段,可以在不修改原有类型的前提下为其扩展功能。
我们只用了三个特性:
- 隐式类
- 方法命名灵活性
- 语法糖机制
但这并不是 Scala DSL 能力的终点。其他可用于 DSL 构建的强大特性还包括:
- 伴生对象(Companion Objects)
- apply 方法
- 高阶函数和 Lambda 表达式
- 使用
{}
替代()
调用单参数方法的能力
一如既往,本文完整源码可以在 GitHub 上找到。