Kotlin / Programming

ザクっと理解するKotlin Inline functions(インライン関数)

インライン関数

高階関数は実行時にオブジェクトに変換され、その関数内で使用されている外部変数やパラメータにアクセスするためクロージャーをキャプチャー(保存・保持)します。
しかし、これにはメモリ割り当てや仮想呼び出しなどの追加の実行時オーバーヘッドが発生します。

多くの場合、このオーバーヘッドはラムダ式をインライン展開することで削減できます。lock関数を例に見てみます。

lock(l) { foo() }

lock関数があると仮定します。
この関数はパラメータを2つ持ち、1つ目のパラメータに対してロックを掛け、2つ目として受け取った関数を実行します。
その後、受け取った関数の実行がエラーになっても1つ目のパラメータのロックを解除します。

これは高階関数であるため実行時にオブジェクトに変換され、オーバーヘッドが発生します。
しかしコンパイラは代わりのコードを生成することができます。

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

こうすることで高階関数を使用する必要がなくなります。
コンパイラにこの変換をさせるには、lock関数にinline修飾子をつける必要があります。

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

inlineを使用すると、上記の「try {…} finally {…}」のように、関数自体と引数として渡されてたラムダ式も影響を受けます。

このようにインライン関数を使うと生成されるコードは長くなりますが、パフォーマンス向上などの利点があります。
ただし大規模な関数のインライン展開を避けましょう。

noinline

パラメータとして受け取るラムダ式にnoinline修飾子をつけると、そのラムダ式だけインライン展開をしません。

inline fun function01(param01: () -> Unit, noinline param02: () -> Unit) { ... }

Non-local returns

修飾子がついていないreturnは名前がある関数か無名関数にしか使えません。
一方、ラムダ式から抜けるためにはラベルを使用します。
ラムダ式自体が囲んでいる関数を返すことはできないため、ラムダ式内での単純なreturn文は禁止されています。

fun main() {
    function01(){
      println("Hello World")
      return // Error
      return@function01 // No Error
    }
}

fun function01(operation: () -> Unit) {
    operation()
}

しかし渡されたラムダ式がinlineの場合、単純なreturnが使用できます。

fun main() {
    function01(){
      println("Hello World")
      return
    }
}

inline fun function01(operation: () -> Unit) {
    operation()
}

このように、ラムダ式内に存在するが、ラムダ式の外側の関数から抜けるようなreturnを非ローカルリターンと呼びます。

一部のインライン関数はパラメータとして渡されたラムダ式を直接呼ばず、ローカルオブジェクトやネストされた関数のように間接的に呼び出す場合があります。
このような場合、インライン関数のラムダパラメータが非ローカルなリターンを使用できないことを示すために、ラムダパラメータにcrossinline修飾子を付けます。

fun main() {
    val items = listOf("Apple", "Banana", "Avocado")
    
    function01(items) { item ->
        println(item)
    }
}

inline fun function01(items: List<String>, action: (String) -> Unit) {
    val innerProperty01 = { x: String ->
        if (x.startsWith("A")) println("This fruit name starts with A")
        else action(x) // Error
    }
    
    items.forEach { item ->
        innerProperty01(item)
    }
}

このコードでは、function01関数が関数型を含むパラメータと共に呼び出されています。
関数型を指定しているパラメータのactionは、innerProperty01変数のラムダ式で呼ばれています。
このようにinline関数のパラメータとして渡されたラムダ式を直接呼ばず、その関数内のローカルオブジェクトやネストされた関数で間接的に呼び出す場合は、ラムダパラメータにcrossinline修飾子をつける必要があります。

crossinline修飾子を付つけていないと下記エラーが出力されます。

Can’t inline ‘action’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘action’
訳: actionは非ローカルリターンを含む可能性があるため、ここでは使用できません。
crossinline修飾子をパラメーターの宣言につけてください。

inline fun processItems(items: List<String>, crossinline action: (String) -> Unit) {
  ...
}

このように付ければ問題ありません。

reified修飾子

reified修飾子とはジェネリクス機能の一部です。
通常、ジェネリックな型パラメータはコンパイル時に型情報が失われ、実行時には具体的な型情報が利用できません。
しかし、reified修飾子がついたパラメータを使用すると、実行時にも型情報を利用することができます。
よってreified修飾子をつけることで実行時に型情報を保持できます。
また、このパラメータのことを具現化された型パラメータと呼びます。

fun main() {
    val list01 = listOf(1, "a", true)
    printType01(Boolean::class.java, list01)
    printType02<Boolean>(list01)
}

fun <T> printType01(clazz: Class<T>, list01: List<*>) {
    list01.forEach {
        if (it is T) { // Error
            println("${list01.size} is ${clazz.simpleName}")
        }
    }
}

inline fun <reified T> printType02(list01: List<*>) {
    list01.forEach {
        if (it is T) {
            println("${list01.size} is ${T::class.simpleName}")
        }
    }
}

printType01はrefiedを使用せずにジェネリックな型をitと比較しています。
しかしジェネリックな型はコンパイル時に情報を失うため比較できず下記エラーになってしまいます。

Cannot check for instance of erased type: T
訳: 消去された型のインスタンスであるTの確認はできません

printType02はrefied修飾子を使っています。
reified修飾子を使うことで実行時でも型情報を保持できます。
そのためエラーにならず型チェックができています。

inline関数以外の関数ではreifiedは使用できません。
また、reifiedではない型パラメータやNothingのように実行時に表現されない型はreified型のパラメータとして設定できません。

インラインプロパティ

inline修飾子はプロパティのアクセサにも使用でき、各アクセッサに個別に指定することも、全体に指定することもできます。

class Class01 {
    // 個別指定
    var prop01: String
    	inline get() = "HI"
    	set(value) {
            prop01 = value
        }
    // 全体指定
    inline var prop02: String
    	get() = "Hello"
    	set(value) {
            prop01 = value
        }
}

publicなAPIのインライン関数に対する制限

インライン関数がpublicまたはprotectedであるが、privateもしくはinternal宣言ではないものの一部である場合、その関数はモジュールのパブリックなAPIであると認識され、その関数が他のモジュールで呼ばれる可能性があります。

これによりインライン関数を宣言するモジュールの変更があった場合、呼び出し元のモジュールが再コンパイルされないとエラーが発生する可能性があります。

このようなモジュール内の非publicなAPIの変更によるエラーをなくすため、publicなAPIのインライン関数は非publicなAPI宣言(privateおよびinternalの宣言およびその一部)を関数本体で使用することは許されていません。

internalな宣言は@PublishedApiとアノテーションすることで、publicなAPIのインライン関数内で使用可能になります。
internalなインライン関数が@PublishedApiとアノテーションされた場合、publicとして扱われるため、その本体自体もチェックされます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です