次の記事の別案。
元記事では LazySuspendFunction
に CoroutineScope
を持たせた。
しかしそうすると 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
}
}
/以上