前回までは、クラスやトレイトなどの定義を取得するような、いわゆる解析、静的操作系の話題をまとめていましたが、今回はインスタンスの操作について書いていこうと思います。
要は、動的にインスタンスを生成したり、メソッド呼び出しをしたりといったところですね。
これまでに書いたエントリのまとめは、こちらです。
導入編
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の関係は
- インスタンス → InstanceMirror
- MethodSymbol → FieldMirror
となり、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の関係は
- インスタンス → InstanceMirror
- MethodSymbol → MethodMirror
となり、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まとめでした〜。