引言
元编程(Metaprogramming)是 Scala 3(Dotty)最「硬核」的能力:让编译器替你写代码。它从 Scala 2 的反射/宏重构成三层递进的体系——inline 内联、transparent inline 透明内联(含编译期求值)、以及基于 Quotes 的完整宏(可拼接语法树)。本文按难度递进,每层给原理 + 可运行示例,最终落到两个生产级用途:编译期校验 与 零开销抽象。
前置:/scala3-modern-features/(enum/given/类型系统基础)、/scala-type-system/(Match Types 依赖的类型级计算)。
目录
- 1. 元编程三层体系
- 2. inline:内联方法与常量折叠
- 3. transparent inline:透明内联
- 4. 编译期求值:constexpr 风格
- 5. Match Types:类型级计算
- 6. 宏基础:Quotes 与 Expr
- 7. 宏实战:生成类型安全 API
- 8. 编译期校验:错误提前到编译
- 9. 内联的性能与陷阱
- 10. 元编程选型速查表
- 延伸阅读
1. 元编程三层体系
| 层级 | 工具 | 能力 | 复杂度 |
|---|---|---|---|
| 第 1 层 | inline | 内联展开、常量折叠 | 低 |
| 第 2 层 | transparent inline | 透明内联 + scala.compiletime 求值 | 中 |
| 第 3 层 | 宏(Quotes) | 语法树生成、任意重构 | 高 |
为什么要元编程:
- 零开销抽象:泛型/类型类在运行期可能要装箱,inline 让抽象在编译期消失
- 编译期校验:字符串、配置、SQL 的错误提前到编译
- 性能:
inline消除函数调用、让常量折叠生效
心智模型:Scala 3 的元编程是「可类型安全的编译器插件 DSL」——你在编译期运行类型安全的 Scala 代码。
2. inline:内联方法与常量折叠
最简单的内联:方法体在调用点展开,消除调用开销:
// 普通方法:每次调用有 JVM 方法调用成本
def square(x: Int): Int = x * x
// 内联:编译期直接展开为 x * x
inline def isquare(x: Int): Int = x * x
常量折叠——编译期常量直接算掉:
inline val Debug = true // 编译期常量
if (Debug) println("debug log") // Debug 恒真 → 运行期无分支
inline 的规则:
inline val必须是编译期常量inline def参数可以是inline参数(强制求值后传入)- 内联方法体内不能依赖运行期值(否则展开不了)
inline def check(cond: Boolean, inline message: String): Unit =
assert(cond, message)
// 调用处 message 是字面量 → 断言信息编译期嵌入
3. transparent inline:透明内联
transparent inline 让内联方法的返回类型也随调用点展开——这是「特化」的基础:
transparent inline def defaultOf(inline kind: String): Any = kind match {
case "int" => 0
case "string" => ""
case "list" => Nil
}
// 调用点的静态类型不再退化成 Any:
val i: Int = defaultOf("int") // ✓ 编译期展开为 0
val s: String = defaultOf("string") // ✓ 展开为 ""
// val x: Long = defaultOf("int") // ✗ 编译错误:实际类型 Int
典型用途:根据编译期常量参数决定返回类型——例如 Enum 相关工具、类型专用 API。
4. 编译期求值:constexpr 风格
scala.compiletime 提供在编译期运行代码的工具,类似 C++ 的 constexpr:
import scala.compiletime.{constValue, constValueOpt, error, erasedValue}
inline def pow2(inline n: Int): Int =
inline n match {
case 0 => 1
case _ => 2 * pow2(n - 1)
}
val eight: Int = pow2(3) // 编译期算出 8,运行期只是常量
// 取类型级常量
inline def bitsOf[Size]: Int = constValue[Size]
val size: Int = bitsOf[8] // 8(由类型参数推导)
inline match:当 n 是编译期常量时,inline n match 在编译期选分支并递归求值——这就是编译期「解释器」。
| API | 用途 |
|---|---|
constValue[T] | 从单例类型取编译期值 |
inline match | 编译期分支 |
error(msg) | 编译期报错(校验失败时) |
summonInline | 编译期隐式搜索 |
erasedValue[T] | 无需值的类型级计算 |
5. Match Types:类型级计算
Match Types 让类型本身参与「计算」——是元编程的类型层:
type Unwrap[T] = T match {
case Option[x] => x
case List[x] => x
case _ => T
}
// 用法:把包装类型解包到内部类型
val a: Unwrap[Option[Int]] = 42 // Int
val b: Unwrap[List[String]] = "hi" // String
递归类型计算(如运行时类型安全集合):
type Len[L <: Tuple] = L match {
case EmptyTuple => 0
case _ *: tail => 1 + Len[tail]
}
// 编译期验证:扁平化元组长度
summon[Len[(Int, String, Boolean)] =:= 3]
应用场景:依赖类型的 DSL(编译期维度检查)、类型级状态机、安全 API 设计。
深入 Match Types 见 /scala3-modern-features/ 的类型系统章节。
6. 宏基础:Quotes 与 Expr
宏是元编程的最高层:你在编译期生成语法树(Expr[T]),用 Quotes 反射 API 拼接:
import scala.quoted.*
// 定义宏:返回 Expr[Int]
inline def doubleSum(inline a: Int, inline b: Int): Int =
${ doubleSumImpl('a, 'b) } // ${} 引用宏实现
def doubleSumImpl(a: Expr[Int], b: Expr[Int])(
using Quotes): Expr[Int] =
'{ ${ a } + ${ b } + ${ a } + ${ b } }
核心概念:
${ ... }(splice):把宏结果拼回代码'{ ... }(quote):把代码片段变成ExprExpr[T]:类型安全的「代码值」Type[T]:类型引用
简单字符串常量宏:
inline def greet(inline name: String): String =
${ greetImpl('name) }
def greetImpl(name: Expr[String])(using Quotes): Expr[String] =
name match {
case Expr(s) => Expr(s"hello, $s") // 匹配到常量
case _ => report.error("需要字符串字面量", name); '{ "" }
}
7. 宏实战:生成类型安全 API
宏最有价值的用途:把运行期字符串错误变成编译期错误。例如编译期 SQL/正则校验:
import scala.quoted.*
// 编译期正则校验宏
inline def regexp(inline pattern: String): String =
${ regexpImpl('pattern) }
def regexpImpl(pattern: Expr[String])(using Quotes): Expr[String] = {
pattern match {
case Expr(p) =>
java.util.regex.Pattern.compile(p) // 编译期就跑一次
pattern
case _ =>
report.error("正则必须是字面量", pattern)
'{ "" }
}
}
// 用法:写错的模式编译期就报错
val ok = regexp("^[a-z]+$") // ✓
// val bad = regexp("^[a-z]+(") // ✗ 编译错误!
另一个实战:编译期 SQL(与 /scala-database-access/ 的 Quill 同源思想):
inline def sqlCheck(inline s: String): Unit = ${ sqlCheckImpl('s) }
def sqlCheckImpl(s: Expr[String])(using Quotes): Expr[Unit] = {
// 连一次解析器,验证语法;失败则 report.error
s match {
case Expr(query) => validateSql(query) match {
case None => '{ () }
case Some(error) => report.error(error, s)
}
case _ => '{ () }
}
}
8. 编译期校验:错误提前到编译
编译期校验的黄金模式——用 inline match + error 让「不可能」变「不可编译」:
import scala.compiletime.error
inline def ensurePositive(inline n: Int): Int =
inline if n <= 0 then error("参数必须为正数,实际为: " + n) else n
// 编译期通过
val a: Int = ensurePositive(5)
// 编译期直接报错
// val b: Int = ensurePositive(-3)
应用清单:
| 场景 | 宏/内联做法 |
|---|---|
| 正则/SQL/DSL 语法 | 编译期解析 + error |
| 配置常量正确性 | inline match 校验 |
| 类型约束 | Match Types / summonInline |
| API 误用 | report.error 提示替代方案 |
效果:上线前抓错,运行时不再有「魔法字符串炸掉」的类。
9. 内联的性能与陷阱
内联带来的好处:
- 消除调用开销(JIT 也做,但内联是编译期保证)
- 常量折叠、死代码消除(
inline val Debug) - 泛型特化(透明内联 + 具体类型)
陷阱:
| 陷阱 | 说明 |
|---|---|
| 代码膨胀 | 内联复制多次,二进制变大 |
| 栈溢出 | 递归 inline 展开深了会卡编译 |
| 依赖隐式副作用 | 内联参数求值时机不同(值内联 vs 引用) |
| 调试困难 | 展开后堆栈/断点对不上源码 |
实践准则:
- 只对小且热的方法用
inline - 用
inline val标记编译期常量 - 宏实现保持「小 + 纯」,复杂逻辑拆到普通方法
- 配合
@targetName保持二进制稳定
10. 元编程选型速查表
| 需求 | 工具 |
|---|---|
| 消除小方法调用开销 | inline |
| 编译期常量折叠 | inline val |
| 按参数特化返回类型 | transparent inline |
| 编译期计算 | scala.compiletime + inline match |
| 类型级计算 | Match Types |
| 生成代码/语法树 | 宏(Quotes) |
| 编译期校验 DSL | report.error |
| 隐式搜索/特化 | summonInline |
一句话记忆:想省开销用 inline,想换类型用 transparent inline,想编译期算数用 constexpr,想改语法树就上宏;一切目标是把错误赶出运行期。
延伸阅读
- /scala3-modern-features/ — enum/given/缩进语法,元编程的类型基础
- /scala-type-system/ — 泛型与 Match Types 的类型级计算
- /scala-database-access/ — Quill 正是「编译期 SQL」宏的实战
- /scala-build-tooling/ — 宏需要 sbt 配置 Macro 支持(scalaVersion 3)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。