Kotlinの型周りのちょっとした話

この記事は Aizu Advent Calendar 2021 - Adventar20日目の記事です

adventar.org

特にネタがなかったので実際のプロダクトコードで罠にハマった話をします
記事にあるコードはすべてKotlinです

型パラメータ

Kotlinだとおなじみのスコープ関数の let を例に上げてみます

public inline fun <T, R> T.let(f: (T) -> R): R = f(this)

このとき T, R が型パラメータにあたり、コンパイル時に実際の型が当てはまります

またこのとき、 T, R に上限境界がないため、任意な型になりえます
KotlinのNullableを型パラメータ上で表現したい場合は、上限境界を Any にしておきましょう

fun <T> functionA(value: T): String? {
    return value?.toString()
}

fun <T : Any> functionB(value: T?): String? {
    return value?.toString()
}

型消去とreified

上記の型パラメータは型消去があるため実行時には基本的に参照することができません
一応Kotlinには reified があるため実行時に参照することが可能です

inline fun <reified T> function(value: T): Boolean {
    return value is T
}

reified を使うと以下のことができます

  • is, asなどの型チェック、キャスト
  • ::class, ::class.javaなどのリフレクション、型クラス取得
  • 別の関数への型パラメータとしての利用

注意点として inline fun なのでインライン展開されるのを意識してください
(例えばJavaから呼び出しとかはできないです)

罠にハマった話

ここがこの記事を書こうと思った本題です サンプルコードなので命名や実装は適当です🙏

open class C()

open class D() : C()

open class A<T : C>(vararg value: T) {
    val clazz = value::class.java.componentType
}

class B<T : C>() : A<T>()

fun main() {
    val a = A<D>()
    val b = B<D>()
    
    println(a.clazz) // class D
    println(b.clazz) // class C
}

脳死で書いていたら無事罠にかかりまして、b.clazz はDではなくCなんですよね
reified を使って回避しようにも、コンストラクタに使えないのでダメですね
色々解決策はありましたが、結構コード自体闇が深くなりそうなので結果的には全く別の実装をしました
戒めとして記事に残します