谈谈Scala的运行时反射

Java 的反射机制使得程序可以在运行期间获取一个类的信息,来让我们的程序具备灵活性,学习 Scala 反射的目的也是相同的。Scala 的反射其实分为两个范畴:运行时反射,编译时反射。而这里仅介绍常用的运行时反射。编译时反射主要用于元编程 —— 而这个工作笔者会偏向于选择用另一门灵活且使用简单的 JVM 语言完成,那就是 Groovy。见: Groovy AST 之旅 - 掘金 (juejin.cn)

在理解反射之前,我们应当熟悉了 Scala 的泛型,形变,上下文界定,隐式转换等概念,并对 Java 泛型擦除有足够的了解。

此篇是介绍 Scala 基本概念的最后一部分内容。笔者后续在此专栏中讨论的话题将是 "如何用 Scala 优雅地实现函数式编程"。

0. 类型擦除:泛型不过是镜花水月

有关 Java 类型擦除机制的概念复习来源于这一篇文章:Java 类型擦除

在泛型出现之前,对同一类数据类型的抽象都可使用 class (类)来概括,且不会引发任何歧义。但在泛型的概念出现之后,这种说法产生了微妙的变化:List<Int>List<Double> 应该算是相同的类(class)吗?

public class Generic {
    public static void main(String[] args) {

        //它们在底层都会被编译成 LinkedList x = new LinkedList();
        LinkedList<Integer> a = new LinkedList<>();
        LinkedList<Double> b = new LinkedList<>();

        System.out.println(a.getClass() == b.getClass());
    }
}
复制代码

由于 Java 并不涉及高阶类型,这段代码经编译器编译之后,变量 a 和变量 bgetClass() 方法上根本看不出有什么区别,所谓的 <Integer> , <Double> 在底层都会被替换成 Object 或者是泛型的上界类型(你也能因此推测出 Java 泛型不支持基本数据类型的原因)。

对于这个问题,Scala 要更加的严谨:它在这里引入了 Type (类型)的概念。Type 本身不仅囊括传统意义上的 class ,还考虑到同一种 class 之间的类型参数的差异。换句话说,两者若类型 Type 相同,则它们必属于同一个类 class ,但反之,两者属于同一类 class ,但是它们的类型 Type 却未必相同。

1. 运行时反射

Scala 的运行时反射机制中存在两种 api:一个是针对类型 TypeTypeTagWeakTypeTag,另一种是针对类(不考虑类型参数) classClassTag 。显然,前者对相等性的要求更加严格。在 2.10 版本之前,Scala 还提供了 ManifestClassManifest 这两个和运行时反射有关的 api ,不过现在来看,它们已经过时了,因此笔者不会再去了解它们。

1.0 绪论:一些声明

对于 Scala 的运行时反射,我们首先需要导入以下依赖(无论是在 REPL 还是 .scala 文件中)。而 scala.reflect.runtime.universe 实际上是一个延迟加载的 JavaUniverse 类型实例,有关于它的声明可以在 scala.reflect.runtime 包对象中找到。

import scala.reflect.runtime.universe._
复制代码

1. 运行时反射 章节所提到的 TypeTagWeakTypeTag 均是以 universe 为前缀的路径依赖类型,它们实际上是作为scala.reflect.api.TypeTags 的内部类 (这里还涉及到了错综复杂的继承关系,但是笔者在这里忽略掉了)。而 ClassTag 则是位于其它位置的独立类型。即:

TypeTag => scala.reflect.runtime.universe.TypeTag ==>  scala.reflect.api.TypeTags#TpyeTag
WeakTypeTag => scala.reflect.runtime.universe.WeakTypeTag => scala.reflect.api.TypeTags#WeakTypeTag
ClassTag => scala.reflect.ClassTag
复制代码

同样,诸如 typeOf, weakTypeOf 等方法实际上都是 universe 实例所提供的方法。

typeOf[T] => scala.reflect.runtime.universe.typeOf[T]
weakTypeOf[T] => scala.reflect.runtime.universe.weakTypeOf[T]
...
复制代码

如果不想导入 universe 的全部内容,也可以用这种方式来调用它们(有些情况下,笔者为了避开意外地导入隐式转换而可以选择这样做,比如在 Mirror 章节就是这样做的):

// ru : runtime
val ru = scala.reflect.runtime.universe
ru.typeOf[T]    // => scala.reflect.runtime.universe.typeOf[T]
ru.TypeTag[T]   // => scala.reflect.runtime.universe.TypeTag[T]
复制代码

出于阅读体验,笔者在下文中会省略掉它们的路径(因为实在是太长了)。另,本篇的代码有相当一部分是在 REPL 交互式环境中运行的,你可以通过主机的 scala 命令进入终端内进行操作。

1.1 TypeTag

typeTag (首字母小写)是一个泛型方法,用于获取一个类型的完整信息,并包装到 TypeTag (首字母大写)类当中返回,包括它们内部的类型参数。

scala> val tt = typeTag[List[List[String]]]
tt: reflect.runtime.universe.TypeTag[List[List[String]]] = TypeTag[scala.List[scala.List[String]]]
复制代码

可以通过调用该 TypeTag.tpe 方法从 TypeTag 当中抽取出其完整的类型,并以 Type 类型返回:

scala> tt.tpe
res0: reflect.runtime.universe.Type = scala.List[scala.List[String]]
复制代码

或者直接调用 typeOf 方法获取一个完整类型,以 Type 类型返回:

scala> typeOf[List[List[String]]]
res1: reflect.runtime.universe.Type = scala.List[scala.List[String]]
复制代码

现在,定义一个 getTypeTag 方法,要求可接收任意类型的对象,并获取它的完整类型信息,然后笔者在下一段介绍为什么需要一个 [T:TypeTag] 的上下文界定:

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

scala> val o = List[List[String]]()
o: List[List[String]] = List()

scala> getTypeTag1(o).toString
res2: String = List[scala.List[String]]
复制代码

Scala 会在编译过程中将类型信息保存到 TypeTag 当中并携带到运行期。我们想要知道 T 的具体类型,则必须通过隐式参数要向编译器请求获取一个 TypeTag[T] (类型 T 在编译期就被确定了),我们才能通过 typeOf 方法获取到 TType 。上述代码块则是将隐式参数转换为了上下文界定的写法。

注意,在调用此 getTypeTag 方法时,其类型 T 一定是能够被确定的,如果向它传递了另一个不确定的泛型 U ,则编译器会报错。对于这种情况,应该使用 weakTypeTag 替代之。

1.1.1 实现更精确的模式匹配

这是之前的模式匹配章节遗留下来的问题。受制于 JVM 类型擦除机制,Scala 程序对这样的模式匹配无能为力:

def typeOfMap(map : Map[_,_]): Unit = {
  map match {
    // 实际上无论是何种类型参数的 Map,在编译时会全部被擦除成 Map[Any,Any],因此总会执行第一个分支。
    case _:Map[Int,Int] => println("this is a Map[Int,Int]")
    case _:Map[Int,String] => println("this is a Map[Int,String]")
    case _ => println("not a valid map.")
  }
}

typeOfMap(Map[String,String]())
复制代码

无论设计了多么精密的模式匹配,程序都只能识别出它是不是 Map 类型,而分辨不出来 Map[Int,Int]Map[Int,String] 的区别。而 Type 却能够将包含泛型的完整类型信息返回,我们可以利用它解决这个难题:

def typeOfMap_+[K : TypeTag, V : TypeTag](map: Map[K, V]): Unit = {

    typeOf[K] match {
        case ktp if ktp =:= typeOf[String] => println("key's type is String!")
        case ktp if ktp =:= typeOf[Int] => println("key's type is Int!")
        case _ => println("key is neither String or Int.")
    }

    typeOf[V] match {
        case vtp if vtp =:= typeOf[String] => println("value's type is String!")
        case vtp if vtp =:= typeOf[Int] => println("value's type is Int!")
        case _ => println("value is neither String or Int.")
    }
}

typeOfMap_+(Map[String, String]())
复制代码

这一次,该模式匹配可以准确识别出 Map[_,_] 的具体类型了。

我们用这个例子引出了 Type 之间的比较方法。设 AB 都属于 Type 类型:

  1. A =:= B ,表示 AB 是同一种类型,包含它们的类型参数。
  2. A <:< B ,表示 AB 的子类型,或者是同一种类型

对于完全相同的类型,无论是 <:< 还是 =:= ,结果都是 true。然而,Type 的比较并没有想象中那么简单,下面笔者要讨论两种情况。

1.1.2 Type 在泛型类中的相等性

Scala 的泛型是支持形变的。对于泛型类来说何为 "父子",何为 "相等",我们还需要进一步讨论。现列出实际的代码清单,主要的比较对象是 AB 特质,并通过获取 Type 比较它们的类型。

scala> trait B[T] {}
defined trait B

scala> trait A[U] extends B[U] {}
defined trait A

scala> class Father {}
defined class Father

scala> class Son extends Father {}
defined class Son
复制代码

注意,TU 都是不变的。这意味着对于 B[Father] 类型,只有 A[Father] 才被认为是它的子类型,除此以外的 A[_] 都不会被认为是它的子类型。同样,任何其它的 B[_] 也不会被认为是其子类型。

scala> println(typeTag[B[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Father]].tpe =:= typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Son]].tpe <:< typeTag[B[Father]].tpe)
false

scala> println(typeTag[A[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[B[Father]].tpe)
false

复制代码

倘若 TU 是协变的,这意味着对于 B[Father] 类型,A[Father]A[Son] 或者是 B[Son] 都可认为是它的子类型(包括它自身)。对于 A[Father] 而言,A[Son] 又可以作为它的子类型。

scala> trait B[+T] {}
defined trait B

scala> trait A[+U] extends B[U] {}
defined trait A

scala> class Father
defined class Father

scala> class Son extends Father
defined class Son
复制代码

在 REPL 中试验我们的代码,推理是正确的:泛型的形变会影响到 Type 类型的判断。

scala> println(typeTag[A[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Son]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[A[Father]].tpe)
true
复制代码

对于 TU 是逆变的情况,其结果应该也很容易推导出来,笔者这里不再给出。

1.1.3 Type 在路径依赖类中的相等性

创建一个类 Outer ,内部还有一个成员内部类 Inner ,并生成两个路径不同的 Inner 实例:

scala> class Outer {
     |   class Inner
     | }
defined class Outer {}

scala> val outer1 = new Outer
outer1: Outer = Outer@68f4865

scala> val outer2 = new Outer
outer2: Outer = Outer@4196c360

scala> val inner1 = new outer1.Inner
inner1: outer1.Inner = Outer$Inner@1e44b638

scala> val inner2 = new outer2.Inner
inner2: outer2.Inner = Outer$Inner@7164ca4c
复制代码

命令行中显示它们本质上都是 Outer$Inner 类,因此若比较两者的 class ,其结果为 true。但若比较两者的 Type ,则结果为 false,原因是它们的 "路径"(或称前缀)并不相同。

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

scala> println(typeOf[outer1.Inner] =:= typeOf[outer2.Inner])
false
复制代码

1.2 WeakTypeTag

对于前文的 TypeTag 章节中介绍过的 getTypeTag 方法:

def getTypeTag[T: TypeTag](o: T): Type = typeOf[T]
复制代码

如果再嵌套一层函数并向 getTypeTag 传递一个类型参数 U

def getTypeTag[T: TypeTag](o: T): Type = typeOf[T]

def foo[U] (x : U)= getTypeTag[U](x)
复制代码

这段代码将不能通过编译,并提示:Error:(xx, xx) No TypeTag available for U 。原因在于传递的类型参数 U 也是不确定的,而 TypeTag 要求必须传入明确的类型参数,这样才能通过 .tpe 得到一个完整的类型 Type

WeakTypeTag 提供了更宽泛的限制,即 WeakTypeTag[T] 允许类型参数 T 也是不具体的(或称抽象的)。

def getTypeTag[T: WeakTypeTag](o: T): Type = weakTypeOf[T]

def foo[U] (x : U)= getTypeTag[U](x)
复制代码

以下是 Scala 官方文档中提供的,与 WeakTypeTag 相关的说明:link

1.3 ClassTag

classTag 也是一个泛型方法 ,用于获得被擦除后的类。返回值为 ClassTag 类。ClassTag 位于一个位置相对独立的包下(见绪论的声明),在使用它进行反射之前,首先应当导入 import scala.reflect.ClassType

scala> classTag[List[List[String]]]
res1: scala.reflect.ClassTag[List[List[String]]] = scala.collection.immutable.List
复制代码

可以通过 .runtimeClass 取出其 Class 类型。

scala> res1.runtimeClass
res2: Class[_] = class scala.collection.immutable.List
复制代码

类似于 typeOf 方法,可以直接通过 classOf 泛型方法获取类 Class[_]

scala> classOf[List[List[Int]]]
res3: Class[List[List[Int]]] = class scala.collection.immutable.List
复制代码

在获取到这个 Class[_]类之后,我们就可以通过 getAnnotationsgetFields , getMethods 等方法供获取类的信息,使用方法和 Java 传统的反射类似,笔者这里不做重点介绍。classOf 的方法定义在 scala.Predef 下,这意味着不用手动导入任何依赖就可以调用该方法并得到对应的 Class[_]

1.4 Mirror

反射就像一面镜子——只需要把实例放在这面镜子之前,你就可以知道它的全貌。

你可以将 Scala 的反射过程理解成是获取到了目标的 “镜像” (即 “mirror”),并且可以通过此镜像获取到此对象内部的属性,方法等等。根据反射的内容不同,笔者整理出了 Mirror 的以下层级:

scala_mirror.png

这里额外定义一个用于实验反射的 Person 类型,并尝试着通过反射来解构它(提醒:伴生类和伴生对象总是要定义在一个文件中。如果想要在 REPL 中定义一个伴生对象和伴生类,需要先输入 :paste 进入粘贴模式,将两个定义同时声明完之后再通过 CTRL + D 退出)。

scala> :paste
// Entering paste mode (ctrl-D to finish)
class Person (val name:String,val age:Int) {
  private def unsafe(): Unit = println("private method.")
}

object Person {
  def greet(): Unit = println("hello!")
}
// Exiting paste mode, now interpreting.

defined class Person
defined object Person
复制代码

首先应当从 scala.reflect.runtime.universe 中获取一个运行时的 JavaMirror 。这里有一点细节需要注意,在这个案例中不要直接引入 import scala.reflect.runtime.universe._ (因为部分隐式转换会引发一些冲突)。笔者使用变量 ru ( runtime 的缩写) 接收了 universe 值。

scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@3c9c0d96

scala> val mirror = ru.runtimeMirror(getClass.getClassLoader)
mirror: ru.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@6fb65c1f ...
复制代码

1.4.1 反射实例

通过调用 mirror.reflect(<obj>) 方法,便可以对某个实例进行反射了,比如说尝试着对一个 Person 实例进行反射:

scala> val im = mirror.reflect(new Person("Wangfang",20))
im: reflect.runtime.universe.InstanceMirror = instance mirror for Person@39b626e5
复制代码

它将返回一个 InstanceMirror 。对于这个实例的镜像,还可以尝试获取两个子镜像,分别用于获取实例的内部方法和内部属性:

  1. MethodMirror :用于反射该实例内部方法的镜像。
  2. FieldMirror :用于反射该实例内部属性的镜像。

使用这两种镜像的思路是:先获取到标识符,然后再去相应的镜像当中获取对应的值。

scala> val term: ru.TermSymbol = ru.typeOf[Person].decl(ru.TermName("name")).asTerm
term: ru.TermSymbol = method name

scala> val nameValue: ru.FieldMirror = im.reflectField(term)
nameValue: ru.FieldMirror = field mirror for private[this] var name: String (bound to Person@48f4713c)

scala> println(s"this instance's name value = ${nameValue.get}")
this instance's name value = Wangfang
复制代码

上述交互命令在 .scala 文件中的写法如下:

// 不要引入直接引入 scala.reflect.runtime.universe._ , 否则会引入意外的一些隐式值
//ru : runtime universe
val ru : JavaUniverse = scala.reflect.runtime.universe

// 获取运行时反射的 mirror 入口
val mirror : ru.Mirror = ru.runtimeMirror(getClass.getClassLoader)

// 将输入的字符串 "name" 转换成标识符。
val term: ru.TermSymbol = ru.typeOf[Person].decl(ru.TermName("name")).asTerm

// 通过调用 reflectField(term) 中获取对应属性的镜像,如果该属性有值,则可以通过 get 获取,或者通过 set 设置。
val nameValue: ru.FieldMirror = mirror.reflect(new Person("Wangfang",20)).reflectField(term)
println(s"this instance's name value = ${nameValue.get}")
复制代码

同理,若想要反射实例的方法,首先根据标识符获取其 MethodMIrror ,并通过 apply 方法调用之。它允许接收可变参数,具体取决于被反射的方法所需要的参数。

// 不要引入直接引入 scala.reflect.runtime.universe._ , 否则会引入意外的一些隐式值
//ru : runtime universe
val ru = scala.reflect.runtime.universe

// 获取运行时反射的 mirror 入口
val mirror  = ru.runtimeMirror(getClass.getClassLoader)

// 将输入的字符串 "name" 转换成标识符。
val term = ru.typeOf[Person].decl(ru.TermName("unsafe")).asMethod

// 通过调用 reflectField(term) 中传入
val im: ru.InstanceMirror = mirror.reflect(new Person("Wangfang",20))

//通过反射调用此方法,apply 可以接收该方法所需要的入参。
im.reflectMethod(term).apply()
复制代码

我们也注意到,尽管 unsafe 方法是私有方法,但是仍然可以通过反射的形式获取并运行它(通过 apply() 来实际执行反射得到的方法)。

1.4.2 反射构造器

如果要反射其 Person 类并获取到构造器,则需要通过 ru ( 即 scala.reflect.runtime.universe)获取到其类型的 ClassSymbol 和构造器方法的 MethodSymbol

val clazzPerson: ru.ClassSymbol = ru.typeOf[Person].typeSymbol.asClass

val cm: ru.ClassMirror = mirror.reflectClass(clazzPerson)

// 如果构造器是多个,则情况会比较复杂,需要通过 ↓
// ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asTerm.alternative(x).asMethod 选择列表中备选的第 x 个构造器。
val ctorPerson: ru.MethodSymbol = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod

cm.reflectConstructor(ctorPerson)
复制代码

1.4.3 反射伴生(单例)对象

ModuleMirror 用于反射一个类的单例对象,在这里,ru.typeOf[_] 的类型参数需要用 Person.type ,而不是 Person

val objectPerson: ru.ModuleSymbol = ru.typeOf[Person.type].termSymbol.asModule

val mm: ru.ModuleMirror = mirror.reflectModule(objectPerson)

mm.instance.asInstanceOf[Person.type].greet()
复制代码

1.5 参考资料:运行时反射