Kotlin のインライン関数は、単なる実行速度向上の手段ではなく、
コードの書き味に大きく貢献している機能です。
この記事では、Kotlin のインライン関数について、基本的な使い方から高度なテクニックまで説明します。
🌍一般的なインライン関数
インライン関数は、Kotlin 固有の概念ではなく、
古くから多くのプログラミング言語で採用されてきた機能です。
ここではまず一般的なインライン関数について簡単に説明します。
インライン関数とは
インライン関数とは、「インライン展開」するように指示1された関数のことです。
インライン展開とは、関数内に実装された処理を、その関数を呼び出している箇所に直接挿入することです。
例えば次のようなコードを考えます。
fun main() {
printHelloWorld()
}
// インライン関数
inline fun printHelloWorld() {
println("Hello World!")
}
これをコンパイルすると、
main
関数内での printHelloWorld
関数の呼び出しが、printHelloWorld
関数の内部処理である println("Hello World")
に置き換えられ、
次のコードをコンパイルした場合と同様のコンパイル結果が得られます。
fun main() {
println("Hello World!")
}
インライン関数を利用する動機
通常の関数呼び出しにはオーバーヘッド2があります。
インライン関数にすれば、実行時には関数呼び出しが行われなくなるため、オーバーヘッドがなくなります。
一般的な言語において、インライン関数を利用する動機は、関数呼び出しのオーバーヘッドをなくすことによる実行速度向上です。
インライン化には、コンパイル時間が増える、実行ファイルが大きくなる、などのデメリットもあります。
またキャッシュが効かなくなってむしろ実行速度が低下することもありえます。
インライン化が効果的なのは、実行時に呼び出される回数が多い、小さな関数です。
モジュール A が公開しているインライン関数をモジュール B で使っている場合、
モジュール A を別バージョンに更新しても、モジュール B を再コンパイルしない限り、モジュール B 内にインライン展開されたインライン関数は更新されません。
Kotlin でのインライン関数
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
しています。
この return
は main
関数からの 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
}
インライン展開されるラムダの中であっても、
break
や continue
は使うことができません。
ただし将来は使えるようになるかもしれません。
🎓インライン関数内での処理と可視性
インライン関数内での処理でアクセスしている型・関数・プロパティの可視性は、インライン関数自身と同じかそれより広くなければなりません。
例えば次のように public
なインライン関数内で private
な関数を呼び出していると、
インライン展開された場合に private
な関数を外部から直接呼び出されることになってしまうからです。
class MyClass {
inline fun myInlineFun() {
// コンパイルエラー!
// public なインライン関数内で
// private な関数を呼び出している。
myPrivateFun()
}
private fun myPrivateFun() {
// ...
}
}
fun main() {
MyClass().myInlineFun()
// ↑がインライン展開されると
// ↓のようになる。
// private 関数にアクセスしている!
MyClass().myPrivateFun()
}
🎓インラインプロパティ
プロパティも、バッキングフィールドを持たなければ、インラインにすることができます。
inline
を get
や set
の前に付けることで個別にインラインにするかどうかを指定することもできますし、
var
や val
の前に付けることでまとめて指定することもできます。
// バッキングフィールドを持つプロパティなのでインラインにはできない。
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
を指定します。
inline fun myInlineFun(
// noinline を指定する。
noinline block: () -> Unit
) {
// ...
}
インライン展開されないため、非局所リターンはできません。
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?
}
指定された型の祖先ノードを探す関数とその呼び出しは、普通は次のようになります。
/**
* 指定された型の祖先ノードを探す。
*/
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 を指定する。
// 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)
}
型推論できる場合は関数呼び出しに型を書く必要もないため、使い勝手がよくなります。
/以上