LoginSignup
5
4

【Kotlin】インライン関数

Posted at

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

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

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

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

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

インライン関数とは

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

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

Kotlin
fun main() {
    printHelloWorld()
}

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

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

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

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

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

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

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

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

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

32px-Kotlin_Icon_2021.svg.pngKotlin でのインライン関数

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

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

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

🔰インライン関数の宣言

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

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

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

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

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

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

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

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

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

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

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

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

ただし、関数呼び出しの ( ) 内に直接書いたラムダ、もしくは末尾のラムダでなければインライン化されません。

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

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

    myInlineFun(localFun)
}

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

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

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

    print("Hello, ")
    localFun()
}

関数型の引数を持たない関数に 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 しています。
この returnmain 関数からの return となるため、
それより後の処理である println("after myInlineFun") は実行されません。

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

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

この非局所リターンは Kotlin の書き味に大きく貢献しています
ある程度 Kotlin コードを書いていれば、スコープ関数forEach のブロックの中でリターンしたことがあるはずです。
それができるのはそれらがインライン関数であり、ラムダがインライン展開されているからです。

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

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

// 非インライン関数
fun myNonInlineFun(block: () -> Unit) {
    // ...
}
独立したラムダの中でリターンはできない
val localFun = {
    // コンパイルエラー!
    return
}

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

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

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

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

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

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

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

🎓インラインプロパティ

プロパティも、バッキングフィールドを持たなければ、インラインにすることができます。

inlinegetset の前に付けることで個別にインラインにするかどうかを指定することもできますし、
varval の前に付けることでまとめて指定することもできます。

// バッキングフィールドを持つプロパティなのでインラインにはできない。
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 を指定します。

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

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

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

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

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

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

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

🎓crossinline

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

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

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

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

🎓reified

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

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

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

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

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 を使えば次のように書くことができます。

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

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

/以上

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

  2. 間接的なコスト。引数や返値のコピー、戻り先アドレスの設定など。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4