反射

符号、树和类型

语言
此文档页面特定于 Scala 2 中提供的功能,这些功能已在 Scala 3 中删除或被替代功能取代。除非另有说明,本页中的所有代码示例均假定你正在使用 Scala 2。

实验性

符号

符号用于在名称和它引用的实体(如类或方法)之间建立绑定。你在 Scala 中定义并可以命名的任何内容都有一个关联的符号。

符号包含有关实体声明的所有可用信息(class/object/trait 等)或成员(val/var/def 等),因此是运行时反射和编译时反射(宏)的核心抽象。

符号可以提供大量信息,从所有符号上可用的基本 name 方法到其他更复杂的概念,例如从 ClassSymbol 获取 baseClasses。符号的其他常见用例包括检查成员签名、获取类的类型参数、获取方法的参数类型或找出字段的类型。

符号所有者层次结构

符号按层次结构组织。例如,表示方法参数的符号由相应的方法符号拥有,方法符号由其封闭的类、特征或对象拥有,类由包含的包拥有,依此类推。

如果符号没有所有者,例如,因为它引用顶级实体,如顶级包,那么它的所有者是特殊的 NoSymbol 单例对象。表示缺失符号,NoSymbol 通常在 API 中用于表示空值或默认值。访问 NoSymbolowner 会引发异常。请参阅 API 文档,了解类型 Symbol 提供的通用接口

TypeSymbol

TypeSymbol 表示类型、类和特征声明,以及类型参数。不适用于更具体的 ClassSymbol 的有趣成员包括 isAbstractTypeisContravariantisCovariant

  • ClassSymbol:提供对类或特征声明中包含的所有信息的访问,例如 name、修饰符(isFinalisPrivateisProtectedisAbstractClass 等)、baseClassestypeParams

TermSymbol

表示 val、var、def 和对象声明以及包和值参数的术语符号类型的类型。

  • MethodSymbol:表示 def 声明的方法符号类型(TermSymbol 的子类)。它支持查询,例如检查方法是否为(主)构造函数,或方法是否支持可变长度参数列表。
  • ModuleSymbol:表示对象声明的模块符号类型。它允许通过成员 moduleClass 查找隐式关联到对象定义的类。反向查找也是可能的。可以通过检查其 selfType.termSymbol 从模块类返回到关联的模块符号。

符号转换

在某些情况下,可以使用返回通用 Symbol 类型实例的方法。在这些情况下,可以将获得的更通用的 Symbol 类型转换为所需的特定、更专门的符号类型。

符号转换(例如 asClassasMethod)用于根据需要转换为 Symbol 的更具体的子类型(例如,如果你想使用 MethodSymbol 接口)。

例如,

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> class C[T] { def test[U](x: T)(y: U): Int = ??? }
defined class C

scala> val testMember = typeOf[C[Int]].member(TermName("test"))
testMember: scala.reflect.runtime.universe.Symbol = method test

在这种情况下,member 返回 Symbol 的实例,而不是 MethodSymbol,就像人们所期望的那样。因此,我们必须使用 asMethod 来确保我们获得 MethodSymbol

scala> testMember.asMethod
res0: scala.reflect.runtime.universe.MethodSymbol = method test

自由符号

两种符号类型 FreeTermSymbolFreeTypeSymbol 具有特殊状态,因为它们引用可用信息不完整的符号。在某些情况下,这些符号在具体化过程中生成(有关更多背景信息,请参阅关于具体化树的相应部分)。每当具体化无法找到符号(这意味着符号在相应类文件中不可用,例如,因为符号引用本地类)时,它会将其具体化为所谓的“自由类型”,这是一种合成虚拟符号,它记住原始名称和所有者,并具有紧密遵循原始类型的代理类型签名。你可以通过调用 sym.isFreeType 来检查符号是否是自由类型。你还可以通过调用 tree.freeTypes 来获取树及其子项引用的所有自由类型的列表。最后,你可以通过使用 -Xlog-free-types 在具体化产生自由类型时获得警告。

类型

顾名思义,Type 的实例表示相应符号类型的相关信息。这包括其成员(方法、字段、类型别名、抽象类型、嵌套类、特征等),直接声明或继承,其基本类型、其擦除等。类型还提供用于测试类型一致性或等价性的操作。

实例化类型

一般来说,有三种方法可以实例化 Type

  1. 通过 scala.reflect.api.TypeTagstypeOf 方法,该方法混合到 Universe 中(最简单、最常见)。
  2. 标准类型,如 IntBooleanAnyUnit 可通过可用 universe 访问。
  3. 使用工厂方法(如 scala.reflect.api.Types 上的 typeRefpolyType)手动实例化(不推荐)。

使用 typeOf 实例化类型

要实例化类型,大多数情况下,可以使用 scala.reflect.api.TypeTags#typeOf 方法。它采用一个类型参数并生成表示该参数的 Type 实例。例如

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> typeOf[List[Int]]
res0: scala.reflect.runtime.universe.Type = scala.List[Int]

在此示例中,返回 scala.reflect.api.Types$TypeRef,它对应于类型构造器 List,应用于类型参数 Int

但是,请注意,此方法要求手动指定我们尝试实例化的类型。如果我们有兴趣获取对应于某个任意实例的 Type 实例,该怎么办?我们可以简单地定义一个在类型参数上具有上下文绑定的方法——这会为我们生成一个专门的 TypeTag,我们可以使用它来获取我们任意实例的类型

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> getType(List(1,2,3))
res1: scala.reflect.runtime.universe.Type = List[Int]

scala> class Animal; class Cat extends Animal
defined class Animal
defined class Cat

scala> val a = new Animal
a: Animal = Animal@21c17f5a

scala> getType(a)
res2: scala.reflect.runtime.universe.Type = Animal

scala> val c = new Cat
c: Cat = Cat@2302d72d

scala> getType(c)
res3: scala.reflect.runtime.universe.Type = Cat

注意: 方法 typeOf 不适用于具有类型参数的类型,例如 typeOf[List[A]],其中 A 是一个类型参数。在这种情况下,可以使用 scala.reflect.api.TypeTags#weakTypeOf 代替。有关更多详细信息,请参阅本指南的 TypeTags 部分。

标准类型

标准类型,如 IntBooleanAnyUnit,可通过 universe 的 definitions 成员访问。例如

scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe

scala> val intTpe = universe.definitions.IntTpe
intTpe: scala.reflect.runtime.universe.Type = Int

标准类型的列表在 scala.reflect.api.StandardDefinitions 中的 StandardTypes 特质中指定。

类型上的常见操作

类型通常用于类型一致性测试或查询成员。对类型执行的三类主要操作是

  1. 检查两个类型之间的子类型关系。
  2. 检查两个类型之间的相等性。
  3. 查询给定类型以获取某些成员或内部类型。

子类型关系

给定两个 Type 实例,可以使用 <:<(在特殊情况下,使用 weak_<:<,如下所述)轻松测试一个是否是另一个的子类型

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> class A; class B extends A
defined class A
defined class B

scala> typeOf[A] <:< typeOf[B]
res0: Boolean = false

scala> typeOf[B] <:< typeOf[A]
res1: Boolean = true

请注意,方法 weak_<:< 用于检查两个类型之间的弱一致性。在处理数字类型时,这通常很重要。

Scala 的数字类型遵循以下顺序(Scala 语言规范的 3.5.3 节)

在某些情况下,Scala 使用更通用的符合关系。如果 S<:T 或 S 和 T 都是原始数字类型且 S 在以下顺序中位于 T 之前,则类型 S 弱符合类型 T,记为 S <:w T

弱一致性关系
Byte <:w Short
Short <:w Int
Char <:w Int
Int <:w Long
Long <:w Float
Float <:w Double

例如,弱一致性用于确定以下 if 表达式的类型

scala> if (true) 1 else 1d
res2: Double = 1.0

在上面显示的 if 表达式中,结果类型被定义为两个类型的弱最小上界(即,关于弱一致性的最小上界)。

因此,由于 Double 被定义为 IntDouble 之间的弱一致性的最小上界(根据上面显示的规范),Double 被推断为我们示例 if 表达式的类型。

请注意,方法 weak_<:< 检查弱一致性(与 <:< 相反,后者检查一致性,而不考虑规范第 3.5.3 节中的弱一致性关系),因此在检查数字类型 IntDouble 之间的一致性关系时返回正确的结果

scala> typeOf[Int] weak_<:< typeOf[Double]
res3: Boolean = true

scala> typeOf[Double] weak_<:< typeOf[Int]
res4: Boolean = false

而使用 <:< 会错误地报告 IntDouble 以任何方式都不一致

scala> typeOf[Int] <:< typeOf[Double]
res5: Boolean = false

scala> typeOf[Double] <:< typeOf[Int]
res6: Boolean = false

类型相等

与类型一致性类似,可以轻松检查两个类型的相等性。也就是说,给定两个任意类型,可以使用方法 =:= 来查看两者是否表示完全相同的编译时类型。

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> class A
defined class A

scala> val a1 = new A; val a2 = new A
a1: A = A@cddb2e7
a2: A = A@2f0c624a

scala> getType(a1) =:= getType(a2)
res0: Boolean = true

请注意,两个实例的精确类型信息必须相同。例如,在以下代码段中,我们有两个具有不同类型参数的 List 实例。

scala> getType(List(1,2,3)) =:= getType(List(1.0, 2.0, 3.0))
res1: Boolean = false

scala> getType(List(1,2,3)) =:= getType(List(9,8,7))
res2: Boolean = true

同样重要的是要注意,=:= 始终用于比较类型的相等性。也就是说,切勿使用 ==,因为它无法在存在类型别名的情况下检查类型相等性,而 =:= 可以

scala> type Histogram = List[Int]
defined type alias Histogram

scala> typeOf[Histogram] =:= getType(List(4,5,6))
res3: Boolean = true

scala> typeOf[Histogram] == getType(List(4,5,6))
res4: Boolean = false

正如我们所看到的,== 错误地报告 HistogramList[Int] 具有不同的类型。

查询类型的成员和声明

给定一个 Type,还可以查询它以获取特定成员或声明。一个 Type成员包括所有字段、方法、类型别名、抽象类型、嵌套类/对象/特征等。一个 Type声明只是在给定的 Type 所表示的类/特征/对象定义中声明(未继承)的那些成员。

要为某个特定成员或声明获取 Symbol,只需使用提供与该类型关联的定义列表的方法 membersdecls。每个方法还有对应的单数形式,即方法 memberdecl。所有四个方法的签名如下所示

/** The member with given name, either directly declared or inherited, an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def member(name: Universe.Name): Universe.Symbol

/** The defined or declared members with name name in this type; an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def decl(name: Universe.Name): Universe.Symbol

/** A Scope containing all members of this type
  * (directly declared or inherited). */
def members: Universe.MemberScope // MemberScope is a type of
                                  // Traversable, use higher-order
                                  // functions such as map,
                                  // filter, foreach to query!

/** A Scope containing the members declared directly on this type. */
def decls: Universe.MemberScope // MemberScope is a type of
                                       // Traversable, use higher-order
                                       // functions such as map,
                                       // filter, foreach to query!

例如,要查找 Listmap 方法,可以执行以下操作

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> typeOf[List[_]].member(TermName("map"))
res0: scala.reflect.runtime.universe.Symbol = method map

请注意,由于我们正在查找方法,因此我们向方法 member 传递 TermName。如果要查找类型成员(例如 List 的自身类型 Self),我们将传递 TypeName

scala> typeOf[List[_]].member(TypeName("Self"))
res1: scala.reflect.runtime.universe.Symbol = type Self

我们还可以以有趣的方式查询类型上的所有成员或声明。我们可以使用方法 members 获取 SymbolTraversableMemberScopeApi 扩展 Traversable),它表示给定类型上所有继承或声明的成员,这意味着我们可以在集合上使用流行的高阶函数,如 foreachfiltermap 等,来探索类型的成员。例如,要打印 List 中为私有的成员,只需执行以下操作

scala> typeOf[List[Int]].members.filter(_.isPrivate).foreach(println _)
method super$sameElements
method occCounts
class CombinationsItr
class PermutationsItr
method sequential
method iterateUntilEmpty

树是 Scala 抽象语法的基础,用于表示程序。它们也称为抽象语法树,通常缩写为 AST。

在 Scala 反射中,生成或使用树的 API 如下

  1. Scala 注解,它使用树表示其参数,在 Annotation.scalaArgs 中公开(更多信息,请参阅本指南的 注解 部分)。
  2. reify,一种特殊的方法,它采用一个表达式并返回表示该表达式的 AST。
  3. 使用宏的编译时反射(在 宏指南 中概述)和使用工具箱的运行时编译都使用树作为其程序表示媒介。

需要注意的是,树是不可变的,除了三个字段——posPosition)、symbolSymbol)和tpeType),这些字段在树进行类型检查时分配。

Tree 的类型

树有三个主要类别

  1. TermTree 的子类,表示项,例如,方法调用由 Apply 节点表示,对象实例化使用 New 节点实现,等等。
  2. TypTree 的子类,表示程序源代码中明确指定的类型,例如,List[Int] 被解析为 AppliedTypeTree注意:TypTree 并非拼写错误,它在概念上也不同于 TypeTree——TypeTree 是不同的东西。也就是说,在编译器构造 Type 的情况下(例如,类型推断期间),它们可以包装在 TypeTree 树中并集成到程序的 AST 中。
  3. SymTree 的子类,引入或引用定义。新定义的引入示例包括表示类和特征定义的 ClassDef,或表示字段和参数定义的 ValDef。现有定义的引用示例包括引用当前作用域中现有定义的 Ident,例如局部变量或方法。

人们可能遇到的任何其他类型的树通常是语法或短暂的构造。例如,包装单个匹配情况的 CaseDef;此类节点既不是项也不是类型,也不带有符号。

检查树

Scala 反射提供了一些可视化树的方法,所有这些方法都可通过 universe 获得。给定一棵树,可以

  • 使用打印树表示的伪 Scala 代码的方法 showtoString
  • 使用 showRaw 方法查看类型检查器操作的原始内部树。

例如,给定以下树

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

我们可以使用 show 方法(或等效的 toString)查看该树表示的内容。

scala> show(tree)
res0: String = x.$plus(2)

正如我们所见,tree 只是将 2 添加到项 x

我们也可以反过来。给定某个 Scala 表达式,我们可以先获得一棵树,然后使用 showRaw 方法查看编译器和类型检查器操作的原始内部树。例如,给定表达式

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val expr = reify { class Flower { def name = "Rose" } }
expr: scala.reflect.runtime.universe.Expr[Unit] = ...

这里,reify 只是获取传递给它的 Scala 表达式,并返回一个 Scala Expr,它只是包装一个 Tree 和一个 TypeTag(有关 Expr 的更多信息,请参阅本指南的 Expr 部分)。我们可以通过以下方式获取 expr 包含的树

scala> val tree = expr.tree
tree: scala.reflect.runtime.universe.Tree =
{
  class Flower extends AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def name = "Rose"
  };
  ()
}

我们可以通过简单地执行以下操作来检查原始树

scala> showRaw(tree)
res1: String = Block(List(ClassDef(Modifiers(), TypeName("Flower"), List(), Template(List(Ident(TypeName("AnyRef"))), emptyValDef, List(DefDef(Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree(), Block(List(Apply(Select(Super(This(typeNames.EMPTY), typeNames.EMPTY), termNames.CONSTRUCTOR), List())), Literal(Constant(())))), DefDef(Modifiers(), TermName("name"), List(), List(), TypeTree(), Literal(Constant("Rose"))))))), Literal(Constant(())))

遍历树

在理解了给定树的结构后,通常下一步是从中提取信息。这是通过遍历树来完成的,并且可以通过两种方式之一完成

  • 通过模式匹配进行遍历。
  • 使用 Traverser 的子类

通过模式匹配进行遍历

通过模式匹配进行遍历是遍历树最简单、最常见的方法。通常,当人们对单个节点处给定树的状态感兴趣时,他们会通过模式匹配遍历树。例如,假设我们只想获取以下树中唯一 Apply 节点的函数和参数

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

我们只需匹配我们的 tree,并且在有 Apply 节点的情况下,只需返回 Apply 的函数和参数

scala> val (fun, arg) = tree match {
     |     case Apply(fn, a :: Nil) => (fn, a)
     | }
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

我们通过将模式匹配放在左侧,可以更简洁地实现相同的功能

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

请注意,Tree 通常可能非常复杂,其中节点可以任意深度地嵌套在其他节点中。一个简单的示例是,如果我们向上述树添加第二个 Apply 节点,该节点用于向我们的和中添加 3

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

如果我们应用与上述相同的模式匹配,我们将获得外部 Apply 节点,其函数包含表示我们上面看到的 x.$plus(2) 的整个树

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus(2).$plus
arg: scala.reflect.runtime.universe.Tree = 3

scala> showRaw(fun)
res3: String = Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus"))

在必须执行更复杂任务的情况下,例如在特定节点处停止或收集和检查特定类型的节点的情况下,使用 Traverser 进行遍历可能更有利。

通过 Traverser 进行遍历

在需要从上到下遍历整个树的情况下,通过模式匹配进行遍历将不可行——要这样做,必须在模式匹配中单独处理我们可能遇到的每种类型的节点。因此,在这些情况下,通常使用类 Traverser

Traverser 确保深度优先搜索给定树中的每个节点。

要使用 Traverser,只需子类化 Traverser 并重写方法 traverse。这样做时,您可以简单地提供自定义逻辑来仅处理您感兴趣的情况。例如,如果给定我们前面部分中的 x.$plus(2).$plus(3) 树,我们希望收集所有 Apply 节点,我们可以这样做

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

scala> object traverser extends Traverser {
     |   var applies = List[Apply]()
     |   override def traverse(tree: Tree): Unit = tree match {
     |     case app @ Apply(fun, args) =>
     |       applies = app :: applies
     |       super.traverse(fun)
     |       super.traverseTrees(args)
     |     case _ => super.traverse(tree)
     |   }
     | }
defined module traverser

在上面,我们打算构建一个 Apply 节点的列表,我们在给定的树中找到这些节点。

我们通过实际上添加一个特殊情况来实现这一点,该情况已经深度优先 traverse 方法定义在父类 Traverser 中,通过子类 traverser 的重写 traverse 方法。我们的特殊情况仅影响与模式 Apply(fun, args) 匹配的节点,其中 fun 是某个函数(由 Tree 表示),args 是参数列表(由 Tree 列表表示)。

当一棵树与模式匹配(,当我们有一个 Apply 节点时),我们只需将其添加到我们的 List[Apply]applies,并继续我们的遍历。

请注意,在我们的匹配中,我们对包装在我们的 Apply 中的函数 fun 调用 super.traverse,并且我们对我们的参数列表 args 调用 super.traverseTrees(基本上与 super.traverse 相同,但对于 List[Tree] 而不是单个 Tree)。在这两个调用中,我们的目标很简单——我们希望确保在 Traverser 中使用默认 traverse 方法,因为我们不知道表示 fun 的 Tree 是否包含我们的 Apply 模式——也就是说,我们希望遍历整个子树。由于 Traverser 超类调用 this.traverse,传递每个嵌套的子树,最终保证为与我们的 Apply 模式匹配的每个子树调用我们的自定义 traverse 方法。

要触发 traverse 并查看匹配的 Apply 节点的结果 List,只需执行

scala> traverser.traverse(tree)

scala> traverser.applies
res0: List[scala.reflect.runtime.universe.Apply] = List(x.$plus(2), x.$plus(2).$plus(3))

创建树

使用运行时反射时,无需手动构建树。但是,使用工具箱进行运行时编译和使用宏进行编译时反射都将树用作其程序表示媒介。在这些情况下,有三种推荐的创建树的方法

  1. 通过方法 reify(应尽可能优先使用)。
  2. 通过 ToolBox 上的方法 parse
  3. 手动构建(不推荐)。

通过 reify 创建树

方法 reify 只需将 Scala 表达式作为参数,并生成该参数的类型化 Tree 表示作为结果。

通过方法 reify 创建树是 Scala Reflection 中创建树的推荐方法。要了解原因,让我们从一个小示例开始

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> { val tree = reify(println(2)).tree; showRaw(tree) }
res0: String = Apply(Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("println")), List(Literal(Constant(2))))

在这里,我们只 reify 调用 println(2),即,我们将表达式 println(2) 转换为其对应的树表示。然后我们输出原始树。请注意,println 方法已转换为 scala.Predef.println。此类转换可确保无论 reify 的结果在哪里使用,它都不会意外地改变其含义。例如,即使此 println(2) 片段稍后插入到定义其自己的 println 的代码块中,也不会影响该片段的行为。

因此,这种创建树的方式是卫生的,因为它保留了标识符的绑定。

拼接树

使用 reify 还可以允许我们从较小的树中组合树。这是使用 Expr.splice 完成的。

注意: Exprreify 的返回类型。可以将其视为一个简单的包装器,其中包含一个类型化 Tree、一个 TypeTag 和一些与重构相关的函数,例如 splice。有关 Expr 的更多信息,请参阅 本指南的相关部分

例如,让我们尝试使用 splice 构建表示 println(2) 的树

scala> val x = reify(2)
x: scala.reflect.runtime.universe.Expr[Int(2)] = Expr[Int(2)](2)

scala> reify(println(x.splice))
res1: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println(2))

在此,我们分别reify 2println,然后简单地将一个splice到另一个中。

但是,请注意,reify的参数要求是有效的且可键入的 Scala 代码。如果我们想要抽象println本身,而不是println的参数,这是不可能的

scala> val fn = reify(println)
fn: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println())

scala> reify(fn.splice(2))
<console>:12: error: Unit does not take parameters
            reify(fn.splice(2))
                            ^

正如我们所看到的,编译器假设我们想要重新指定对println的调用,没有参数,而我们真正想要的是捕获要调用的函数的名称。

使用reify时,这些类型的用例目前无法表达。

通过ToolBox上的parse创建树

Toolbox可用于类型检查、编译和执行抽象语法树。工具箱还可用于将字符串解析为 AST。

注意:使用工具箱需要scala-compiler.jar位于类路径中。

让我们看看parse如何处理上一节中的println示例

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@7bc979dd

scala> showRaw(tb.parse("println(2)"))
res2: String = Apply(Ident(TermName("println")), List(Literal(Constant(2))))

重要的是要注意,与reify不同,工具箱不受可键入性要求的限制——尽管这种灵活性是通过牺牲鲁棒性来实现的。也就是说,在这里我们可以看到,与reify不同,parse并不反映println应该绑定到标准println方法这一事实。

注意:在使用宏时,不应使用ToolBox.parse。这是因为宏上下文中已经内置了parse方法。例如

bash$ scala -Yrepl-class-based:false

scala> import scala.language.experimental.macros
import scala.language.experimental.macros

scala> def impl(c: scala.reflect.macros.whitebox.Context) = c.Expr[Unit](c.parse("println(2)"))
def impl(c: scala.reflect.macros.whitebox.Context): c.Expr[Unit]

scala> def test: Unit = macro impl
def test: Unit

scala> test
2

您可以在此宏文章中找到有关这两个Context的更多信息。

使用工具箱进行类型检查

正如前面提到的,ToolBox不仅可以从字符串构建树。它们还可用于类型检查、编译和执行树。

除了概述程序的结构之外,树还包含有关程序语义的重要信息,这些信息编码在symbol(分配给引入或引用定义的树的符号)和tpe(树的类型)中。默认情况下,这些字段为空,但类型检查会填充它们。

使用运行时反射框架时,类型检查由 ToolBox.typeCheck 实现。使用宏时,在编译时可以使用 Context.typeCheck 方法。

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val tree = reify { "test".length }.tree
tree: scala.reflect.runtime.universe.Tree = "test".length()

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = ...

scala> val ttree = tb.typeCheck(tree)
ttree: tb.u.Tree = "test".length()

scala> ttree.tpe
res5: tb.u.Type = Int

scala> ttree.symbol
res6: tb.u.Symbol = method length

在此,我们简单创建一个树,表示对 "test".length 的调用,并使用 ToolBox tbtypeCheck 方法对树进行类型检查。正如我们所看到的,ttree 获取了正确的类型 Int,并且其 Symbol 设置正确。

通过手动构造创建树

如果其他方法均失败,则可以手动构造树。这是创建树的最低级方法,只有在其他方法均无法使用时才应尝试使用此方法。与 parse 相比,它有时会提供更大的灵活性,尽管这种灵活性是以冗长和脆弱为代价的。

我们之前涉及 println(2) 的示例可以手动构造如下

scala> Apply(Ident(TermName("println")), List(Literal(Constant(2))))
res0: scala.reflect.runtime.universe.Apply = println(2)

此技术的规范用例是当目标树需要从动态创建的部分组装时,这些部分彼此孤立时没有意义。在这种情况下,reify 很可能不适用,因为它要求其参数可输入。 parse 可能也不起作用,因为通常,树是在子表达式级别组装的,而各个部分无法表示为 Scala 源。

此页面的贡献者