Help us understand the problem. What is going on with this article?

【Kotlin】CompanionObjectの定義から取得したKFunctionをインスタンス無しで呼び出せるKFunctionのように振る舞わせる

TL;DR

  • インスタンスから取得したKFunctionは引数のみで呼び出せる
  • CompanionObjectから取得したKFunctionの場合、呼び出しには引数以外にCompanionObjectのインスタンスが必要1
  • KFunctionを継承したクラスでcall/callByメソッドをよしなにやることで、CompanionObjectから取得したKFunctionもインスタンスから取得した関数同様に扱うことができる

課題になったこと

CompanionObjectから取得したKFunctionの場合、呼び出しには引数以外にCompanionObjectのインスタンスが必要です。
ここで、「コンストラクタ、static関数、CompanionObjectに定義した関数の3種類から適切な関数を抽出し、実行する」プログラムを書いていた所、コンストラクタとstatic関数はそのまま呼び出せるのに、CompanionObjectに定義した関数は扱いが異なってしまうという問題が発生しました。

ラムダでラップしてやれば扱いは同様にできますが、ラップする手間や無駄な生成コストが嫌だったので、CompanionObjectの定義から取得したKFunctionをインスタンス無しで呼び出せるKFunctionのように振る舞わせる方法を探しました。

解決方法

以下の通り、KFunctionを継承したクラスでインスタンス関連の取り扱いを隠ぺいすることで、課題が解決できます。

import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.instanceParameter

class CompanionKFunction<T>(
    private val function: KFunction<T>, private val instance: Any
): KFunction<T> by function {
    private val instanceParam by lazy { mapOf(function.instanceParameter!! to instance) }

    override val parameters: List<KParameter> by lazy {
        function.parameters.filter { it.kind != KParameter.Kind.INSTANCE }
    }

    override fun call(vararg args: Any?): T = function.call(instance, *args)

    override fun callBy(args: Map<KParameter, Any?>): T = function.callBy(instanceParam + args)
}
使い方
val clazz: KClass<*> = // クラス取得
val companionObjectInstance: Any = clazz.companionObjectInstance!! // コンパニオンオブジェクト取得
val companionFunction: KFunction<*> = companionObjectInstance.functions.first { /* 特定関数の取得 */ }

// インスタンス無しで呼び出せるように隠ぺい
val companionKFunction = CompanionKFunction(companionFunction, companionObjectInstance)

val returnValue = companionKFunction.call(/* インスタンス以外の引数 */)

使ったテクニック

Class Delegation

以下のように、$インターフェース by $移譲先インスタンスとしてやることで、インターフェースの実装を他クラスに移譲できます。
これにより、最低限の手間でインターフェースを実装することができます。

class CompanionKFunction<T>(
    private val function: KFunction<T>, private val instance: Any
): KFunction<T> by function {

可変長引数の展開

可変長引数を別の関数の可変長引数に渡す場合、*(スプレッド演算子)で展開する必要が有ります。

override fun call(vararg args: Any?): T = function.call(instance, *args)

参考にさせて頂いた記事

宣伝

Kotlin向けModelMapper的なライブラリを作っています。
是非使ってみて下さい。


  1. Class::class.companionObject/.companionObjectInstanceで取得した場合の話。method referenceを取得する場合はインスタンス無しで呼び出せる。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした