Edit Page

インライン関数

高階関数を使用すると、ある種のランタイムペナルティを課せられます。 各関数はオブジェクトであり、それはクロージャ、すなわち、関数の本体でアクセスされるそれらの変数をキャプチャします。 メモリ割り当て(関数オブジェクトとクラス用の両方)と仮想呼び出しは、実行時のオーバーヘッドを招きます。

しかし、多くの場合、オーバーヘッドはこの種のラムダ式をインライン化することによって解消することができると思われます。 以下に示す関数は、このような状況の良い例です。 すなわち、lock() 関数は、簡単に呼び出し箇所でインライン化することができました。次のケースを考えてみます。

lock(l) { foo() }

パラメータために関数オブジェクトを作成してコールを生成する代わりに、 コンパイラは次のコードを生成しても良いでしょう:

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

コンパイラがこのようにできるようにするためには、 inline 修飾子で lock() 関数をマークする必要があります:

inline fun <T> lock(lock: Lock, body: () -> T): T { ... }

inline 修飾子は、関数自体や関数の引数に渡されたラムダの両方を呼び出し箇所にインライン化させるはたらきをもちます。

インライン化では生成されるコードが大きくなる可能性がありますが、合理的な方法で(大きな関数をインライン化しないで)実行すると、 パフォーマンスの向上で割に合います、 特にループ内の 「メガモーフィック (megamorphic)」な呼び出し箇所では。

noinline`

インライン関数に渡されたラムダのうち、インライン化したくないものがある場合は、 noinline 修飾子を付けることができます:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

インライン化可能なラムダは、インライン関数内で呼び出すか、 インライン可能な引数として他に渡すかのどちらかくらいしか出来ません。 ですが、 noinline は、好きなように操作できます。例えば、フィールドに保持したり、誰かに渡したり等。

インライン関数にインライン化できる関数の引数がなく、 reified型パラメータが指定されていない場合、 コンパイラは警告を発します。 このような関数のインライン展開が有益な事はめったに無いためです (自分のケースではインライン展開が必要と確信を持っているなら、@Suppress("NOTHING_TO_INLINE") アノテーションで警告を抑制できます)。

非局所リターン

Kotlinでは、名前付き関数または無名関数から抜けるためには、通常、ラベル無し return のみが使用できます。 ラムダを終了するにはラベルを使用しなければなりません。 ラムダが自身を囲んでいる関数からの return を作ることができないため、ラムダ内での裸のリターンは禁止されています。

fun ordinaryFunction(block: () -> Unit) { println("hi!") } //sampleStart fun foo() { ordinaryFunction { return // エラー: `foo` をここで return することはできない } } //sampleEnd fun main() { foo() }

しかし、ラムダが渡された関数がインライン関数の場合、 returnも同様にインライン化することができます。 その場合はそれ(訳注:ラムダの中のreturn)が許可されています:

inline fun inlined(block: () -> Unit) { println("hi!") } //sampleStart fun foo() { inlined { return // OK: ラムダはインライン化される } } //sampleEnd fun main() { foo() }

(ラムダに位置するが、それを囲んでいる関数から抜ける)このようなリターンは、 非局所リターン(non-local return) と呼ばれています。 このような構造は通常はループで起こるもので、 そこではしばしばインライン関数に囲まれています:

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // hasZeros から return する
    }
    return false
}

インライン関数の中には、パラメータとして渡されたラムダを、関数本体から直接ではなく、 ローカルオブジェクトやネストされた関数などの別の実行コンテキストから呼び出すものがあります。 このような場合には、ラムダの中の非局所制御フローは許可されません。 インライン関数のパラメータのラムダが非局所リターンを使えないという事を示すために、 ラムダパラメータを crossinline 修飾子でマークする必要があります:

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ...
}

breakcontinue はインライン化されたラムダではまだ利用できませんが、我々はそれらをサポートすることを計画しています。

reified型パラメータ

(訳注:reifyは具体化する、というような意味)

時にはパラメータとして渡された型にアクセスする必要があります。

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

ここでは、ツリーをたどってリフレクションを使用して、ノードに特定のタイプがあるかどうかを確認します。全く問題はないのですが、呼び出し箇所はそれほど美味しくなりません:

treeNode.findParentOfType(MyTreeNodeType::class.java)

この関数に単に型を渡せる方が良い解決策でしょう。それなら、以下のように呼び出せます:

treeNode.findParentOfType<MyTreeNodeType>()

これを実現するために、インライン関数は reified型パラメータ をサポートしています。 そのおかげで、私たちは以下のように書くことができます:

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

上のコードでは reified 修飾子で型パラメータを修飾しています。 これで、関数内で型がアクセス可能になり、これは通常のクラスとまるで同じであるかのように機能します。 関数はインライン化されているので、リフレクションは必要ありません。 !isas のような通常の演算子が動作するようになります。 また、前述したようなやりかたで呼び出すことができます:myTree.findParentOfType<MyTreeNodeType>()

リフレクションは多くの場合に必要とされないかもしれませんが、reified型パラメータではリフレクションを使う事も出来ます:

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

通常の関数(inlineとしてマークされていない)はreifiedパラメータをもつことはできません。 実行時表現を持たない型(例えば、reifiedされていない型パラメータや Nothing のような架空の型)は、reified 型のパラメータの引数として使用できません。

インラインプロパティ

inlineの修飾子はプロパティのアクセサにも、 バッキングフィールドを持たないプロパティであればつける事が出来ます。 プロパティの個々のアクセサにアノテート出来ます。

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ...
    inline set(v) { ... }

プロパティ全体をアノテートする事も出来ます。その場合は両方ともinlineになります:

inline var bar: Bar
    get() = ...
    set(v) { ... }

呼び出し側では、インラインのアクセサは通常のインライン関数と同様にインライン化されます。

パブリックなAPIでのインライン関数の制限

インライン関数がpublicprotectedで、しかもprivateinternalの宣言の一部で無い場合は、 モジュールのパブリックAPIとみなされます。 それは他のモジュールから呼ばれ得るし、 つまりは他のモジュールの呼び出し側でインライン化されます。

これは、モジュールがインライン関数として宣言しているものの中身を変更した時に呼び出し側を再コンパイルしない場合にバイナリ非互換になるリスクがあります。

モジュールがパブリックAPIを変更した時にそのようなリスクを引き起こすのを避ける為に、 パブリックなAPIのインライン関数では、非パブリックなAPIとして宣言されたものを使う事は許されていません。 つまり、privateinternal宣言されているものをそうした関数の本体で使う事は出来ません。

internal宣言に@PublishedApiアノテーションをつけると、パブリックなAPIのインライン関数で使う事が出来るようになります。 internalなインライン関数に@PublishedApiマークをつけると、 その本体もパブリックであるかのようにチェックされるようになります。