はじめに
先日、Kotlin 1.4のプレビューバージョンであるKotlin 1.4-M1のリリースがありました。Kotlin 1.4-M1では型推論、Contracts、コンパイラーなどの機能改善がありましたが、その中でもContractsのアップデート情報を紹介したいと思います。
Kotlin Contractsとは
Kotlin 1.3からプログラムの正確性の向上を支援するためにExperimentalな機能としてContractsが導入されました。コンパイラに対して契約を宣言することで快適なコーディングの支援に繋がります。標準ライブラリでもいくつか利用されていて、例えばスコープ関数などでContractsが利用されています。
Kotlin 1.4-M1のContractsの動向
Kotlin 1.4-M1では、Contractsの機能改善といくつかの標準ライブラリにConstractsが利用されていますので、それぞれ紹介したいと思います。まず初めに具体化型パラメータへの参照を許可されました。対象のIssueはこちらです。
@OptIn(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?) {
contract {
returns() implies (value is T)
}
assertTrue(value is T)
}
上記の関数ではインスタンス型検証を行なっています。T型パラメータが具象化されているため、関数内でその型を確認することができ、Contractsを利用できるようになりました。上記の関数は今後、kotlin.testライブラリに追加されるようです。
次にfinalのメンバー関数にConstractsが利用可能となりました。対象のIssueはこちらです。また対象のPRを見てみると、下記のようにfinalのメンバー関数でContractsが利用できることがわかります。今まではトップレベル関数でしかContractsを利用できなかったことを考えると、大きなサポートと言えます。
open class Class {
fun member(x: Boolean) {
contract { returns() implies (x) }
}
inline fun inlineMember(x: Boolean) {
contract { returns() implies (x) }
}
abstract fun abstractMember(x: Boolean) {
<!CONTRACT_NOT_ALLOWED!>contract<!> { returns() implies (x) }
}
open fun openMemeber(x: Boolean) {
<!CONTRACT_NOT_ALLOWED!>contract<!> { returns() implies (x) }
}
suspend fun suspendMember(x: Boolean) {
contract { returns() implies (x) }
}
}
次にmeasureTimeMillis()、measureNanoTime()の標準ライブラリにもConstractsが利用されていますので、それぞれ内容を確認してみましょう。対象のIssueはこちらです。
public inline fun measureTimeMillis(block: () -> Unit): Long {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
}
val start = System.currentTimeMillis() |
block()
return System.currentTimeMillis() - start |
}
public inline fun measureNanoTime(block: () -> Unit): Long {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
}
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
measureTimeMillis()、measureNanoTime()は特定のコードブロックの実行時間をベンチマークするための関数です。Contractsが利用されていることがわかります。次のmeasureTimeMillis()を呼び出すコードではコンパイルが通るようになります。
val x: Int
measureTimeMillis {
x = 1 // コンパイルOK(Kotlin 1.4)
}
println(x) // コンパイルOK(Kotlin 1.4)
ですが、Contractsが利用されていないKotlin 1.3で実行してみるとコンパイルエラーになります。
val x: Int
measureTimeMillis {
x = 1 // コンパイルNG(Kotlin 1.3)
}
println(x) // コンパイルN(Kotlin 1.3)
なぜダメなのでしょうか。その理由ですが、measureTimeMillis()のラムダ内では、再代入できない変数xが再割り当ての可能性があるため、初期化が禁止されているためです。後続のprintln(x)の部分では、変数xが初期化されていないと認識されます。
measureTimeMillis()内のcontractのラムダ内を見てみると、callsInPlace(block, InvocationKind.EXACTLY_ONCE)
が定義されています。これは変数blockが一回だけ実行されることを定義しています。つまり、先ほどの変数xがmeasureTimeMillis()のラムダ内で一回しか実行されないため、変数xに1が初期化されて、後続処理である println(x)が1を出力する結果となります。
このようにしてmeasureTimeMillis()、measureNanoTime()にContractsが利用されました。では最後にuse()の標準ライブラリにもContractsが利用されているので見てみましょう。use()とはリソースの使用後にuse()を呼び出すことで、リソースの解放を行われてリソースの解放漏れを防ぐことができます。次のコードがuse()の実装内容で、対象のIssueはこちらです。
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) // Kotlin 1.4でContractsが利用された
}
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
this == null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}
以下のuse()の呼び出しコードでは、再代入不可の変数xをuse()のラムダ内で初期化しています。今まではuse()のラムダ内で変数xの初期化ができなかったのですが、contract内でcallsInPlace(block, InvocationKind.EXACTLY_ONCE)
を定義しているため、ラムダ内の処理が一回だけと定義しています。その結果、変数xの初期化が正常に行われて、println(x)の結果として1が出力されます。
val x: Int
"".byteInputStream().use {
x = 1 // コンパイルOK(Kotlin 1.4)
}
println(x) // コンパイルOK(Kotlin 1.4)
Kotlin 1.3ではuse()にContractsが利用されていないため、以下のようにコンパイルエラーになります。
val x: Int
"".byteInputStream().use {
x = 1 // コンパイルNG(Kotlin 1.3)
}
println(x) // コンパイルNG(Kotlin 1.3)
おわり
Kotlin 1.4 M1がリリースされましたので、Contractsのアップデート情報を紹介しました。ContractsはExperimentalな機能ですが、Kotlin 1.4のリリースがとても待ち遠しいですね。引き続き動向を追っていこうと思います。