此文档页面特定于 Scala 3,可能涵盖 Scala 2 中不可用的新概念。除非另有说明,此页面中的所有代码示例均假定你正在使用 Scala 3。
反射 API 提供了对代码结构更复杂、更全面的视图。它提供了类型化抽象语法树及其属性(如类型、符号、位置和注释)的视图。
API 可用于宏以及 检查 TASTy 文件。
如何使用 API
反射 API 在类型 Quotes
中定义为 reflect
。实际实例取决于当前范围,其中使用引号或引号模式匹配。因此,每个宏方法都会收到 Quotes 作为附加参数。由于 Quotes
是上下文相关的,因此要访问其成员,我们需要命名参数或调用它。标准库中的以下定义详细说明了访问它的规范方法
package scala.quoted
transparent inline def quotes(using inline q: Quotes): q.type = q
我们可以使用 scala.quoted.quotes
来导入当前范围内的 Quotes
import scala.quoted.* // Import `quotes`, `Quotes`, and `Expr`
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....
val tree: Tree = ...
...
这将导入 API 的所有类型和模块(带有扩展方法)。
如何浏览 API
可以在 scala.quoted.Quotes.reflectModule
的 API 文档 中找到完整的 API。不幸的是,在此阶段,此自动生成的文档不容易浏览。
页面上最重要的元素是层次结构树,它提供了 API 中类型子类型关系的综合概述。对于树中的每个类型 Foo
- 特征
FooMethods
包含类型Foo
上可用的方法 - 特征
FooModule
包含对象Foo
上可用的静态方法。最值得注意的是,构造函数(apply/copy
)和unapply
方法,后者提供了模式匹配所需的提取器 - 对于所有类型
Upper
,使得Foo <: Upper
,UpperMethods
中定义的方法在Foo
上也可用
例如,TypeBounds
是 TypeRepr
的子类型,表示形式为 T >: L <: U
的类型树:类型 T
是 L
的超类型,是 U
的子类型。在 TypeBoundsMethods
中,你会找到方法 low
和 hi
,它们允许你访问 L
和 U
的表示。在 TypeBoundsModule
中,你会找到 unapply
方法,它允许你编写
def f(tpe: TypeRepr) =
tpe match
case TypeBounds(l, u) =>
由于 TypeBounds <: TypeRepr
,TypeReprMethods
中定义的所有方法在 TypeBounds
值上可用
def f(tpe: TypeRepr) =
tpe match
case tpe: TypeBounds =>
val low = tpe.low
val hi = tpe.hi
与 Expr/Type 的关系
Expr 和 Term
表达式(Expr[T]
)可以看作是 Term
的包装器,其中 T
是该项的静态已知类型。下面,我们使用扩展方法 asTerm
将表达式转换为项。此扩展方法仅在导入 quotes.reflect.asTerm
后可用。然后,我们使用 asExprOf[Int]
将该项转换回 Expr[Int]
。如果该项没有提供的类型(在本例中为 Int
),或者该项不是有效的表达式,则此操作将失败。例如,如果方法 fn
采用类型参数,则 Ident(fn)
是一个无效的项,在这种情况下,我们需要一个 Apply(Ident(fn), args)
。
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
val tree: Term = x.asTerm
val expr: Expr[Int] = tree.asExprOf[Int]
expr
Type 和 TypeRepr
类似地,我们还可以将 Type[T]
看作 TypeRepr
的包装器,其中 T
是静态已知的类型。要获取 TypeRepr
,我们使用 TypeRepr.of[T]
,它期望在范围内给定 Type[T]
(类似于 Type.of[T]
)。我们还可以使用 asType
方法将其转换回 Type[?]
。由于 Type[?]
的类型不是静态已知的,因此我们需要使用存在类型对其进行命名才能使用它。这可以通过使用 '[t]
模式来实现。
def g[T: Type](using Quotes) =
import quotes.reflect.*
val tpe: TypeRepr = TypeRepr.of[T]
tpe.asType match
case '[t] => '{ val x: t = ${...} }
...
符号
Term
和 TypeRepr
的 API 在某种意义上是相对封闭的,方法生成并接受其类型在 API 中定义的值。但是,您可能会注意到标识定义的 Symbol
的存在。
Term
和 TypeRepr
(因此 Expr
和 Type
)都有一个关联的符号。使用 ==
比较两个定义,Symbol
使得您可以知道它们是否相同。此外,Symbol
公开并被许多有用的方法使用。例如
declaredFields
和declaredMethods
允许您迭代符号中定义的字段和成员flags
允许您检查符号的多个属性companionClass
和companionModule
提供了一种跳转到伴随对象/类并返回的方式TypeRepr.baseClasses
返回类型扩展的类的符号列表Symbol.pos
使您可以访问符号定义的位置、定义的源代码,甚至符号定义的文件名- 您可以在
SymbolMethods
中找到许多其他内容
转换为 Symbol 并返回
考虑一个名为 val tpe: TypeRepr = ...
的 TypeRepr
类型的实例。然后
tpe.typeSymbol
返回由TypeRepr
表示的类型的符号。给定Type[T]
,获取Symbol
的推荐方法是TypeRepr.of[T].typeSymbol
- 对于单例类型,
tpe.termSymbol
返回底层对象或值的符号 tpe.memberType(symbol)
返回提供的符号的TypeRepr
- 在对象
t: Tree
上,t.symbol
返回与树关联的符号。鉴于Term <: Tree
,Expr.asTerm.symbol
是获取与Expr[T]
关联的符号的最佳方法 - 在对象
sym: Symbol
上,sym.tree
返回与符号关联的Tree
。使用此方法时要小心,因为符号的树可能未定义。请阅读 最佳实践页面 上的更多信息
宏 API 设计
创建执行宏的某些常见逻辑的帮助程序方法或提取器通常很有用。
最简单的方法是仅在签名中提及 Expr
、Type
和 Quotes
的方法。在内部,它们可能使用反射,但这不会在方法的使用位置看到。
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
...
在某些情况下,某些方法可能不可避免地会期望或返回 Tree
或 quotes.reflect
中的其他类型。对于这些情况,最佳做法是遵循以下方法签名示例
获取 quotes.reflect.Term
参数的方法
def f(using Quotes)(term: quotes.reflect.Term): String =
import quotes.reflect.*
...
返回 quotes.reflect.Tree
的 quotes.reflect.Term
的扩展方法
extension (using Quotes)(term: quotes.reflect.Term)
def g: quotes.reflect.Tree = ...
匹配 quotes.reflect.Term
的提取器
object MyExtractor:
def unapply(using Quotes)(x: quotes.reflect.Term) =
...
Some(y)
避免将
Quotes
上下文保存在字段中。字段中的Quotes
不可避免地会因导致涉及具有不同路径的Quotes
的错误而使其使用变得更加困难。通常,这些模式已在使用 Scala 2 方式定义扩展方法或上下文非应用的代码中看到。现在我们有了可以在其他参数之前添加的
given
参数,所有这些旧的解决方法都不再需要了。新的抽象在定义站点和使用站点都使其变得更简单。
调试
运行时检查
表达式 (Expr[T]
) 可以看作是 Term
的包装器,其中 T
是该术语的静态已知类型。因此,这些检查将在运行时进行(即宏展开时的编译时)。
建议在开发宏或宏测试时启用 -Xcheck-macros
标志。此标志将启用额外的运行时检查,这些检查将尝试在创建错误格式的树或类型时立即找到它们。
还有 -Ycheck:all
标志,它检查树的格式良好的所有编译器不变量。这些检查通常会因断言错误而失败。
打印树
quotes.reflect
包中类型上的 toString
方法不适合调试,因为它们显示的是内部表示形式,而不是 quotes.reflect
表示形式。在许多情况下,这些情况是相似的,但有时它们可能会导致调试过程出现偏差,因此不应依赖它们。
相反,quotes.reflect.Printers
提供了一组用于调试的有用打印机。值得注意的是,TreeStructure
、TypeReprStructure
和 ConstantStructure
类非常有用。它们将打印树结构,大致遵循匹配它所需的提取器。
val tree: Tree = ...
println(tree.show(using Printer.TreeStructure))
可以在 Tree
上的模式匹配末尾添加此内容,这是最有用的地方之一。
tree match
case Ident(_) =>
case Select(_, _) =>
...
case _ =>
throw new MatchError(tree.show(using Printer.TreeStructure))
这样,如果错过了某个情况,错误将报告一个熟悉的结构,可以复制粘贴以开始修复问题。
如果需要,可以将此打印机设为默认值
import quotes.reflect.*
given Printer[Tree] = Printer.TreeStructure
...
println(tree.show)
更多
即将推出