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 和变量 b 从 getClass() 方法上根本看不出有什么区别,所谓的 <Integer> , <Double> 在底层都会被替换成 Object 或者是泛型的上界类型(你也能因此推测出 Java 泛型不支持基本数据类型的原因)。
对于这个问题,Scala 要更加的严谨:它在这里引入了 Type (类型)的概念。Type 本身不仅囊括传统意义上的 class ,还考虑到同一种 class 之间的类型参数的差异。换句话说,两者若类型 Type 相同,则它们必属于同一个类 class ,但反之,两者属于同一类 class ,但是它们的类型 Type 却未必相同。
1. 运行时反射
Scala 的运行时反射机制中存在两种 api:一个是针对类型 Type 的 TypeTag 与 WeakTypeTag,另一种是针对类(不考虑类型参数) class 的 ClassTag 。显然,前者对相等性的要求更加严格。在 2.10 版本之前,Scala 还提供了 Manifest,ClassManifest 这两个和运行时反射有关的 api ,不过现在来看,它们已经过时了,因此笔者不会再去了解它们。
1.0 绪论:一些声明
对于 Scala 的运行时反射,我们首先需要导入以下依赖(无论是在 REPL 还是 .scala 文件中)。而 scala.reflect.runtime.universe 实际上是一个延迟加载的 JavaUniverse 类型实例,有关于它的声明可以在 scala.reflect.runtime 包对象中找到。
import scala.reflect.runtime.universe._
在 1. 运行时反射 章节所提到的 TypeTag ,WeakTypeTag 均是以 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 方法获取到 T 的 Type 。上述代码块则是将隐式参数转换为了上下文界定的写法。
注意,在调用此 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 之间的比较方法。设 A 和 B 都属于 Type 类型:
A =:= B,表示A与B是同一种类型,包含它们的类型参数。A <:< B,表示A是B的子类型,或者是同一种类型。
对于完全相同的类型,无论是 <:< 还是 =:= ,结果都是 true。然而,Type 的比较并没有想象中那么简单,下面笔者要讨论两种情况。
1.1.2 Type 在泛型类中的相等性
Scala 的泛型是支持形变的。对于泛型类来说何为 "父子",何为 "相等",我们还需要进一步讨论。现列出实际的代码清单,主要的比较对象是 A 和 B 特质,并通过获取 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
注意,T 和 U 都是不变的。这意味着对于 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
倘若 T 和 U 是协变的,这意味着对于 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
对于 T 与 U 是逆变的情况,其结果应该也很容易推导出来,笔者这里不再给出。
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[_]类之后,我们就可以通过 getAnnotations ,getFields , getMethods 等方法供获取类的信息,使用方法和 Java 传统的反射类似,笔者这里不做重点介绍。classOf 的方法定义在 scala.Predef 下,这意味着不用手动导入任何依赖就可以调用该方法并得到对应的 Class[_]。
1.3.1 通过 ClassTag 传递泛型数组
观察下方的逻辑代码。myMap 本质上是为了实现 Array[A] => Array[B] 的功能,而重点在于附加的 using caluse ( Scala 2 版本中相当于 implicit clause )。
def myMap[A,B](array: Array[A])(convert : A => B)(using ev$1: scala.reflect.ClassTag[B]): Array[B] =
// 计划返回一个 Array[B] 类型的数据。
for (x <- array) yield convert(x)
如果没有它,那么程序将不能通过编译,原因在于 Array[B],归根结底是 Java 的数组创建机制导致的。如果觉得这难以理解,不妨先从下面的 Java 问题代码入手:
public <T> T[] newSeqs() {
// Type parameter 'T' cannot be instantiated directly
return new T[100]; // error!
}
为了保证类型安全,Java 编译器总是会拦截直接创建泛型数组的行为 ( 因为数组元素的类型信息要求编译期确定,但这却和类型擦除机制冲突 )。类似地,Scala 也不会允许直接构造一个 Array[B],原因也是一样的。
想要打破这个局面,则需要在运行期利用 Scala 的反射机制保存 B 的类型并传递,从而保证运行期构造正确的 Array 。这也是声明这个 using clause 的原因。见:Scala ERROR: No ClassTag available for A (n3xtchen.github.io) | Generic arrays in Java - Stack Overflow | Scala generic method - No ClassTag available for T - Stack Overflow | scala - No ClassTag available for A - but implicit parameter present - Stack Overflow
1.4 Mirror
反射就像一面镜子——只需要把实例放在这面镜子之前,你就可以知道它的全貌。
你可以将 Scala 的反射过程理解成是获取到了目标的 “镜像” (即 “mirror”),并且可以通过此镜像获取到此对象内部的属性,方法等等。根据反射的内容不同,笔者整理出了 Mirror 的以下层级:
这里额外定义一个用于实验反射的 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 。对于这个实例的镜像,还可以尝试获取两个子镜像,分别用于获取实例的内部方法和内部属性:
MethodMirror:用于反射该实例内部方法的镜像。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()