【Scala反射】反射概述

萧然 2019-06-27

概述

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

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

Scala直到2.10这个版本也还没有任何反射工具。而代替方式是,使用Java反射API的一部分,即处理提供动态检查类和对象并访问其成员的能力。然而,在独立Java反射下,许多Scala特定的元素是不可恢复的,它仅公开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 作为参数调用我们的方法,并调用返回 TypeTag 中包含的类型的 tpe。 正如我们所看到的,我们得到了正确的完整类型(包括 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 ++)

在运行时启动类型

通过反射获得的类型可以通过使用适当的 “调用者” mirror 调用它们的构造函数来实例化(mirror 在下面展开)。 让我们通过一个使用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 ...

第一步,我们获得一个mirror 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 类的构造函数的访问。

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

Persons 构造函数的 symbol 可以通过在 Person 类型的声明中查找来仅使用运行时universe  ru 来获得。

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)

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

通常,运行时类型的成员可以使用适当的“调用者” mirror 来访问(mirror 在下面展开)。让我们通过一个使用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 ...

正如我们在前面的例子中所做的那样,我们首先获得一个mirror m,它使得所有可用的类和类型都可用,这些类和类型由类加载器加载,该类也加载了类 pPurchase),我们需要它才能访问成员 shipped

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

我们现在查看 shipped 字段的声明,它给了我们一个 TermSymbol(一种 Symbol)。稍后我们需要使用这个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 成员,我们需要一个 mirror 用于我们的特定实例,p 的实例镜像 im。 考虑到我们的实例 mirror,我们可以为表示 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中的运行时类 vs. Scala中的运行时类型

那些习惯使用Java反射在运行时获得Java类实例的人可能已经注意到,在Scala中,我们改为获得运行时类型。

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

首先,我们用一个抽象类型成员 T 定义一个基类 E,并从中得出两个子类 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 Reflection 的 getClassisAssignableFrom 获取表示 CD 运行时类的 java.lang.Class 实例,然后测试运行时类 D 是否为 C 的运行时表示的子类。

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

在上面的代码中,我们看到 D 扩展 C,但最后的结果有点令人惊讶。在执行这个简单的运行时类型检查时,人们会认为问题“C的子类是否为D”的结果是 true 。但是,正如你可能在上面已经注意到的那样,当 CD 被实例化时,Scala编译器实际上分别创建了 CD 的匿名子类。这是由于 Scala 编译器必须将 Scala 特定的(即非Java)语言特性翻译成 Java 字节码中的某些等价物以便能够在JVM上运行。因此,Scala编译器经常在运行时创建用来代替用户定义的类的合成类(即自动生成的类)。这在Scala中相当普遍,并且在使用具有多个Scala功能的Java反射时可以观察到,例如,闭包、类型成员、类型优化、本地类等。

在这些情况下,我们可以使用 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,在包 scala.reflect.api 中提供。 这样可以在使用运行时反射的宏和实现之间共享通用代码。

请注意,宏指南侧重于宏特性,而本指南重点介绍反射API的一般方面。不过,许多概念直接应用于宏,例如抽象语法树,这些将在关于符号,树和类型的章节中详细讨论。

环境

所有反射任务都需要建立适当的环境。这个环境根据反射任务是在运行时还是在编译时完成而不同。在运行时或编译时使用的环境之间的区别被封装在一个所谓的 universe 中。反射环境的另一个重要方面是我们可以反射访问的一组实体。这组实体由所谓的 mirror 确定。

mirror 不仅确定可反射访问的一组实体。 他们还提供对这些实体进行反射的操作。 例如,在运行时反射中,调用者 mirror 可用于调用类的方法或构造函数。

Universe

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

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

import scala.reflect.runtime.universe._

Mirror

Mirror 是 Scala Reflection 的核心部分。通过反射提供的所有信息都可以通过这些所谓的 mirror 访问。根据要获取的信息类型或要采取的反射行为,必须使用不同的 mirror 风格。

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

相关推荐

kekeromer / 0评论 2020-04-30