0
1

More than 1 year has passed since last update.

KotlinのCloseableのuseの挙動が気になる

Last updated at Posted at 2022-07-23

はじめに

今更ながら、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)
        }
}

ポイントは以下の通り。

  1. Closeableを継承したオブジェクトをラムダ式blockの引数にして、実行する
  2. 例外を捕捉した場合、退避しておき呼び出し元に伝播する
  3. 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オブジェクトで対応できる。

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1