0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Kotlin】suspend 関数を lazy に呼び出す《改良版:別案》

Last updated at Posted at 2023-04-23

次の記事の別案。

元記事では LazySuspendFunctionCoroutineScope を持たせた。
しかしそうすると CoroutineScope の生存期間を考慮しなければならなくなりちょっと面倒だ。
そこで CoroutineScope を持たせないバージョンを作成する。

まず準備として、
未初期化の状態を表せるように次の型を用意する。

/**
 * 未初期化/初期化済みのいずれかの具象型のみを持つシールドクラス。
 */
sealed interface Initializable<out T> {
    /** 未初期化であることを表す、[Initializable] インターフェイスのオブジェクト。 */
    object Uninitialized : Initializable<Nothing>

    /** 初期化済みの値を持つ、[Initializable] インターフェイスを実装した具象クラス。 */
    data class Initialized<T>(
        /** 初期化された値。 */
        val value: T
    ) : Initializable<T>
}

LazySuspendFunction クラスは次のようになる。


/**
 * 初回 [invoke] 時のみ評価され、以降の [invoke] 時には同じ結果を返す、
 * suspend 関数オブジェクトクラス。
 *
 * @param R 返値の型
 */
class LazySuspendFunction<R>(
    /**
     * 初期化関数。
     *
     * 初回 [invoke] 時に評価され、その結果がそのときおよび以降の [invoke] の返値となる。
     */
    private val initializer: suspend () -> R
) {
    @Volatile
    private var initializable: Initializable<R> = Initializable.Uninitialized

    private val mutex: Mutex = Mutex()

    /**
     * [initializer] を評価した結果を返す。
     *
     * 評価は初回呼び出し時のみ行い、
     * それ以降の呼び出しでは同じ値を返す。
     */
    suspend operator fun invoke(): R =
        ensureInitialized().value

    private suspend fun ensureInitialized(): Initializable.Initialized<R> {
        // ダブルチェックロッキング
        initializable.also {
            if (it is Initializable.Initialized<R>) {
                return it
            }
        }

        mutex.withLock {
            // ダブルチェックロッキング
            initializable.also {
                if (it is Initializable.Initialized<R>) {
                    return it
                }
            }

            return initializer()
                .let { Initializable.Initialized(it) }
                .also {
                    initializable = it
                }
        }
    }
}

元記事の方では、LazySuspendFunction.invoke() で初期化中に呼び出し元がキャンセルされても初期化は続く。しかし初期化中に CoroutineScope がキャンセルされると「キャンセルされた」という状態で初期化され、常に CancellatioException をスローするようになる。
この記事の方では、初期化中に呼び出し元がキャンセルされると、初期化が行われていない状態になり、次の呼び出しの際に再度初期化が行われる。

動作確認。


// 「呼び出しコストが大きい」suspend 関数。
// 今回の呼び出しを除いた呼び出し回数を出力し、
// 今回の呼び出しを含めた呼び出し回数を返す。
suspend fun mySuspendFunction(): Int {
    try {
        println("old count: $myCount")

        // ...
        delay(1_000)

        myCount += 1
        return myCount
    } catch (e: CancellationException) {
        println("Cancelled!")
        throw e
    }
}

private var myCount = 0


fun main() {
    // 遅延評価される suspend 関数。
    // この宣言はコルーチン外で行うことができる。
    val myLazySuspendFunction =
        LazySuspendFunction {
            mySuspendFunction()
        } // 何も出力しない。

    runBlocking {
        // ^ suspend 関数を呼び出すことからコルーチンビルダーで囲う必要があるが、
        // ^ launch 関数だと呼び出し元が即座に処理を返してメインスレッドが終了してしまうため、
        // ^ runBlocking 関数を使用する。

        val job = launch {
            myLazySuspendFunction() // > old count: 0
                .also { println("new count: $it") } // > new count: 1
        }
        delay(500)
        job.cancel() // > Canceled!

        myLazySuspendFunction() // > old count: 0
            .also { println("new count: $it") } // > new count: 1
    }
}

/以上

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?