次の記事の改良版です。併せてお読みください。
元記事では 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
の使用についての注意点は変わっていませんのでご注意ください。
/以上