はじめに
今更ながら、Kotlinの学習を始めました。
長らくJavaメインでやってましたが、Kotlinだと同じコードがスッキリ書けて、とても気持ち良いですね!
今回は、そんな気持ち良いポイントの一つuse
の挙動が気になったので、試してみました。
useって?
Closeableなオブジェクト.use
と書くことで、使い終わったら自動的にクローズしてくれます。
例えば、こんな感じで使用します。
// ファイルを取得する
val file = File("〜")
// ファイルに出力する
FileWriter(file).use {
it.write("文字列")
}
気になったポイント
Kotlinの気持ち良くないポイントの「非ローカルリターン」でも、ちゃんとクローズしてくれるのか?
例えば、初心者のハマりポイントのこんなコード...
fun method(list: List<String>) {
val notEmptyList: MutableList<String> = mutableListOf()
list.forEach {
if (it.isEmpty()) {
return // list.forEachではなく、methodからリターンしてしまう
} else {
notEmptyList += "$it is not empty"
}
}
// notEmptyListを利用した処理
}
このように、Kotlinはreturn
が特徴的で、return@関数名
といった感じでリターンする関数を指定することもできます。
これをuse
と併用しても、ちゃんとクローズしてくれるのか試してみました。
検証
Closeableなオブジェクト
以下の通り、適当なCloseableオブジェクトを定義します。
class TestCloseable(private val name: String) : Closeable {
init {
println("$name: init")
}
fun success() {
}
fun error() {
throw Exception()
}
override fun close() {
println("$name: close")
}
}
検証ケース
TestCloseable
を生成して使うだけ。
fun test1() {
TestCloseable("Test 1").use {
it.success()
}
}
同じくTestCloseable
を生成して使うだけだが、例外が発生する。
fun test2() {
TestCloseable("Test 2").use {
it.error()
}
}
ラムダ式内でTestCloseable
を生成して、上位の関数をリターンする。
fun test3() {
listOf(1, 2, 3).forEach { i: Int ->
TestCloseable("Test 3-$i").use { testCloseable: TestCloseable ->
if (i >= 2) return@forEach
testCloseable.success()
}
}
}
ラムダ式内でTestCloseable
を生成して、メソッド自体からリターンする。
fun test4() {
listOf(1, 2, 3).forEach { i: Int ->
TestCloseable("Test 4-$i").use { testCloseable: TestCloseable ->
if (i >= 2) return@test4
testCloseable.success()
}
}
}
検証結果
以下の通り、いずれのケースでもきちんとクローズしてくれている。
クローズ漏れのリスクなく、安全に使えることがわかった。
test1
Test 1: init
Test 1: close
test2
Test 2: init
Test 2: close
test3
Test 3-1: init
Test 3-1: close
Test 3-2: init
Test 3-2: close
Test 3-3: init
Test 3-3: close
test4
Test 4-1: init
Test 4-1: close
Test 4-2: init
Test 4-2: close
実装を覗いてみる
そもそもuse
の実装がどうなっているのか、見てみた。
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
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
}
}
}
}
internal fun Closeable?.closeFinally(cause: Throwable?) = when {
this == null -> {}
cause == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}
ポイントは以下の通り。
-
Closeable
を継承したオブジェクトをラムダ式block
の引数にして、実行する - 例外を捕捉した場合、退避しておき呼び出し元に伝播する
-
finally
処理で、- 自インスタンス(
Closeable
継承オブジェクト)がnull
なら何もしない - 異常終了(退避した例外がある)なら、抑制例外に追加して
close
- 正常終了(退避した例外がない)なら、単純に
close
- 自インスタンス(
提案
複数のCloseableオブジェクトを使用して、同じことをしたい場合がある。
元の実装をまねて、以下のような関数を定義すると良いかもしれない。
inline fun <T1 : Closeable?, T2 : Closeable?, R> Pair<T1, T2>.use(block: (T1, T2) -> R): R {
var exception: Throwable? = null
try {
return block(this.first, this.second)
} catch (e: Throwable) {
exception = e
throw e
} finally {
if (this.second != null) this.second.closeFinally(exception)
if (this.first != null) this.first.closeFinally(exception)
}
}
fun Closeable?.closeFinally(cause: Throwable?) {
when {
this == null -> {}
cause == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}
}
こんな感じで利用する。
fun test5() {
val a = TestCloseable("Test 5-a")
val b = TestCloseable("Test 5-b")
Pair(a, b).use { a: TestCloseable, b: TestCloseable ->
a.success()
b.success()
}
}
上記はPair
を利用してるが、Triple
を利用すれば3つのCloseableオブジェクトで対応できる。
参考