前書き
Kotlin 1.7
まででは、引数がデフォルト引数か否かを判定する方法が公式には提供されていません。
引数がオブジェクト型のみという制約を付けられるなら、以下の記事の方法で対応可能ですが、この方法は今後利用できなくなる可能性があります。
そこで、この記事ではどれだけコードが汚くなってでも引数がデフォルト引数か否かを判定する方法を考えてみます。
この方法はリフレクション等の魔法を用いずに実現可能な範囲で、プリミティブ型にも対応しています。
ただし、かなり非現実的なコードになっています。
サンプルコード
以下のSample
クラスでは、コンストラクタに引数を指定したか否かをfooSpecified
/barSpecified
に格納しています。
class Sample(
ctxt: DefaultProvider = DefaultProviderImpl(),
val foo: Int = ctxt.fooDefault,
val bar: String? = ctxt.barDefault
) {
sealed interface DefaultProvider {
val fooDefault: Int
val barDefault: String?
}
private class DefaultProviderImpl : DefaultProvider {
var fooSpecified: Boolean = true
private set
override val fooDefault: Int get() {
fooSpecified = false
return -1
}
var barSpecified: Boolean = true
private set
override val barDefault: String? get() {
barSpecified = false
return null
}
}
val fooSpecified: Boolean
val barSpecified: Boolean
init {
val ctxtImpl = ctxt as DefaultProviderImpl
fooSpecified = ctxtImpl.fooSpecified
barSpecified = ctxtImpl.barSpecified
}
}
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@Test
fun 引数指定無し() {
Sample().apply {
assertFalse(fooSpecified)
assertFalse(barSpecified)
}
}
@Test
fun 引数指定あり() {
Sample(foo = 1, bar = "2").apply {
assertTrue(fooSpecified)
assertTrue(barSpecified)
}
}
解説
Kotlin
では、デフォルト引数に別のデフォルト引数を用いた値を設定できます。
サンプルコードではこれを利用して、DefaultProviderImpl
にデフォルト引数の利用状況を記録しています。
考察
まず第一の残念ポイントは、コードが非常に複雑になってしまうことです。
手動で一々こんなものを書いていられません。
第二の残念ポイントは、DefaultProvider
が必ず第一引数に来てしまうため、利用側が実質的に名前付き引数の利用を強制されてしまう点です。
Kotlin 1.7
現在、デフォルト引数同士の参照は順番の制約が有るため、どうしてもこうなってしまいます。
これが無ければ、使う側が何か意識する必要が無くなったんですが……。
ということで、コード生成するならワンチャン無い位の仕上がりとなりました。
強大な黒魔術に手を染めれば、使い手的にスマートな見栄えは実現できなくもない気がしますが、流石にそこまでやる気にならなかったのでここまでとします。