mtkw.dev

Kotlinのinline/noinline/crosslineについて勉強する

inlineってなんだっけ?

inline関数は呼び出し元に処理内容を展開します。

fun main() {
    val name = "Taro"
    execute {
        println("my name is $name")
    }
}
 
fun execute(block: () -> Unit) {
    block()
}

このようなコードがあったとき、execute関数は引数のラムダをインスタンスとして受け取り実行します。またラムダはnameをキャプチャしています。

// 展開前
fun main() {
    val name = "Taro"
    execute {
        println("my name is $name")
    }
}
 
// 展開後のイメージ
fun main() {
    val name = "Taro"
    println("my name is $name")
}
 
inline fun execute(block: () -> Unit) {
    block()
}

executeをinline関数として定義すると、execute関数の引数のラムダのインスタンスは作成せず、インスタンスの実行も処理元に展開します。

これにより、以下が可能になります。

  1. パフォーマンス向上(高階関数でのインスタンス作成とinvokeのコスト削減)
  2. non-local returnを可能にする

non-local returnって?

inline関数にすることで引数のラムダも呼び出し元に展開できることがわかりました。つまり、returnの意味がinline関数と、非inline関数で変わってきます。

fun main() {
    val name = "Taro"
    execute {
        println("my name is $name")
        return // 'return' is prohibited here.
    }
}
 
fun execute(block: () -> Unit) {
    block()
}

このコードはコンパイルエラーになります。なぜなら、このreturnはmain関数をreturnしようとしていますが、引数のラムダはmain関数で実行される訳ではないため呼ぶことができないためです。

fun main() {
    val name = "Taro"
    execute {
        println("my name is $name")
        return // OK
    }
}
 
inline fun execute(block: () -> Unit) {
    block()
}

一方でinline関数にすることでコンパイルが成功します。なぜなら、inline関数のため引数のラムダは呼び出し元のmainに展開されるため、main関数でreturnすることができるためです。

non-local returnとはラムダ内から、ラムダを囲んでいる外側の関数をreturnすることです。

今回の例では、inline関数にすることでラムダ内のreturnが外側のmain関数のreturnになりました。

noinlineとcrossinline

inline関数にすると引数のラムダが展開されますが、細かな制御をnoinlineとcrossinlineで行うことができます。

fun main() {
    val name = "Taro"
    execute(
        block1 = {
            println("my name is $name")
            return // 'return' is prohibited here.
        },
        block2 = {
            println("Nice to meet you")
            return // 'return' is prohibited here.
        },
        block3 = {
            println("I like Kotlin")
            return // OK
        }
    )
}
 
inline fun execute(
    noinline block1: () -> Unit, // inline展開しない
    crossinline block2: () -> Unit // inline展開しないが、non-local returnは禁止する
    block3: () -> Unit,
) {
    block1()
    block2()
    block3()
}

noinlineは名前の通り、inline展開しません。そのためnon-local returnも利用できません。

crossinlineはinline展開はするが、non-local returnを禁止することができます。

crossinlineの利用場面

crossinlineはnon-local returnを禁止することができました。これはどのような場面で利用するのでしょうか?

それはinlineラムダを別のラムダやオブジェクトの中から呼ぶ場合です。

fun main() {
    val name = "Taro"
    execute(
        block = {
            println("my name is $name")
        },
    )
}
 
inline fun execute(
    block: () -> Unit
) {
    Runnable {
        block() // Cannot inline 'block: () -> Unit' here: it might contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block: () -> Unit'.
    }
}

上記のコードはinlineラムダをRunnableに渡しています。このような場合、Kotlinの静的解析は賢くコンパイルエラーにしてくれ、crossinlineを促します。
なぜならinlineラムダ内のnon-local returnの問題があるためです。

crossinlineにしてnon-local returnを禁止することでコンパイルが通ります。

まとめ

inline関数について学び呼び出し元での展開とnon-local returnについて知りました。また細かな制御でnon-inlineとcrossinlineについて理解しました。