反射

概述

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

实验性

Heather Miller、Eugene Burmako、Philipp Haller

反射是程序检查甚至修改自身的能力。它在面向对象、函数式和逻辑编程范式中有着悠久的历史。虽然一些语言以反射为指导原则而构建,但许多语言随着时间的推移逐渐发展了它们的反射能力。

反射涉及具体化(即显式化)程序中原本隐含的元素的能力。这些元素可以是类、方法或表达式等静态程序元素,也可以是当前延续或执行事件(如方法调用和字段访问)等动态元素。人们通常根据反射过程执行的时间来区分编译时反射和运行时反射。编译时反射是开发程序转换器和生成器的强大方式,而运行时反射通常用于调整语言语义或支持软件组件之间的非常晚绑定。

在 2.10 之前,Scala 没有任何自己的反射功能。相反,可以使用 Java 反射 API 的一部分,即处理提供动态检查类和对象并访问其成员的能力。但是,许多特定于 Scala 的元素在独立的 Java 反射下无法恢复,Java 反射仅公开 Java 元素(没有函数、没有特质)和类型(没有存在性、高阶、路径相关和抽象类型)。此外,Java 反射还无法恢复编译时为泛型的 Java 类型的运行时类型信息;这一限制适用于 Scala 中泛型类型的运行时反射。

在 Scala 2.10 中,引入了一个新的反射库,不仅解决了 Java 的运行时反射在特定于 Scala 和泛型类型上的缺陷,还为 Scala 添加了一个更强大的通用反射功能工具包。除了针对 Scala 类型和泛型的全功能运行时反射外,Scala 2.10 还附带编译时反射功能,以 的形式出现,以及将 Scala 表达式具体化为抽象语法树的能力。

运行时反射

什么是运行时反射?在运行时给定某个对象的类型或实例,反射是

  • 检查该对象的类型,包括泛型类型,
  • 实例化新对象,
  • 或访问或调用该对象的成员。

让我们深入了解并通过一些示例了解如何执行上述各项操作。

示例

检查运行时类型(包括运行时的泛型类型)

与其他 JVM 语言一样,Scala 的类型在编译时会擦除。这意味着,如果你要检查某个实例的运行时类型,你可能无法访问 Scala 编译器在编译时可用的所有类型信息。

TypeTag 可以被认为是携带编译时所有类型信息到运行时的对象。不过,需要注意的是,TypeTag 始终由编译器生成。每当使用需要 TypeTag 的隐式参数或上下文绑定时,都会触发此生成。这意味着,通常情况下,人们只能使用隐式参数或上下文绑定获取 TypeTag

例如,使用上下文绑定

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)

scala> def getTypeTag[T: ru.TypeTag](obj: T) = ru.typeTag[T]
getTypeTag: [T](obj: T)(implicit evidence$1: ru.TypeTag[T])ru.TypeTag[T]

scala> val theType = getTypeTag(l).tpe
theType: ru.Type = List[Int]

在上面,我们首先导入 scala.reflect.runtime.universe(必须始终导入它才能使用 TypeTag),并且我们创建了一个名为 lList[Int]。然后,我们定义了一个方法 getTypeTag,它有一个类型参数 T,它有一个上下文绑定(如 REPL 所示,这等同于定义一个隐式“证据”参数,它会导致编译器为 T 生成一个 TypeTag)。最后,我们使用 l 作为其参数调用我们的方法,并调用 tpe,它返回 TypeTag 中包含的类型。正如我们所看到的,我们得到了正确、完整类型(包括 List 的具体类型参数),List[Int]

一旦我们获得了所需的 Type 实例,我们就可以检查它,例如

scala> val decls = theType.decls.take(10)
decls: Iterable[ru.Symbol] = List(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++)

在运行时实例化类型

通过反射获得的类型可以通过使用适当的“调用者”镜像(镜像在 下面进行了扩展)调用它们的构造函数来实例化。让我们通过 REPL 演示一个示例

scala> case class Person(name: String)
defined class Person

scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: scala.reflect.runtime.universe.Mirror = JavaMirror with ...

在第一步中,我们获得一个镜像 m,它使当前类加载器加载的所有类和类型可用,包括类 Person

scala> val classPerson = ru.typeOf[Person].typeSymbol.asClass
classPerson: scala.reflect.runtime.universe.ClassSymbol = class Person

scala> val cm = m.reflectClass(classPerson)
cm: scala.reflect.runtime.universe.ClassMirror = class mirror for Person (bound to null)

第二步涉及使用 reflectClass 方法为类 Person 获取 ClassMirrorClassMirror 提供对类 Person 的构造函数的访问。(如果此步骤导致异常,简单的解决方法是在启动 REPL 时使用这些标志。 scala -Yrepl-class-based:false

scala> val ctor = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod
ctor: scala.reflect.runtime.universe.MethodSymbol = constructor Person

仅使用运行时 universe ru 就可以通过在类型 Person 的声明中查找 Person 构造函数的符号。

scala> val ctorm = cm.reflectConstructor(ctor)
ctorm: scala.reflect.runtime.universe.MethodMirror = constructor mirror for Person.<init>(name: String): Person (bound to null)

scala> val p = ctorm("Mike")
p: Any = Person(Mike)

访问和调用运行时类型的成员

通常,使用适当的“调用方”镜像访问运行时类型的成员(镜像在 此处 进行扩展)。让我们使用 REPL 演示一个示例

scala> case class Purchase(name: String, orderNumber: Int, var shipped: Boolean)
defined class Purchase

scala> val p = Purchase("Jeff Lebowski", 23819, false)
p: Purchase = Purchase(Jeff Lebowski,23819,false)

在此示例中,我们将尝试通过反射获取和设置 Purchase pshipped 字段。

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> val m = ru.runtimeMirror(p.getClass.getClassLoader)
m: scala.reflect.runtime.universe.Mirror = JavaMirror with ...

与前一个示例中一样,我们将首先获取一个镜像 m,它使类加载器加载的所有类和类型可用,该类加载器也加载了 pPurchase)的类,这是我们访问成员 shipped 所需的。

scala> val shippingTermSymb = ru.typeOf[Purchase].decl(ru.TermName("shipped")).asTerm
shippingTermSymb: scala.reflect.runtime.universe.TermSymbol = method shipped

我们现在查找 shipped 字段的声明,它为我们提供一个 TermSymbolSymbol 的一种类型)。我们稍后需要使用此 Symbol 来获取镜像,以便我们访问该字段的值(对于某些实例)。

scala> val im = m.reflect(p)
im: scala.reflect.runtime.universe.InstanceMirror = instance mirror for Purchase(Jeff Lebowski,23819,false)

scala> val shippingFieldMirror = im.reflectField(shippingTermSymb)
shippingFieldMirror: scala.reflect.runtime.universe.FieldMirror = field mirror for Purchase.shipped (bound to Purchase(Jeff Lebowski,23819,false))

为了访问特定实例的 shipped 成员,我们需要一个特定实例的镜像,即 p 的实例镜像 im。给定我们的实例镜像,我们可以为表示 p 类型字段的任何 TermSymbol 获取 FieldMirror

现在我们为特定字段获取了 FieldMirror,我们可以使用 getset 方法来获取/设置特定实例的 shipped 成员。让我们将 shipped 的状态更改为 true

scala> shippingFieldMirror.get
res7: Any = false

scala> shippingFieldMirror.set(true)

scala> shippingFieldMirror.get
res9: Any = true

Java 中的运行时类与 Scala 中的运行时类型

习惯于使用 Java 反射在运行时获取 Java Class 实例的人可能会注意到,在 Scala 中,我们获取的是运行时 类型

下面在 REPL 中运行的代码展示了一个非常简单的场景,其中对 Scala 类使用 Java 反射可能会返回令人惊讶或不正确的结果。

首先,我们定义一个基类 E,它具有一个抽象类型成员 T,然后从该基类派生出两个子类 CD

scala> class E {
     |   type T
     |   val x: Option[T] = None
     | }
defined class E

scala> class C extends E
defined class C

scala> class D extends C
defined class D

然后,我们创建 CD 的实例,同时使类型成员 T 具体化(在两种情况下,都是 String

scala> val c = new C { type T = String }
c: C{type T = String} = $anon$1@7113bc51

scala> val d = new D { type T = String }
d: D{type T = String} = $anon$1@46364879

现在,我们使用 Java 反射中的方法 getClassisAssignableFrom 来获取 java.lang.Class 的实例,该实例表示 cd 的运行时类,然后我们测试以查看 d 的运行时类是否是 c 的运行时表示的子类。

scala> c.getClass.isAssignableFrom(d.getClass)
res6: Boolean = false

从上面,我们看到 D 扩展 C,这个结果有点令人惊讶。在执行这个简单的运行时类型检查时,人们会期望“d 的类是 c 的类的子类吗?”问题的答案是 true。然而,正如你可能在上面注意到的,当实例化 cd 时,Scala 编译器实际上创建了 CD 的匿名子类。这是因为 Scala 编译器必须将 Scala 特有的(非 Java)语言特性转换为 Java 字节码中的一些等效项,以便能够在 JVM 上运行。因此,Scala 编译器经常创建合成类(即自动生成的类),这些类在运行时用于代替用户定义的类。这在 Scala 中很常见,并且在使用 Java 反射与许多 Scala 特性(例如闭包、类型成员、类型细化、局部类)时可以观察到。

在这样的情况下,我们可以使用 Scala 反射来获取这些 Scala 对象的精确运行时类型。Scala 运行时类型携带了编译时期的所有类型信息,避免了编译时和运行时之间的这些类型不匹配。

在下面,我们定义了一个方法,该方法使用 Scala 反射来获取其参数的运行时类型,然后检查这两个类型之间的子类型关系。如果其第一个参数的类型是其第二个参数类型的子类型,则返回 true

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> def m[T: ru.TypeTag, S: ru.TypeTag](x: T, y: S): Boolean = {
    |   val leftTag = ru.typeTag[T]
    |   val rightTag = ru.typeTag[S]
    |   leftTag.tpe <:< rightTag.tpe
    | }
m: [T, S](x: T, y: S)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T], implicit evidence$2: scala.reflect.runtime.universe.TypeTag[S])Boolean

scala> m(d, c)
res9: Boolean = true

正如我们所看到的,我们现在得到了预期的结果—— d 的运行时类型实际上是 c 的运行时类型的子类型。

编译时反射

Scala 反射启用了一种元编程形式,它使程序能够在编译时修改自身。这种编译时反射以宏的形式实现,它提供了在编译时操作抽象语法树的方法的能力。

宏的一个特别有趣的方面是,它们基于与 Scala 的运行时反射也使用的相同 API,该 API 在包 scala.reflect.api 中提供。这使得在宏和利用运行时反射的实现之间共享通用代码成为可能。

请注意,宏指南重点关注宏的细节,而本指南重点关注反射 API 的一般方面。许多概念直接适用于宏,例如在符号、树和类型部分中更详细讨论的抽象语法树。

环境

所有反射任务都需要设置一个合适的环境。该环境根据反射任务是在运行时还是在编译时完成而有所不同。在运行时或编译时使用的环境之间的区别封装在一个所谓的宇宙中。反射环境的另一个重要方面是我们具有反射访问权限的实体集。该实体集由所谓的镜像决定。

镜像不仅决定了可以反射访问的实体集。它们还提供对这些实体执行的反射操作。例如,在运行时反射中,调用者镜像可用于调用类的某个方法或构造函数。

宇宙

Universe 是 Scala 反射的入口点。宇宙提供了一个接口,用于反射中使用的所有主要概念,例如 TypesTreesAnnotations。有关更多详细信息,请参阅本指南中关于 宇宙 的部分,或 Universe API 文档 中的包 scala.reflect.api

要使用 Scala 反射的大多数方面,包括本指南中提供的大多数代码示例,需要确保导入 UniverseUniverse 的成员。通常,要使用运行时反射,可以使用通配符导入来导入 scala.reflect.runtime.universe 的所有成员

import scala.reflect.runtime.universe._

镜像

Mirror 是 Scala 反射的核心部分。反射提供的所有信息都可以通过这些所谓的镜像访问。根据要获取的信息类型或要执行的反射操作,必须使用不同类型的镜像。

有关更多详细信息,请参阅本指南中关于 镜像 的部分,或 Mirrors API 文档 中的包 scala.reflect.api

此页面的贡献者