3
3

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 に呼び出す《改良版》

Posted at

次の記事の改良版です。併せてお読みください。

元記事では Deferred 型の lazy プロパティを使用していました。
しかしそれだと次の2点が美しくありませんでした。

  • 拡張プロパティなのに型が本来の値の型ではなく Deferred になっている。
  • 本来の値を取得するのに await() の呼び出しが必要である。

そこで次のラッパークラスを作ることでこれを改善します。

/**
 * 初回 [invoke] 時のみ評価され、以降の [invoke] 時には同じ結果を返す、
 * suspend 関数オブジェクトクラス。
 *
 * @param R 返値の型。
 */
class LazySuspendFunction<R>(
    /** [initializer] を実行する際に使用されるコルーチンスコープ。 */
    private val scope: CoroutineScope,
    /**
     * 初期化関数。
     *
     * 初回 [invoke] 時のみ実行され、その結果がそのときおよびそれ以降の [invoke] の返値となる。
     */
    private val initializer: suspend () -> R,
) {
    private val deferred: Deferred<R> by lazy {
        scope.async {
            initializer()
        }
    }

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

これを使うと次のように、普通の suspend 関数のように扱うことができます。

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

    delay(1_000)

    myCount += 1
    return myCount
}
private var myCount = 0

val scope = CoroutineScope(Dispatchers.Default)

val myLazySuspendFunction =
    LazySuspendFunction(scope) {
        mySuspendFunction()
    } // 何も出力しない。

fun main() {
    runBlocking {
        myLazySuspendFunction() // > old count: 0
            .also { println("new count: $it") } // > new count: 1
        myLazySuspendFunction() // 何も出力しない。
            .also { println("new count: $it") } // > new count: 1
    }
}
出力
old count: 0
new count: 1
new count: 1

処理中にキャンセルした場合は次のようになります。

fun main() {
    runBlocking {
        launch {
            try {
                myLazySuspendFunction() // > old count: 0
                    .also { println("new count: $it") } // 出力されない。
            } catch (e: CancellationException) {
                println("Cancelled!")
                // ^ scope.cancel() が実行されたときに出力される。
                // ^ > Cancelled!
            }
        }

        // ↑の launch の処理が完了する前にキャンセルする。
        delay(500)
        scope.cancel()

        launch {
            try {
                myLazySuspendFunction() // 何も出力しない。
                    .also { println("new count: $it") } // 出力されない。
            } catch (e: CancellationException) {
                println("Cancelled!") // > Cancelled!
            }
        }
    }
}
出力
old count: 0
Cancelled!
Cancelled!

処理後にキャンセルした場合は次のようになります。

fun main() {
    runBlocking {
        launch {
            try {
                myLazySuspendFunction() // > old count: 0
                    .also { println("new count: $it") } // > new count: 1
            } catch (e: CancellationException) {
                println("Cancelled!") // 出力されない。
            }
        }

        // ↑の launch の処理が完了した後にキャンセルする。
        delay(1_500)
        scope.cancel()

        launch {
            try {
                myLazySuspendFunction() // 何も出力しない。
                    .also { println("new count: $it") } // > new count: 1
            } catch (e: CancellationException) {
                println("Cancelled!") // 出力されない。
            }
        }
    }
}
出力
old count: 0
new count: 1
new count: 1

なお、CoroutineScope の使用についての注意点は変わっていませんのでご注意ください。

/以上

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?