0
0

More than 3 years have passed since last update.

【Kotlin】リフレクションで、kotlin-reflect無しでデフォルト引数を用いて関数を呼び出す

Last updated at Posted at 2021-04-01

TL;DR

  • Kotlinのデフォルト引数付き関数はJavaのリフレクションからも呼び出せる = kotlin-reflect無しでも呼び出せる
  • ただし、kotlin-reflect無しで呼び出しのための情報を集めることは困難が伴うため、依然としてkotlin-reflectの削除は難しい

本文

デフォルト引数付き関数がどうコンパイルされるか

まず、デフォルト引数付き関数がどうコンパイルされるかを見ていきます。
例として、以下のようなデフォルト引数付きコンストラクタを取り上げます。

data class Clazz(val foo: String = "Hello World.")

このコンストラクタは以下のようにコンパイルされます。

デコンパイル結果からコンストラクタ部分の抜粋
   public Clazz(@NotNull String foo) {
      Intrinsics.checkNotNullParameter(foo, "foo");
      super();
      this.foo = foo;
   }

   // $FF: synthetic method
   public Clazz(String var1, int var2, DefaultConstructorMarker var3) {
      if ((var2 & 1) != 0) {
         var1 = "Hello World.";
      }

      this(var1);
   }

2番目に出ているコンストラクタでは、条件分岐でデフォルト引数が設定されていることが分かります。
ここで、引数は以下のような意味を持っています。

  1. コード上で設定した引数
  2. 引数が設定されていれば0、設定されていなければ1となるビットマスク
  3. デフォルト引数を利用することを示すマーカー(基本的にnullを渡せばOK)

実際に呼び出してみる

このデフォルト引数付きコンストラクタは、コード上からは呼び出せませんが、Javaのリフレクションからは呼び出すことができます。
実際に呼び出してみたコードが以下です。

import java.lang.reflect.Constructor

data class Clazz(val foo: String = "Hello World.")

fun main() {
    val clazz: Class<Clazz> = Clazz::class.java
    val constructor: Constructor<Clazz> = clazz.constructors.first { constructor ->
        // デフォルトコンストラクタの引数末尾のクラス名は必ずDefaultConstructorMarker
        val isDefaultConstructor = constructor.parameters.lastOrNull()
            ?.let { it.type.name == "kotlin.jvm.internal.DefaultConstructorMarker" }
            ?: false
        // 今回の引数サイズは1、マスクとDefaultConstructorMarkerで2引数、合計3引数になる
        val isCollectArgumentSize = constructor.parameters.size == 3

        isDefaultConstructor && isCollectArgumentSize
    } as Constructor<Clazz>

    println(constructor.newInstance("こんにちは。", 0, null))
    println(constructor.newInstance("こんにちは。", 1, null))
    println(constructor.newInstance("こんにちは。", 2, null))
}

このコードを呼び出すと以下のような実行結果となります。
適切なビットマスクを設定した場合、渡した引数は無視され、デフォルト引数が用いられていることが分かります。

呼び出し結果
Clazz(foo=こんにちは。)
Clazz(foo=Hello World.)
Clazz(foo=こんにちは。)

これがどう役立つか

Kotlinのリフレクションでデフォルト引数を用いた関数呼び出しを行う場合、kotlin-reflectモジュールを使ってcallByするのがほぼ唯一の方法です。
一方、kotlin-reflectモジュールは容量が重く、環境によっては扱いにくさが有ります。

今回紹介した方法を上手く使うことができれば、リフレクションを用いて関数呼び出しするツールからkotlin-reflectを削除することができるかもしれません。

ただし、実際にこれをやる場合、kotlin-reflect無しのKFunctionからはパラメータ情報が取得できない、Javaのリフレクション上のConstructor/Methodを取得できないといった事情が有るため、単純にkotlin-reflectを削除できるとはなりません。
まだ詰め切れていませんが、自分が分かっている限りではkapt + KotlinPoetみたいな感じで事前に処理できる場合には役に立つかもという程度です。

参考にさせて頂いた内容

下記コードの中で登場するTestClassに対して生成されるTestClassJsonAdapterfromJson関数にて、実際にこれを用いてデフォルト値を含む関数呼び出しを実装している様子が見られます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0