呼び出しコストが大きい関数(以下、重たい関数)は、
呼び出さずに済むなら呼び出さないようにしたいし、
呼び出さなくてはならないなら呼び出し回数を最小限にしたいものです。
非 suspend 関数の場合
重たい関数が suspend 関数ではない普通の関数であれば、Lazy
委譲プロパティ(delegated property)を使うことで簡単にそのようにできます。
Lazy
委譲プロパティは lazy
関数を使うことで生成できます。
// 「呼び出しコストが大きい」関数。
// 今回の呼び出しを除いた呼び出し回数を出力し、
// 今回の呼び出しを含めた呼び出し回数を返す。
fun myFunction(): Int {
println("old count: $myCount")
// ...
myCount += 1
return myCount
}
private var myCount = 0
fun main() {
// Lazy 委譲プロパティ
val myLazyProperty by lazy {
myFunction()
} // 何も出力しない。
myLazyProperty // > old count: 0
.also { println("new count: $it") } // > new count: 1
myLazyProperty // 何も出力しない。
.also { println("new count: $it") } // > new count: 1
}
重たい関数が呼ばれるのは Lazy
委譲プロパティを使ったプロパティ(以下、lazy プロパティ)(上の例では myLazyProperty
プロパティ)が初めて参照されたときだけで、
以降の参照では同じ値が返されます。
suspend 関数の場合
では、重たい関数が suspend 関数だった場合はどうでしょう。
// 「呼び出しコストが大きい」suspend 関数。
// 今回の呼び出しを除いた呼び出し回数を出力し、
// 今回の呼び出しを含めた呼び出し回数を返す。
suspend fun mySuspendFunction(): Int {
println("old count: $myCount")
// ...
myCount += 1
return myCount
}
private var myCount = 0
直接呼び出すことはできない
lazy
関数のブロック内では suspend 関数を直接呼び出すことはできません。
val myLazyProperty: Int by lazy {
mySuspendFunction() // コンパイルエラー!
}
runBlocking
はスレッドをブロックする
runBlocking
関数を使うのはどうでしょう。
val myLazyProperty: Int by lazy {
runBlocking {
mySuspendFunction()
}
}
fun main() {
println("new count: $myLazyProperty") // スレッドがブロックされてしまう!
}
コンパイルはできるようになります。
問題なく動く場合もあるでしょう。
しかし lazy プロパティを最初に呼び出したスレッドが、重たい関数が処理を返すまでブロックされてしまいます。
それが UI スレッドであれば、その間アプリがフリーズすることになります。
また、重たい関数の処理に使用されるスレッドと lazy プロパティを呼び出したスレッドが同じであれば、デッドロックになってしまいます。
async
関数と Deferred
型を使うとよい
次の例のように、プロパティの返値型を Deferred
型 にして、lazy
関数のブロック内で async
関数を使って重たい関数の呼び出しを囲いましょう。
プロパティの呼び出し側では、Deferred
オブジェクトから本来の値を取り出すため、await
関数を呼び出します。
await
関数は suspend 関数なので、コルーチンの中で呼び出す必要があります。
// Lazy 委譲プロパティを使ったプロパティ
val myLazyProperty: Deferred<Int> by lazy {
CoroutineScope(Dispatchers.Default) // 注意! 状況に応じて適切な CoroutineScope を使うこと!
.async {
mySuspendFunction()
}
}
fun main() {
runBlocking {
// ^ suspend 関数である await 関数を呼び出すため、コルーチンビルダーで囲う必要があるが、
// ^ launch 関数だと呼び出し元が即座に処理を返してメインスレッドが終了してしまうため、
// ^ runBlocking 関数を使用する。
println("new count: ${myLazyProperty.await()}")
// ^ > old count: 0
// ^ > new count: 1
println("new count: ${myLazyProperty.await()}")
// ^ > new count: 1
}
}
async
関数は呼び出されると即座に Deferred
オブジェクトを返します。
Deferred
オブジェクトは結果の引換券のようなものです。
await
関数を呼び出すと、async
関数のブロックでの処理が終わって結果を得られるまで、呼び出し元コルーチンが中断されます。
1つの Deferred
オブジェクトに対して await()
を複数回呼び出しても、async
ブロックは一度しか実行されず、同じ結果が返ります。
async
ブロックの処理が終わってから await()
を呼び出すと、即座に結果が返ります。
CoroutineScope
に注意
上の例では CoroutineScope(Dispatchers.Default)
を使用しましたが、状況に応じて適切な CoroutineScope
オブジェクトを使用するようにしてください。
lazy プロパティをメンバに持つオブジェクトが破棄されても、CoroutineScope
オブジェクトがキャンセルされていないと、async
ブロックが無駄に処理を続けてしまうことがあるかもしれません。
逆に async
ブロックが処理を完了する前に CoroutineScope
がキャンセルされてしまうと、await()
を呼び出した際に CancellationException
がスローされることになります。
改良版(2023-03-22 追記)
次の記事で使いやすく改良しました。
/以上