反射

注释、名称、范围等

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

实验性

注释

在 Scala 中,可以使用 scala.annotation.Annotation 的子类型对声明进行注释。此外,由于 Scala 与 Java 的注释系统 集成,因此可以处理标准 Java 编译器生成的注释。

如果相应的注释已持久化,则可以通过反射检查注释,以便可以从包含注释声明的类文件中读取它们。可以通过继承 scala.annotation.StaticAnnotationscala.annotation.ClassfileAnnotation 来使自定义注释类型持久化。因此,注释类型的实例作为特殊属性存储在相应的类文件中。请注意,仅子类化 scala.annotation.Annotation 不足以使相应的元数据持久化以进行运行时反射。此外,子类化 scala.annotation.ClassfileAnnotation 不会使你的注释在运行时作为 Java 注释可见;这需要用 Java 编写注释类。

API 区分两种注释

  • Java 注释:Java 编译器生成的定义上的注释,,附加到程序定义的 java.lang.annotation.Annotation 的子类型。当 Scala 反射读取时,scala.annotation.ClassfileAnnotation 特征会自动作为子类添加到每个 Java 注释。
  • Scala 注释:Scala 编译器生成的定义或类型上的注释。

Java 和 Scala 注释之间的区别体现在 scala.reflect.api.Annotations#Annotation 的契约中,它同时公开了 scalaArgsjavaArgs。对于扩展 scala.annotation.ClassfileAnnotation 的 Scala 或 Java 注释,scalaArgs 为空,参数(如果有)存储在 javaArgs 中。对于所有其他 Scala 注释,参数存储在 scalaArgs 中,javaArgs 为空。

scalaArgs 中的参数表示为类型化树。请注意,这些树不会被类型检查器之后的任何阶段转换。javaArgs 中的参数表示为从 scala.reflect.api.Names#Namescala.reflect.api.Annotations#JavaArgument 的映射。JavaArgument 实例表示不同种类的 Java 注解参数

  • 文字(原始值和字符串常量)、
  • 数组,以及
  • 嵌套注解。

名称

名称是字符串的简单包装器。Name 有两个子类型 TermNameTypeName,它们区分术语名称(如对象或成员)和类型名称(如类、特征和类型成员)。同名的术语和类型可以共存于同一对象中。换句话说,类型和术语有独立的名称空间。

名称与一个宇宙相关联。示例

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

scala> val mapName = TermName("map")
mapName: scala.reflect.runtime.universe.TermName = map

上面,我们正在创建一个与运行时反射宇宙关联的 Name(这在其路径相关类型 reflect.runtime.universe.TermName 中也可见)。

名称通常用于查找类型的成员。例如,要搜索在 List 类中声明的 map 方法(这是一个术语),可以执行以下操作

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

scala> listTpe.member(mapName)
res1: scala.reflect.runtime.universe.Symbol = method map

要搜索类型成员,可以使用相同的过程,使用 TypeName 代替。

标准名称

某些名称(例如“_root_”)在 Scala 程序中具有特殊含义。因此,它们对于反射性地访问某些 Scala 构造至关重要。例如,反射性地调用构造函数需要使用标准名称 universe.termNames.CONSTRUCTOR,术语名称 <init> 表示 JVM 上的构造函数名称。

两者都有

  • 标准术语名称,例如,“<init>”、“package”和“_root_”,以及
  • 标准类型名称,例如,“<error>”、“_”和“_*”。

某些名称(如“package”)既存在于类型名称中,也存在于术语名称中。标准名称可通过类 UniversetermNamestypeNames 成员获得。有关所有标准名称的完整规范,请参阅 API 文档

作用域

作用域对象通常将名称映射到相应词法作用域中可用的符号。作用域可以嵌套。然而,反射 API 中公开的基本类型仅公开一个最小界面,将作用域表示为 Symbol 的可迭代对象。

scala.reflect.api.Types#TypeApi 中定义的 membersdecls 返回的成员作用域中公开了其他功能。 scala.reflect.api.Scopes#MemberScope 支持 sorted 方法,该方法按声明顺序对成员进行排序。

以下示例按声明顺序返回 List 类的所有最终成员的符号列表

scala> val finals = listTpe.decls.sorted.filter(_.isFinal)
finals: List(method isEmpty, method map, method collect, method flatMap, method takeWhile, method span, method foreach, method reverse, method foldRight, method length, method lengthCompare, method forall, method exists, method contains, method find, method mapConserve, method toList)

Expr

除了抽象语法树的基本类型 scala.reflect.api.Trees#Tree 外,类型化树还可以表示为类型 scala.reflect.api.Exprs#Expr 的实例。 Expr 封装了一个抽象语法树和一个内部类型标记,以提供对树类型的访问。 Expr 主要用于简单方便地创建类型化抽象语法树,以便在宏中使用。在大多数情况下,这涉及方法 reifysplice(有关详细信息,请参阅 宏指南)。

标志和标志集

标志用于通过 scala.reflect.api.Trees#Modifiersflags 字段为表示定义的抽象语法树提供修饰符。接受修饰符的树是

  • scala.reflect.api.Trees#ClassDef。类和特质。
  • scala.reflect.api.Trees#ModuleDef。对象。
  • scala.reflect.api.Trees#ValDef。Vals、vars、参数和自身类型注释。
  • scala.reflect.api.Trees#DefDef。方法和构造函数。
  • scala.reflect.api.Trees#TypeDef。类型别名、抽象类型成员和类型参数。

例如,要创建一个名为 C 的类,可以编写类似以下内容

ClassDef(Modifiers(NoFlags), TypeName("C"), Nil, ...)

此处,标志集为空。要使 C 为私有,可以编写类似以下内容

ClassDef(Modifiers(PRIVATE), TypeName("C"), Nil, ...)

标志还可以与竖线运算符 (|) 结合使用。例如,私有 final 类可以编写类似以下内容

ClassDef(Modifiers(PRIVATE | FINAL), TypeName("C"), Nil, ...)

所有可用标志的列表在 scala.reflect.api.FlagSets#FlagValues 中定义,可通过 scala.reflect.api.FlagSets#Flag 获得。(通常,为此使用通配符导入,例如, import scala.reflect.runtime.universe.Flag._。)

定义树被编译为符号,以便这些树的修饰符上的标志转换为结果符号上的标志。与树不同,符号不公开标志,而是提供遵循 isXXX 模式的测试方法(例如, isFinal 可用于测试 final 性)。在某些情况下,这些测试方法需要使用 asTermasTypeasClass 进行转换,因为某些标志仅对特定类型的符号有意义。

注意:反射 API 的这一部分被认为是重新设计的候选部分。在反射 API 的未来版本中,标志集很可能被其他内容替换。

常量

Scala 规范称为常量表达式的某些表达式可以在编译时由 Scala 编译器求值。以下类型的表达式是编译时常量(请参阅 Scala 语言规范的第 6.24 节

  1. 基本值类的字面量(ByteShortIntLongFloatDoubleCharBooleanUnit) - 直接表示为对应的类型。

  2. 字符串字面量 - 表示为字符串的实例。

  3. 对类的引用,通常使用 scala.Predef#classOf 构造 - 表示为 类型

  4. 对 Java 枚举值引用 - 表示为 符号

常量表达式用于表示

  • 抽象语法树中的文字(参见 scala.reflect.api.Trees#Literal),以及
  • Java 类文件注释的文字参数(参见 scala.reflect.api.Annotations#LiteralArgument)。

示例

scala> Literal(Constant(5))
val res6: reflect.runtime.universe.Literal = 5

以上表达式创建一个 AST,表示 Scala 源代码中的整数文字 5

Constant 是“虚拟案例类”的一个示例,,一个可以构建其实例并对其进行匹配的类,就好像它是一个案例类一样。两种类型 LiteralLiteralArgument 都具有一个 value 方法,该方法返回文字底层的编译时常量。

示例

Constant(true) match {
  case Constant(s: String)  => println("A string: " + s)
  case Constant(b: Boolean) => println("A Boolean value: " + b)
  case Constant(x)          => println("Something else: " + x)
}
assert(Constant(true).value == true)

类引用表示为 scala.reflect.api.Types#Type 的实例。可以使用 RuntimeMirror(例如 scala.reflect.runtime.currentMirror)的 runtimeClass 方法将此类引用转换为运行时类。(必须从类型转换为运行时类,因为当 Scala 编译器处理类引用时,底层运行时类可能尚未编译。)

Java 枚举值引用表示为符号(scala.reflect.api.Symbols#Symbol 的实例),在 JVM 上指向返回底层枚举值的方法。RuntimeMirror 可用于检查底层枚举或获取对枚举的引用的运行时值。

示例

// Java source:
enum JavaSimpleEnumeration { FOO, BAR }

import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface JavaSimpleAnnotation {
  Class<?> classRef();
  JavaSimpleEnumeration enumRef();
}

@JavaSimpleAnnotation(
  classRef = JavaAnnottee.class,
  enumRef = JavaSimpleEnumeration.BAR
)
public class JavaAnnottee {}

// Scala source:
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{currentMirror => cm}

object Test extends App {
  val jann = typeOf[JavaAnnottee].typeSymbol.annotations(0).javaArgs

  def jarg(name: String) = jann(TermName(name)) match {
    // Constant is always wrapped in a Literal or LiteralArgument tree node
    case LiteralArgument(ct: Constant) => value
    case _ => sys.error("Not a constant")
  }

  val classRef = jarg("classRef").value.asInstanceOf[Type]
  println(showRaw(classRef))         // TypeRef(ThisType(), JavaAnnottee, List())
  println(cm.runtimeClass(classRef)) // class JavaAnnottee

  val enumRef = jarg("enumRef").value.asInstanceOf[Symbol]
  println(enumRef)                   // value BAR

  val siblings = enumRef.owner.typeSignature.decls
  val enumValues = siblings.filter(sym => sym.isVal && sym.isPublic)
  println(enumValues)                // Scope {
                                     //   final val FOO: JavaSimpleEnumeration;
                                     //   final val BAR: JavaSimpleEnumeration
                                     // }

  val enumClass = cm.runtimeClass(enumRef.owner.asClass)
  val enumValue = enumClass.getDeclaredField(enumRef.name.toString).get(null)
  println(enumValue)                 // BAR
}

打印机

用于漂亮地打印 TreesTypes 的实用程序。

打印树

方法 show 显示反射工件的“美化”表示形式。此表示形式为用户提供 Scala 代码的去糖化 Java 表示形式。例如

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

scala> def tree = reify { final class C { def x = 2 } }.tree
tree: scala.reflect.runtime.universe.Tree

scala> show(tree)
res0: String =
{
  final class C extends AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def x = 2
  };
  ()
}

方法 showRaw 显示给定反射对象的内部结构,作为 Scala 抽象语法树 (AST),这是 Scala 类型检查器操作的表示形式。

请注意,虽然此表示形式似乎生成了正确的树,人们可能会认为可以在宏实现中使用这些树,但这通常并非如此。符号没有得到完全表示(只有它们的名称)。因此,此方法最适合用于简单地检查给定一些有效 Scala 代码的 AST。

scala> showRaw(tree)
res1: String = Block(List(
  ClassDef(Modifiers(FINAL), TypeName("C"), 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("x"), List(), List(), TypeTree(),
        Literal(Constant(2))))))),
  Literal(Constant(())))

方法 showRaw 还可以打印正在检查的工件旁边的 scala.reflect.api.Types

scala> import scala.tools.reflect.ToolBox // requires scala-compiler.jar
import scala.tools.reflect.ToolBox

scala> import scala.reflect.runtime.{currentMirror => cm}
import scala.reflect.runtime.{currentMirror=>cm}

scala> showRaw(cm.mkToolBox().typeCheck(tree), printTypes = true)
res2: String = Block[1](List(
  ClassDef[2](Modifiers(FINAL), TypeName("C"), List(), Template[3](
    List(Ident[4](TypeName("AnyRef"))),
    emptyValDef,
    List(
      DefDef[2](Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree[3](),
        Block[1](List(
          Apply[4](Select[5](Super[6](This[3](TypeName("C")), typeNames.EMPTY), ...))),
          Literal[1](Constant(())))),
      DefDef[2](Modifiers(), TermName("x"), List(), List(), TypeTree[7](),
        Literal[8](Constant(2))))))),
  Literal[1](Constant(())))
[1] TypeRef(ThisType(scala), scala.Unit, List())
[2] NoType
[3] TypeRef(NoPrefix, TypeName("C"), List())
[4] TypeRef(ThisType(java.lang), java.lang.Object, List())
[5] MethodType(List(), TypeRef(ThisType(java.lang), java.lang.Object, List()))
[6] SuperType(ThisType(TypeName("C")), TypeRef(... java.lang.Object ...))
[7] TypeRef(ThisType(scala), scala.Int, List())
[8] ConstantType(Constant(2))

打印类型

方法 show 可用于生成类型的可读字符串表示形式

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

scala> def tpe = typeOf[{ def x: Int; val y: List[Int] }]
tpe: scala.reflect.runtime.universe.Type

scala> show(tpe)
res0: String = scala.AnyRef{def x: Int; val y: scala.List[Int]}

scala.reflect.api.TreesshowRaw 方法类似,scala.reflect.api.TypesshowRaw 提供了 Scala 类型检查器操作的 Scala AST 的可视化。

scala> showRaw(tpe)
res1: String = RefinedType(
  List(TypeRef(ThisType(scala), TypeName("AnyRef"), List())),
  Scope(
    TermName("x"),
    TermName("y")))

showRaw 方法还具有命名参数 printIdsprintKinds,两者均具有默认参数 false。当将 true 传递给它们时,showRaw 还会显示符号的唯一标识符及其类型(包、类型、方法、getter 等)。

scala> showRaw(tpe, printIds = true, printKinds = true)
res2: String = RefinedType(
  List(TypeRef(ThisType(scala#2043#PK), TypeName("AnyRef")#691#TPE, List())),
  Scope(
    TermName("x")#2540#METH,
    TermName("y")#2541#GET))

位置

位置(Position 特征的实例)用于跟踪符号和树节点的来源。它们通常在显示警告和错误时使用,以指示程序中的不正确点。位置指示源文件中的列和行(从源文件开头算起的偏移量称为其“点”,有时使用起来不太方便)。它们还包含所引用的行的内容。并非所有树或符号都有位置;使用 NoPosition 对象指示缺少的位置。

位置可以仅引用源文件中的单个字符,也可以引用范围。在后一种情况下,将使用范围位置(不是范围位置的位置也称为偏移位置)。范围位置还具有 startend 偏移量。可以使用 focusStartfocusEnd 方法“聚焦”startend 偏移量,这些方法返回位置(当在不是范围位置的位置上调用时,它们只返回 this)。

可以使用诸如 precedes 之类的方法比较位置,如果两个位置都已定义(,位置不是 NoPosition),并且 this 位置的终点不比给定位置的起点大,则该方法成立。此外,可以测试范围位置是否包含(使用 includes 方法)和是否重叠(使用 overlaps 方法)。

范围位置要么是透明的,要么是不透明的(不透明)。范围位置是否不透明会影响其允许的使用,因为包含范围位置的树必须满足以下不变性

  • 具有偏移位置的树永远不包含具有范围位置的子级
  • 如果具有范围位置的树的子级也具有范围位置,那么子级的范围包含在父级的范围内。
  • 同一节点的子级的非透明范围位置不重叠(这意味着它们的重叠最多为一个点)。

使用 makeTransparent 方法,可以将不透明范围位置转换为透明范围位置;所有其他位置保持不变。

此页面的贡献者