interprism's blog

インタープリズム株式会社の開発者ブログです。

KotlinでInline Constructorっぽいものをつくる

こんにちは、andoです。

KotlinはBetter Javaとしてとても使いやすく弊社の開発でも採用が増えています。 Javaから多くの改善が行われていますが、Javaとの相互運用のため制限となっている機能もあり、その一つに型消去があります。
型消去については詳しく書きませんが、Javaでは総称型のメソッド、コンストラクタの実行時に型パラメータの情報を取得することがでません。 Kotlinの場合、関数については具象化された型パラメータを用いることで実行時に型情報を取得できるのですが、コンストラクタについては具象化された型パラメータは使用できないため、型情報を取得できません。
今回はそのコンストラクタ(っぽい)で型情報を取得してみたというお話です。

型引数から情報を取得

関数編

O/Rマッパーにありそうなレコードを取得するselect()の定義を考えてみます。
テーブルの型や型制約については本題ではないので省略させてください。

できればこう書きたい

この記述で動作すればシンプルで良いのですが、型消去により関数の実行時に呼び出し側からTの型情報が渡されないためコンパイルエラーとなります。

fun <T> select(): Sequence<T> {
    println(T::class) // Cannot use 'T' as reified type parameter. Use a class instead.
    return TODO("not implemented")
}

fun main() {
    val xs = select<String>()
}

Javaっぽく書く

Javaでは引数にClassを渡すのが定石です。
関数の定義は冗長ですが、呼び出す側の型パラメータは型推論により省略できます。

fun <T> select(t: Class<T>): Sequence<T> {
    println(t)
    return TODO("not implemented")
}

fun main() {
    val xs = select(String::class.java)
}

具象化された型パラメータ

reifiedを使用します。reifiedを使用する場合はinlineも宣言する必要があります。
また、synthetic methodとなるためJavaから呼び出すことはできなくなります。

inline fun <reified T> select(): Sequence<T> {
    println(T::class)
    return TODO("not implemented")
}

fun main() {
    val xs = select<String>()
}

コンストラクタ編

O/Rマッパーにありそうなカラムの値を取得する委譲プロパティの定義を考えてみます。
getValueinlineにして引数と戻り値の型をreifiedとしています。

abstract class Entity

class PropertyDelegate {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

fun main() {
    val person = Person()
    val id = person.id
}

もう一つ型引数を追加すると、、、

getValueに型パラメータを追加してもレシーバと戻り値以外は型推論できずコンパイルエラーとなります。 そのため、型パラメータをコンストラクタに追加してみますが、関数の場合と同様にコンストラクタの実行時に呼び出し側からTの型情報が渡されないためコンパイルエラーとなります。

class PropertyDelegate<T> {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(T::class) // Cannot use 'T' as reified type parameter. Use a class instead.
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate<String>()
    val name: String by PropertyDelegate<String>()
}

具象化された型パラメータ

コンストラクタは具象化された型パラメータを扱えないため、いずれもコンパイルエラーとなります。

// inlineの宣言がありません。
class PropertyDelegate<reified T> { // Only type parameters of inline functions can be reified
    ...
}
// コンストラクタにinline修飾子は適用できません。
class PropertyDelegate<reified T> inline constructor() { // Modifier 'inline' is not applicable to 'constructor'
    ...
}
// inline classは記述できますが別の機能です。
inline class PropertyDelegate<reified T> { // Only type parameters of inline functions can be reified
    ...
}

Javaっぽく書くしかない?

動きますが、、、書きたくないでござる!

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate(String::class.java)
    val name: String by PropertyDelegate(String::class.java)
}

ファクトリ関数経由で簡潔に

Kotlinらしくなりました。

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

inline fun <reified T> delegate() = PropertyDelegate(T::class.java)

class Person : Entity() {
    val id: Int by delegate()
    val name: String by delegate()
}

ファクトリ関数の名前をクラス名と同じにする

大文字始まりの関数名はKotlinでは一般的ではないこと、また、クラス名と名前空間が異なるため、IDEリファクタリング機能でクラス名を変更しても関数名は変わらない点に注意が必要です。

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

inline fun <reified T> PropertyDelegate() = PropertyDelegate(T::class.java)

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

ファクトリ関数をコンパニオンオブジェクトに定義する

ファクトリ関数をコンパニオンオブジェクトに定義し、Invoke演算子オーバーロードすることで呼び出す際の関数名を無くします。

class PropertyDelegate<T>(val t: Class<T>) {

    companion object {
        inline operator fun <reified T> invoke() = PropertyDelegate(T::class.java)
    }

    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

最後に

なんとかコンストラクタっぽくなりましたが、実体はコンパニオンオブジェクトに対するinvoke演算子オーバーロードであることに留意してください。また、ほとんどのケースはファクトリ関数で十分なはずです。トリッキーなコードは可読性、保守性が低下しますので、使用するにはそれなりの理由、根拠が必要ではないでしょうか。

また、今回は型情報を渡すのにjava.lang.Classを使用しましたが、Javaとの相互運用を考慮しないのであれば KClassがより適切ではないかと思います。

PAGE TOP