CLOVER🍀

That was when it all began.

ScalaのReflectionについて、まとめてみる - インスタンス操作編

前回までは、クラスやトレイトなどの定義を取得するような、いわゆる解析、静的操作系の話題をまとめていましたが、今回はインスタンスの操作について書いていこうと思います。

要は、動的にインスタンスを生成したり、メソッド呼び出しをしたりといったところですね。

これまでに書いたエントリのまとめは、こちらです。

導入編
http://d.hatena.ne.jp/Kazuhira/20130730/1375192075
定義情報取得編 - 1
http://d.hatena.ne.jp/Kazuhira/20130801/1375370390
定義情報取得編 - 2
http://d.hatena.ne.jp/Kazuhira/20130803/1375526971
インスタンス操作編
http://d.hatena.ne.jp/Kazuhira/20130804/1375604912
オマケ
http://d.hatena.ne.jp/Kazuhira/20130804/1375607954

前回までと同じく、これから記載するソースには以下のimport文が書かれているものとします。

import scala.reflect.runtime.universe

リフレクションで操作するクラスの定義は、以下とします。

class SuperClass {
  private val superValField: String = "super val field"
  var superVarField: String = _

  def superMethod(param: String): String =
    s"Hello ${param}!"
}

class SubClass(name: String) extends SuperClass {
  val valField: String = "val field"
  var varField: String = _

  def method(param1: String, param2: String): String =
    s"SubClass[$name]:method, params [${param1}, ${param2}]"
}

SubClassの方を中心に扱います。

Typeについては、以下で取得しているものとします。

val theType = universe.typeOf[SubClass]

では、いってみましょう。

JavaMirrorを取得する

インスタンスを動的に操作するような場合は、とにかくJavaMirrorがないと始まりません。

Mirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirror

JavaMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.JavaMirrors$JavaMirror

JavaMirrorは、UniverseまたはTypeTagから取得することができます。

val runtimeMirror =
  universe.runtimeMirror(Thread.currentThread.getContextClassLoader)

もしくは、こうです。

val runtimeMirror = universe.typeTag[SubClass].mirror

それぞれの例で使用しているクラスローダーが違う(と思われる)のは、ご愛嬌…。

インスタンスを動的に生成したい

インスタンスをリフレクションで生成するには、以下のクラスを使用する必要があります。

ClassSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$ClassSymbol

ClassMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$ClassMirror

MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol

MethodMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$MethodMirror

まずは、ClassSymbolをTypeから取得します。

val classSymbol = theType.typeSymbol.asClass

続いて、ClassMirrorを取得します。

val classMirror = runtimeMirror.reflectClass(classSymbol)

ここで、JavaMirrorが必要になります。

続いて、コンストラクタを表すMethodSymbolを取得します。

val constructorMethod = theType.member(universe.nme.CONSTRUCTOR).asMethod

そして、取得したMethodSymbolに対応するMethodMirrorを取得します。

val constructorMirror = classMirror.reflectConstructor(constructorMethod)

あとは、MethodMirror#applyを呼び出すことで、インスタンスを生成することができます。

val newInstance = constructorMirror("Reflect Instance")

引数も渡せます。

SymbolとMirrorがごっちゃになりやすい気もしますが、

  • ClassSymbol → ClassMirror
  • MethodMirror → MethodMirror

という形で対応付けられていて、あくまでメソッド呼び出しを指示するのはMirrorに対して、となります。

以降でも、扱う対象とMirrorについては適宜紐付けていきたいと思います。

フィールドの値を読み書きしたい

valやvarなフィールドの値を読み書きする方法について、書いていきます。

操作する対象のインスタンスは、前述のインスタンス生成で使用したものを使いますが、別に普通にnewして作成したものでもかまいません。

ここでは、以下のクラスを使用します。

InstanceMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$InstanceMirror

MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol

FieldMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$FieldMirror

まずは、親クラスのvalの値を読んでみましょう。

JavaMirrorから、InstanceMirrorを取得します。

val instanceMirror = runtimeMirror.reflect(newInstance)

valのgetterを取得します。getterの戻り値は、SymbolになっているのでasMethodでMethodSymbolにしてあげます。

val superValField =
  theType
    .member(universe.newTermName("superValField"))
    .asMethod
    .getter  // valなのでgetter
    .asMethod

取得したMethodSymbolを使って、InstanceMirrorからFieldMirrorを取得します。

val superValFieldMirror = instanceMirror.reflectField(superValField)

あとは、FieldMirror#getでvalの値を取得することができます。

require(superValFieldMirror.get == "super val field")

varの場合は、読み書きが可能なのでgetterとseterをそれぞれ別々に取得します。

val superVarField = theType.member(universe.newTermName("superVarField")).asMethod
val superVarGetField = superVarField.getter.asMethod  // varのgetter
val superVarSetField = superVarField.setter.asMethod  // varのsetter

続いて、それぞれのMethodSymbolに対してFieldMirrorを取得し

val superVarGetFieldMirror = instanceMirror.reflectField(superVarGetField)
val superVarSetFieldMirror = instanceMirror.reflectField(superVarSetField)

setterに対してFieldMirror#setで値を設定することができ、

superVarSetFieldMirror.set("super var field")

getter側のFieldMirror#getで値が取得できていることを確認できます。

require(superVarGetFieldMirror.get == "super var field")

もちろん、自分自身の持っているvalやvarに対しても、普通に読み書き可能です。

val valField = theType.member(universe.newTermName("valField")).asMethod.getter.asMethod
val valFieldMirror = instanceMirror.reflectField(valField)
require(valFieldMirror.get == "val field")

val varField = theType.member(universe.newTermName("varField")).asMethod
val varGetField = varField.getter.asMethod  // varのgetter
val varSetField = varField.setter.asMethod  // varのsetter
val varGetFieldMirror = instanceMirror.reflectField(varGetField)
val varSetFieldMirror = instanceMirror.reflectField(varSetField)
varSetFieldMirror.set("var field")
require(varGetFieldMirror.get == "var field")

というわけで、今回のインスタンスおよびSymbolとMirrorの関係は

となり、FieldMirrorのgetやsetでフィールドの読み書きを行います。InstanceMirrorはFieldMirrorの取得するための起点となっているだけですが、これがないとFieldMirrorが取得できません…。

メソッド呼び出しをしたい

フィールドの読み書きと近いことになりますが、今度はMethodMirorを使用します。

InstanceMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$InstanceMirror

MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol

MethodMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$MethodMirror

まずは、呼び出し対象のメソッドのMethodSymbolを取得します。

val superMethod = theType.member(universe.newTermName("superMethod")).asMethod

ここでは、親クラスのメソッドのMethodSymbolを取得しています。

続いて、MethodSymbolを使用してMethodMirorを取得します。ここでもInstanceMirrorが必要です。

val instanceMirror = runtimeMirror.reflect(newInstance)

val superMethodMirror = instanceMirror.reflectMethod(superMethod)

あとは、MethodMirror#applyでメソッド呼び出しを行うことができます。

require(superMethodMirror("World") == "Hello World!")

コンストラクタの時と同じく、引数も渡せます。

上記は親クラスの例でしたが、もちろん自分に定義されているメソッドも普通に呼べますので。

val method = theType.member(universe.newTermName("method")).asMethod
val methodMirror = instanceMirror.reflectMethod(method)
require(methodMirror("P1", "P2") == "SubClass[Reflect Instance]:method, params [P1, P2]")

今回のインスタンスおよびSymbolとMirrorの関係は

となり、MethodMirrorに対してメソッド呼び出し指示を行います。

SymbolとかとMirrorの関係が…?

Javaのリフレクションと違って、突然Mirrorが登場するので、よくわからないことになりそうだなーと思ってちょっとまとめてみます。

基本的に、操作したい対象についてのMirrorを取得し、操作自体はMirrorに命令すると覚えておけば大丈夫なのでは?

やりたいこと 最終的に指示するMirror 左記のMirrorの操作元となるもの 左記のMirrorの取得元Mirror
インスタンス生成 MethodMirror MethodSymbol ClassMirror
フィールドの読み書き FieldMirror MethodSymbol InstanceMirror
メソッド呼び出し MethodMirror MethodSymbol InstanceMirror

では、ClassMirrorとInstanceMirrorはどうやって取得するのかというと

欲しいMirror 取得に必要なもの 取得に必要なMirror
ClassMirror ClassSymbol ClassMirror
InstanceMirror 操作対象のインスタンス(Any) JavaMirror

で、JavaMirrorは

val runtimeMirror =
  universe.runtimeMirror(クラスローダー)

または

val runtimeMirror = universe.typeTag[何らかの型].mirror

で取得します。

ClassTagについて

上記までで基本的なインスタンス操作はできるようになりますが、そうするとメソッド呼び出しとかフィールドの読み書きをラップする、こんなメソッドとかを用意したくなると思います。

// メソッド呼び出し
def invokeMethod[T: universe.TypeTag, B](instance: T, methodName: String, args: Any*): B = {
  val typeTag = universe.typeTag[T]
  val theType = typeTag.tpe
  val runtimeMirror = typeTag.mirror

  val instanceMirror = runtimeMirror.reflect(instance)

  val methodSymbol = theType.member(universe.newTermName(methodName)).asMethod
  val methodMirror = instanceMirror.reflectMethod(methodSymbol)

  methodMirror(args: _*).asInstanceOf[B]
}

// フィールドの値取得
def getFieldValue[T: universe.TypeTag, B](instance: T, fieldName: String): B = {
  val typeTag = universe.typeTag[T]
  val theType = typeTag.tpe
  val runtimeMirror = typeTag.mirror

  val instanceMirror = runtimeMirror.reflect(instance)

  val getterSymbol = theType.member(universe.newTermName(fieldName)).asMethod.getter.asMethod
  val fieldMirror = instanceMirror.reflectField(getterSymbol)
  fieldMirror.get.asInstanceOf[B]
}

なんですけど、これをコンパイルすると

No ClassTag available for T
[error]   val instanceMirror = runtimeMirror.reflect(instance)

みたいに、ClassTagが使えないんだけど、と言われます。

これは、JavaMirror#reflectのImplicit ParameterにClassTagが定義されていて、上記のようなメソッド定義ではClassTagを取ってこれないからです。

ClassTag
http://www.scala-lang.org/api/current/index.html#scala.reflect.ClassTag

では、上記のinvokeMethodをコンパイル・実行可能なように修正してみます。

とりあえず、ClassTagをimportしましょう。

import scala.reflect.ClassTag

あとは、いくつか練習を兼ねてバリエーションを挙げてみたいと思います。

Context BoundでClassTagを加える

先ほどのメソッドの宣言を、こう変えます。

def invokeMethod[T: universe.TypeTag: ClassTag, B](instance: T, methodName: String, args: Any*): B = {
  〜省略〜
}

中身は、完全に同じです。これで、コンパイル、実行共に可能になります。

ClassTag#applyでClassTagを取得する

最初の実装で、ここまでは同じにします。Context Boundでは、TypeTagを指定したままで。

def invokeMethod[T: universe.TypeTag, B](instance: T, methodName: String, args: Any*): B = {
  val typeTag = universe.typeTag[T]
  val theType = typeTag.tpe
  val runtimeMirror = typeTag.mirror

次に、ClassTag#applyでClassTagを取得し、Implicit Parameterとします。

  implicit val classTag: ClassTag[T] = ClassTag(instance.getClass)

あとは一緒です。

つまり、こうなります。

def invokeMethod[T: universe.TypeTag, B](instance: T, methodName: String, args: Any*): B = {
  val typeTag = universe.typeTag[T]
  val theType = typeTag.tpe
  val runtimeMirror = typeTag.mirror

  implicit val classTag: ClassTag[T] = ClassTag(instance.getClass)

  val instanceMirror = runtimeMirror.reflect(instance)

  val methodSymbol = theType.member(universe.newTermName(methodName)).asMethod
  val methodMirror = instanceMirror.reflectMethod(methodSymbol)

  methodMirror(args: _*).asInstanceOf[B]
}
Context Boundの対象を、ClassTagにしてみる

メソッドの宣言で、Context Boundの書いているところを以下のようにします。

def invokeMethod[T: ClassTag, B](instance: T, methodName: String, args: Any*): B = {

TypeTag、無視(笑)。

で、ClassTagをimplicitlyで取得します。

  val classTag = implicitly[ClassTag[T]]

ClassTagが取れると、runtimeClassでClassクラスも取得が可能です。

  val clazz = classTag.runtimeClass

Classが取れれば、JavaMirrorが取れます。

  val runtimeMirror = universe.runtimeMirror(clazz.getClassLoader)

JavaMirrorが取れれば、ClassクラスからTypeに変換できます。

  val theType = runtimeMirror.classSymbol(clazz).typeSignature

あとは、最初の例と同じですね。

こういう結果になります。

def invokeMethod[T: ClassTag, B](instance: T, methodName: String, args: Any*): B = {
  val classTag = implicitly[ClassTag[T]]

  val clazz = classTag.runtimeClass

  val runtimeMirror = universe.runtimeMirror(clazz.getClassLoader)
  val theType = runtimeMirror.classSymbol(clazz).typeSignature

  val instanceMirror = runtimeMirror.reflect(instance)

  val methodSymbol = theType.member(universe.newTermName(methodName)).asMethod
  val methodMirror = instanceMirror.reflectMethod(methodSymbol)

  methodMirror(args: _*).asInstanceOf[B]
}

以上、ScalaのReflectionまとめでした〜。