Open7

【Kotlin】学習メモ(ラムダ式 / コルーチン)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

ラムダ式について

https://developer.android.com/codelabs/basic-android-kotlin-compose-function-types-and-lambda?hl=ja&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-2-pathway-1%3Fhl%3Dja%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-function-types-and-lambda#0

  • ラムダ式を使わずに関数を参照する
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") }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

コルーチンについて

https://developer.android.com/codelabs/basic-android-kotlin-compose-coroutines-kotlin-playground?hl=ja&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-5-pathway-1%3Fhl%3Dja%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-coroutines-kotlin-playground

  • 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

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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"
}

出力

  • getForecastgetTemperatureの2つの関数は同時実行されている(体感4秒くらいなので)
[start] main
[start] runBlocking
before await ← ここまで一気に表示される
Sunny, 30°C  ← 3秒まって、ここから最後までが一気に表示される
[end] runBlocking
[end] main
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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となる
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

コルーチンの例外処理

  • 子階層のコルーチンが例外を発生させると、親コルーチンに伝播し、親コルーチンがキャンセルされる。
    • 親コルーチンがカンセルされたことで、他の子コルーチンがキャンセルされる
    • 最後にエラーが上方に天破されて、プログラムがクラッシュする(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
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

コルーチンのキャンセル処理

  • 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
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

コルーチンの概念について

  • 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()
            }
        }