【Kotlin】インライン関数


Kotlin のインライン関数は、単なる実行速度向上の手段ではなく、
コードの書き味に大きく貢献している機能です。

この記事では、Kotlin のインライン関数について、基本的な使い方から高度なテクニックまで説明します。


## 🌍一般的なインライン関数

インライン関数は、Kotlin 固有の概念ではなく、
古くから多くのプログラミング言語で採用されてきた機能です。

ここではまず一般的なインライン関数について簡単に説明します。


### インライン関数とは

インライン関数とは、「インライン展開」するように指示[^インライン展開するように指示]された関数のことです。
インライン展開とは、関数内に実装された処理を、その関数を呼び出している箇所に直接挿入することです。

[^インライン展開するように指示]: プログラマーからコンパイラへの指示。言語によっては、指示があっても状況(※)に応じてインライン展開しないことがある。※インライン展開しない方が効率がよいと判断された場合など

例えば次のようなコードを考えます。

```Kotlin:Kotlin
fun main() {
    printHelloWorld()
}

// インライン関数
inline fun printHelloWorld() {
    println("Hello World!")
}
```

これをコンパイルすると、
`main` 関数内での `printHelloWorld` 関数の呼び出しが、`printHelloWorld` 関数の内部処理である `println("Hello World")` に置き換えられ、
次のコードをコンパイルした場合と同様のコンパイル結果が得られます。

```Kotlin:Kotlin
fun main() {
    println("Hello World!")
}
```


### インライン関数を利用する動機

通常の関数呼び出しにはオーバーヘッド[^オーバーヘッド]があります。
インライン関数にすれば、実行時には関数呼び出しが行われなくなるため、オーバーヘッドがなくなります。

[^オーバーヘッド]: 間接的なコスト。引数や返値のコピー、戻り先アドレスの設定など。

一般的な言語において、インライン関数を利用する動機は、関数呼び出しのオーバーヘッドをなくすことによる実行速度向上です。

:::note warn
インライン化には、コンパイル時間が増える、実行ファイルが大きくなる、などのデメリットもあります。
またキャッシュが効かなくなってむしろ実行速度が低下することもありえます。

インライン化が効果的なのは、実行時に呼び出される回数が多い、小さな関数です。
:::

:::note warn
モジュール A が公開しているインライン関数をモジュール B で使っている場合、
モジュール A を別バージョンに更新しても、モジュール B を再コンパイルしない限り、モジュール B 内にインライン展開されたインライン関数は更新されません。
:::


## ![32px-Kotlin_Icon_2021.svg.png](https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Kotlin_Icon_2021.svg/32px-Kotlin_Icon_2021.svg.png)Kotlin でのインライン関数

Kotlin でも、一般的な言語と同様に、インライン関数にすることで実行速度向上を図れます。
**しかしそれだけではありません。**

以下では、Kotlin でのインライン関数について説明していきます。

🔰：初級者向け。既存のインライン関数を使う際に知っておきたいこと
🎓：中上級者向け。新たにインライン関数を実装する際に知っておきたいこと


### 🔰インライン関数の宣言

関数をインライン関数にするには、関数を宣言する際に `fun` の前に修飾子 `inline` を付けます。

```Kotlin
// fun の前に inline を付けることでインライン関数になる
inline fun myInlineFun() {
    // ...
}
```

標準ライブラリーの関数がインライン関数かどうかを公式の API ドキュメントで確認するには、
**各関数のページ**をご覧ください。
インライン関数であれば `fun` の前に `inline` が付いています。

たとえばスコープ関数 `let` であればここです。

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html

**関数一覧画面では `inline` が省略されている**ので判別できません。
関数一覧画面の例：

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/


### 🔰インライン関数の引数のラムダ

関数型の引数を持つインライン関数を呼び出す際に
その引数としてラムダを書くと、
そのラムダもインライン展開されます。

例えば次のようなコードをコンパイルすると

```Kotlin:インライン展開されるラムダ
fun main() {
    // インライン関数の引数で関数型のものをラムダで書く
    myInlineFun {
        println("world!")
    }
}

// インライン関数
inline fun myInlineFun(block: () -> Unit) {
    print("Hello, ")
    block()
}
```

次のコードをコンパイルしたのと同等の結果が得られます。

```Kotlin
fun main() {
    print("Hello, ")
    println("world!")
}
```

ただし、関数呼び出しの `(` `)` 内に直接書いたラムダ、もしくは[末尾のラムダ](https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas)でなければインライン化されません。

例えば次のコードをコンパイルしてもラムダはインライン展開されず

```Kotlin:インライン展開されないラムダ
fun main() {
    // 関数呼び出しの外で定義したラムダ
    val localFun = {
        println("world!")
    }

    myInlineFun(localFun)
}

// 先ほどの例と同じインライン関数
inline fun myInlineFun(block: () -> Unit) {
    print("Hello, ")
    block()
}
```

次のコードをコンパイルした場合と同等の結果になります。

```Kotlin
fun main() {
    val localFun = {
        println("world!")
    }

    print("Hello, ")
    localFun()
}
```

:::note warn
関数型の引数を持たない関数に `inline` を付けると、次のように警告されます。
> Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

（予想される、インライン化によるパフォーマンスへの影響は、僅かです。インライン化は関数型の引数を持つ関数に適しています。）
:::


### 🔰非局所リターン（non-local return）

インライン展開されたラムダ内の `return` もインライン展開され、
インライン関数の呼び出し元の関数からリターンすることができます。

これは「非局所リターン（non-local return）」と呼ばれます。
（局所（ラムダ内）に閉じたリターンではないということですね。）

次の例では `main` 関数の中でインライン関数 `myInlineFun` を呼び出しており、それに渡したラムダの中で `return` しています。
この `return` は `main` 関数からの `return` となるため、
それより後の処理である `println("after myInlineFun")` は実行されません。

```Kotlin
fun main() {
    myInlineFun {
        println("in myInlineFun")
        // 非局所リターン
        return
    }
    // ↑の非局所リターンにより main 関数が終了してしまうため、
    // ↓は呼ばれない。
    println("after myInlineFun")
}

inline fun myInlineFun(block: () -> Unit) {
    block()
}
```

この**非局所リターンは Kotlin の書き味に大きく貢献しています**。
ある程度 Kotlin コードを書いていれば、[スコープ関数](https://kotlinlang.org/docs/scope-functions.html)や [`forEach`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/for-each.html) のブロックの中でリターンしたことがあるはずです。
それができるのはそれらがインライン関数であり、ラムダがインライン展開されているからです。

:::note info
インライン展開されないラムダの中では非局所リターンはできません。

```Kotlin:非インライン関数の引数のラムダの中でリターンはできない
fun main() {
    myNonInlineFun {
        // コンパイルエラー！
        // 非インライン関数の引数のラムダ内では非局所リターンはできない。
        return
    }
}

// 非インライン関数
fun myNonInlineFun(block: () -> Unit) {
    // ...
}
```

```Kotlin:独立したラムダの中でリターンはできない
val localFun = {
    // コンパイルエラー！
    return
}
```
:::

:::note info
インライン展開されるラムダの中であっても、
`break` や `continue` は使うことができません。
ただし将来は使えるようになるかもしれません。
:::


### 🎓インライン関数内での処理と可視性

インライン関数内での処理でアクセスしている型・関数・プロパティの可視性は、インライン関数自身と同じかそれより広くなければなりません。

例えば次のように `public` なインライン関数内で `private` な関数を呼び出していると、
インライン展開された場合に `private` な関数を外部から直接呼び出されることになってしまうからです。

```Kotlin
class MyClass {
    inline fun myInlineFun() {
        // コンパイルエラー！
        // public なインライン関数内で
        // private な関数を呼び出している。
        myPrivateFun()
    }
    
    private fun myPrivateFun() {
        // ...
    }
}

fun main() {
    MyClass().myInlineFun()
    
    // ↑がインライン展開されると
    // ↓のようになる。

    // private 関数にアクセスしている！
    MyClass().myPrivateFun()
}
```


### 🎓インラインプロパティ

プロパティも、[バッキングフィールド](https://kotlinlang.org/docs/properties.html#backing-fields)を持たなければ、インラインにすることができます。

`inline` を `get` や `set` の前に付けることで個別にインラインにするかどうかを指定することもできますし、
`var` や `val` の前に付けることでまとめて指定することもできます。

```Kotlin
// バッキングフィールドを持つプロパティなのでインラインにはできない。
var number: Double = Double.NaN

/** 反数 */
// プロパティ全体をまとめてインラインにする
inline var opposite: Double
    get() = 0.0 - number
    set(opposite) {
        number = 0.0 - opposite
    }

/** 逆数 */
var reciprocal: Double
    // get と set を個別にインラインにする
    inline get() = 1.0 / number
    inline set(reciprocal) {
        number = 1.0 / reciprocal
    }
```


### 🎓`noinline`

ラムダをインライン展開したくない場合は `noinline` を指定します。

```Kotlin:noinline
inline fun myInlineFun(
    // noinline を指定する。
    noinline block: () -> Unit
) {
    // ...
}
```

インライン展開されないため、非局所リターンはできません。

```Kotlin:noinline のラムダでは非局所リターンはできない
inline fun myInlineFun(
    // noinline を指定する。
    noinline block: () -> Unit
) {
    // ...
}

fun main() {
    myInlineFun {
        // コンパイルエラー！
        return
    }
}
```

インライン展開されないため、返値にしたり、非インライン関数に渡したりできます。

```Kotlin:
inline fun myInlineFun(
    // noinline を指定する。
    noinline block: () -> Unit
): () -> Unit {
    // noinline なので返値にできる。
    return block
}
```

```Kotlin
inline fun myInlineFun(
    // noinline を指定する。
    noinline block: () -> Unit
) {
    // noninline なので非インライン関数に渡すことができる。
    myNonInlineFun(block)
}

// 非インライン関数
fun myNonInlineFun(block: () -> Unit) {
    // ...
}
```


### 🎓`crossinline`

インライン関数の引数のラムダを、インライン関数の中で定義したローカルオブジェクトやローカル関数（ラムダを含む）の中で呼び出したい場合は、
`crossinline` を指定します。

```Kotlin
inline fun myInlineFun(
    // crossinline を指定する。
    crossinline block: () -> Unit
) {
    myNonInlineFun {
        // ローカル関数(※)の中で呼び出すことができる。
        // ※今回の場合は、myNonInlineFun 関数の引数のラムダ
        block()
    }
}

// 非インライン関数
fun myNonInlineFun(block: () -> Unit) {
    // ...
}
```

`crossinline` が付いた引数に渡したラムダは、
インライン展開されますが、
非局所リターンは使えません。


### 🎓`reified`

ジェネリック関数の中で型パラメータを具体的な型として扱いたい場合は
`reified` を使います。

例えば、次のような親を持つノードを表すインターフェイスがあるとします。

```Kotlin
/** 親を持つノード */
interface Node {
    /** 親ノード */
    val parent: Node?
}
```

指定された型の祖先ノードを探す関数とその呼び出しは、普通は次のようになります。

```Kotlin:reifiedを使わなかった場合
/**
 * 指定された型の祖先ノードを探す。
 */
fun <T> findAncestorOfType(node: Node, clazz: Class<T>): T? {
    var ancestor = node.parent
    while (ancestor != null) {
        // Class<T> 型の引数を使用して変数の型を調べる。
        if (clazz.isInstance(ancestor)) {
            return clazz.cast(ancestor)
        }

        ancestor = ancestor.parent
    }

    return null
}

fun main() {
    val found = findAncestorOfType(myNode, MyType::class.java)
}
```

呼び出し元で `Class` オブジェクト（`MyType::class.java`）を渡してやる必要があるため、美しくありません。

`reified` を使えば次のように書くことができます。

```Kotlin:reifiedを使った場合
/**
 * 指定された型の祖先ノードを探す。
 */
// reified を指定する。
// Class<T> 型の引数は不要になる。
inline fun <reified T> findAncestorOfType(node: Node): T? {
    var ancestor = node.parent
    while (ancestor != null) {
        // 型パラメータ T が具体的な型であるかのようにして、
        // 変数の型を調べることができる。
        if (ancestor is T) {
            return ancestor
        }

        ancestor = ancestor.parent
    }

    return null
}

fun main() {
    val found: MyType? = findAncestorOfType(myNode)
}
```

型推論できる場合は関数呼び出しに型を書く必要もないため、使い勝手がよくなります。


／以上