Open7
【Kotlin】学習メモ(ラムダ式 / コルーチン)
ラムダ式について
- ラムダ式を使わずに関数を参照する
fun main() {
val trickFunc = ::trick // ::は関数を値として参照するための関数参照演算子
// ↑推論したval trickFuncの型は KFunction0<Unit>
trick()
trickFunc()
}
fun trick() {
println("this is trick func")
}
// 出力
// this is trick func
// this is trick func
- ↑をラムダ式で記述
fun main() {
val trickFunc = trick // trickは関数名ではなく変数を参照するように変化したので::を削除
// ↑推論したval trickFuncの型は () -> Unit
trick() // 関数のように実行もできる
trickFunc()
}
val trick = {
println("this is trick func")
}
// 省略しないで型を書くと
// val trick: () -> Unit = {
// 出力
// this is trick func
// this is trick func
関数の戻り値が関数の例
fun main() {
val func = trueOfFalse(true)
// funcの型は、() -> Unit
func()
}
// 戻り値の型が() -> Unit(関数)
fun trueOfFalse(isTrue: Boolean): () -> Unit {
if (isTrue) {
return trueFunc
} else {
return falseFunc
}
}
val trueFunc: () -> Unit = {
println("this is true func")
}
val falseFunc: () -> Unit = {
println("this is false func")
}
// 出力
// this is true func
関数を引数として別の関数に渡す
fun main() {
getFuncFunc(printNumFunc)
}
// 引数として(int) -> Unit型の関数を受け取る
fun getFuncFunc(func: (Int) -> Unit) {
println("this is getFuncFunc")
func(10)
}
// 引数としてInt型の値を受け取り、Unit型の値を返す関数を返す
val printNumFunc: (Int) -> Unit = { num ->
println("this is printNumFunc: $num")
}
// 出力
// this is getFuncFunc
// this is printNumFunc: 10
- 上記の例で、受け取る関数がnull許容である場合
// (型)?で囲む
fun getFuncFunc(func: ((Int) -> Unit)?) {
println("this is getFuncFunc")
if (func == null) {
println("func is null")
} else {
func(10)
}
}
- パラメータが一つである場合は、暗黙的にit名に割り当てられる。よって
パラメータ名 ->
を省略できる
val printNumFunc: (Int) -> Unit = {
println("this is printNumFunc: $it")
}
ラムダ式を関数に直接渡す、後置ラムダ構文
- 別の関数の引数に関数を渡す時に以下の方法がある
- ラムダ式を変数にいれて変数経由で渡す(そのラムダ式を複数回使う時とか)
- 変数を作らず、引数に直接ラムダ式を渡す(そのラムダ式を使うのが1回だけのとき)
fun main() {
getFuncFunc("AAA", { println("this is num: $it") } )
}
fun getFuncFunc(letter: String, func: (Int) -> Unit) {
println("this is getFuncFunc, letter is $letter")
func(10)
}
// 出力
// this is getFuncFunc, letter is AAA
// this is num: 10
- これはAndroidStudioでは注意される
- ラムダ式が渡されるのが最後のパラメータの場合は、省略形を利用することが可能
// 省略前
getFuncFunc("AAA", { println("this is num: $it") } )
// 省略後
getFuncFunc("AAA") { println("this is num: $it") }
コルーチンについて
- Kotlinのコルーチンは、構造化された同時実行というコンセプトに従っている
- 同時実行を明示的に要求しない限り(launch() を使用するなど)、コードはデフォルトで順次処理され、基となるイベントループと連携します。
runBlockingで直接suspend関数を呼ぶ
import kotlinx.coroutines.*
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
printForecast()
printTemperature()
println("[end] runBlocking")
}
println("[end] main")
}
suspend fun printForecast() {
delay(3000)
println("Sunny")
}
suspend fun printTemperature() {
delay(3000)
println("30\u00b0C")
}
出力
- suspend関数は順次呼び出される。
- 同時実行を行うためには、明示的にそう指示する必要がある→次の
launch
へ。
- 同時実行を行うためには、明示的にそう指示する必要がある→次の
[start] main
[start] runBlocking ← 実行した瞬間ここまで表示
Sunny ← これが表示されるまで3秒待つ
30°C ← これが表示されるまでさらに3秒待つ。ここから最後までが一気に表示される
[end] runBlocking
[end] main
-
launch
が、Fire and Forget(撃ちっぱなし)...新しいコルーチンを起動し、処理がいつ終了するのか気にしない。
launchで同時実行
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
launch {
printForecast()
}
launch {
printTemperature()
}
println("[end] runBlocking")
}
println("[end] main")
}
// printForecast等は略
出力
実行時間は体感4秒
[start] main
[start] runBlocking
[end] runBlocking ← 実行した瞬間ここまで表示
Sunny ← 3秒まって、ここから最後までが一気に表示される
30°C
[end] main
async()
-
launch()
は撃ちっぱなしなので、実行の終了タイミングを待てない。 - 終了タイミングを待ち、コルーチンからの戻り値が必要な場合は
async()
を利用する -
async()
関数は、Deferred
型のオブジェクトが変える。これは、準備ができたらそこに結果が入る約束のようなもの。 -
await()
を使用してDeferred
オブジェクトの結果にアクセスできる
async, awaitを利用したコード
import kotlinx.coroutines.*
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
val forecast: Deferred<String> = async {
getForecast()
}
val temperature: Deferred<String> = async {
getTemperature()
}
println("before await")
println("${forecast.await()}, ${temperature.await()}")
println("[end] runBlocking")
}
println("[end] main")
}
suspend fun getForecast(): String {
delay(3000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(3000)
return "30\u00b0C"
}
出力
-
getForecast
とgetTemperature
の2つの関数は同時実行されている(体感4秒くらいなので)
[start] main
[start] runBlocking
before await ← ここまで一気に表示される
Sunny, 30°C ← 3秒まって、ここから最後までが一気に表示される
[end] runBlocking
[end] main
coroutineScopeで複数の同時実行オペレーションを一つにまとめる
-
coroutineScope()
は、起動したコルーチンを含む全ての処理が完了した場合に返される。 - 複数の同時実行オペレーションを単一の動機オペレーションにまとめることができる
コード
package com.example.kotlinlearning
import kotlinx.coroutines.*
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
println(getWeatherReport())
println("[end] runBlocking")
}
println("[end] main")
}
suspend fun getWeatherReport() = coroutineScope {
println("[start] getWeatherReport")
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
println("before await")
"${forecast.await()}, ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(3000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(3000)
return "30\u00b0C"
}
出力
[start] main
[start] runBlocking
[start] getWeatherReport
before await ← ここまで一気に表示される
Sunny, 30°C ← 3秒まって、ここから最後までが一気に表示される
[end] runBlocking
[end] main
coroutineScopeの戻り値について
- 最後に評価される式がStringであれば戻り値はString
- UnitであればUnitとなる
- なお、Unitが戻り値の場合の
println(getWeatherReport())
はkotlin.Unit
となる
- なお、Unitが戻り値の場合の
コルーチンの例外処理
- 子階層のコルーチンが例外を発生させると、親コルーチンに伝播し、親コルーチンがキャンセルされる。
- 親コルーチンがカンセルされたことで、他の子コルーチンがキャンセルされる
- 最後にエラーが上方に天破されて、プログラムがクラッシュする(Exit code 1)
例外を発生させるコード
// これ以外の全てのコードは一つ上のcoroutineScopeと同じ
suspend fun getTemperature(): String {
delay(1000) // getForecastの3000msよりも短いので先に例外が発生する
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
出力
[start] main
[start] runBlocking
[start] getWeatherReport
before await ← ここまで一気に表示される ↓エラーは1秒後に表示される
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
at com.example.kotlinlearning.ProgramKt.getTemperature(Program.kt:32)
at com.example.kotlinlearning.ProgramKt$getTemperature$1.invokeSuspend(Program.kt)
at ...略
Process finished with exit code 1
try catchで例外を捕捉するコード
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
try {
println(getWeatherReport())
} catch (e: AssertionError) {
println("Catch AssertionError: ${e.message}")
}
println("[end] runBlocking")
}
println("[end] main")
}
出力
[start] main
[start] runBlocking
[start] getWeatherReport
before await ← ここまで一気に表示される
Catch AssertionError: Temperature is invalid ← 1秒でここが表示され、したまで一気に出力される
[end] runBlocking
[end] main
Process finished with exit code 0
try...catchで例外捕捉②、正常に生きるコルーチンは活かす方法
fun main() {
println("[start] main")
runBlocking {
println("[start] runBlocking")
println(getWeatherReport())
println("[end] runBlocking")
}
println("[end] main")
}
suspend fun getWeatherReport() = coroutineScope {
println("[start] getWeatherReport")
val forecast = async { getForecast() }
val temperature = async {
try {
getTemperature()
} catch (e: AssertionError) {
println("Caught AssertionError $e")
"Temperature is unknown"
}
}
println("before await")
"${forecast.await()}, ${temperature.await()}"
}
出力
[start] main
[start] runBlocking
[start] getWeatherReport
before await ← ここまで一瞬
Caught AssertionError java.lang.AssertionError: Temperature is invalid ← これが1000ms後
Sunny, Temperature is unknown ← ここから最後までが3000ms秒後に表示される
[end] runBlocking
[end] main
コルーチンのキャンセル処理
- asyncで戻されるDeferred型のインスタンスに対して
cancel()
をただ呼べばよいだけ
コード
suspend fun getWeatherReport() = coroutineScope {
println("[start] getWeatherReport")
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
delay(2000)
temperature.cancel()
// "${forecast.await()}, ${temperature.await()}"
"${forecast.await()}"
}
出力
[start] main
[start] runBlocking
[start] getWeatherReport ← ここまで一気に出力
Sunny ← 3000msからこれ以下が一気に出力
[end] runBlocking
[end] main
コルーチンの概念について
-
launch()
の戻り値の型はJob
である。 -
async() 関数で開始されたコルーチンから返されるオブジェクト Deferred は Job でもあり、コルーチンの今後の結果を保持します。
fun main() {
runBlocking {
var job = launch { printForecast() }
job.cancel() // キャンセル可能
}
}
suspend fun printForecast() {
delay(3000)
println("Sunny")
}
- Jobは親子構造を持つ。その際の重要な概念↓
親ジョブがキャンセルされると、その子ジョブもキャンセルされます。
job.cancel() で子ジョブがキャンセルされると、子ジョブは終了しますが、親ジョブはキャンセルされません。
ジョブが例外で失敗した場合、その例外で親がキャンセルされます。これを、エラーの上方伝播(親、親の親など)といいます。
スレッドの確認
通常のlaunch
fun main() {
println("${Thread.currentThread().name} - [start] main")
runBlocking {
println("${Thread.currentThread().name} - [start] runBlocking")
launch {
println("${Thread.currentThread().name} - [start] launch")
delay(3000)
println("10 result found.")
}
println("${Thread.currentThread().name} - [after] launch")
println("Loading...")
}
}
出力
main - [start] main
main - [start] runBlocking
main - [after] launch
Loading...
main - [start] launch ← ここまですぐ
10 result found. ← 3000ms後
withContextでDispacherを指定
- withContextのブロックを抜けると、再びmainに戻る
fun main() {
println("${Thread.currentThread().name} - [start] main")
runBlocking {
println("${Thread.currentThread().name} - [start] runBlocking")
launch {
println("${Thread.currentThread().name} - [start] launch")
withContext(Dispatchers.Default)
{
println("${Thread.currentThread().name} - [start] withContext")
delay(3000)
println("10 result found.")
}
println("${Thread.currentThread().name} - [end] launch")
}
println("${Thread.currentThread().name} - [after] launch")
println("Loading...")
}
}
出力
main - [start] main
main - [start] runBlocking
main - [after] launch
Loading...
main - [start] launch
DefaultDispatcher-worker-1 - [start] withContext ←ここまで一気に
10 result found. ←ここから最後までが3000ms後
main - [end] launch
asyncの呼び出しでも以下のようにcontextを切替可能
val temperature: Deferred<String> = async {
withContext(Dispatchers.Default) {
getTemperature()
}
}