인라인 함수

인라인 함수 (Inline functions)

고차 함수(higher-order functions)를 사용하면 어떤 런타임 비용이 따릅니다. 각 함수는 객체이고 클로저(closure)를 캡처하거든요. 클로저는 함수 본문에서 접근할 수 있는 변수들의 스코프를 말합니다. 메모리 할당(함수 객체와 클래스 모두)과 가상 호출(virtual call)은 런타임 오버헤드를 만들어냅니다.

출처: Kotlin 공식 문서 — Inline functions

본문

하지만 많은 경우 이런 오버헤드는 람다 표현식을 인라이닝(inlining)해서 없앨 수 있어요. 아래에 보여주는 함수들이 이런 상황의 좋은 예시입니다. lock() 함수는 호출 지점에서 쉽게 인라인될 수 있죠. 다음 경우를 생각해 보세요.

lock(l) { foo() }

매개변수에 대한 함수 객체를 만들고 호출을 생성하는 대신, 컴파일러는 다음 코드를 방출할 수 있습니다.

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

컴파일러가 이렇게 하도록 하려면 lock() 함수를 inline 수정자로 표시하세요.

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") 애노테이션으로 경고를 없앨 수 있습니다).

비지역 점프 표현식 (Non-local jump expressions)

Return

코틀린에서는 일반적이고 한정되지 않은(unqualified) return을 이름 있는 함수나 익명 함수를 빠져나갈 때만 쓸 수 있어요. 람다를 빠져나가려면 라벨을 사용하세요. 람다 안에서 한정되지 않은 return은 금지됩니다. 람다는 바깥 함수를 return시킬 수 없기 때문이에요.

fun ordinaryFunction(block: () -> Unit) {
    println("hi!")
}
//sampleStart
fun foo() {
    ordinaryFunction {
        return // ERROR: cannot make `foo` return here
    }
}
//sampleEnd
fun main() {
    foo()
}

하지만 람다가 전달되는 함수가 인라인되면, return도 함께 인라인될 수 있어요. 그래서 아래는 허용됩니다.

inline fun inlined(block: () -> Unit) {
    println("hi!")
}
//sampleStart
fun foo() {
    inlined {
        return // OK: the lambda is inlined
    }
}
//sampleEnd
fun main() {
    foo()
}

이런 return(람다 안에 위치하지만 바깥 함수를 빠져나가는 것)을 비지역 return(non-local return)이라고 해요. 이런 종류의 구조는 보통 인라인 함수가 감싸고 있는 루프에서 발생합니다.

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // returns from hasZeros
    }
    return false
}

일부 인라인 함수는 매개변수로 전달된 람다를 함수 본문에서 직접 호출하지 않고, 지역 객체나 중첩 함수 같은 다른 실행 컨텍스트에서 호출할 수도 있다는 점을 유의하세요. 이런 경우에도 람다에서 비지역 제어 흐름은 허용되지 않습니다. 인라인 함수의 람다 매개변수가 비지역 return을 사용할 수 없다는 것을 나타내려면, 람다 매개변수를 crossinline 수정자로 표시하세요.

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

Break와 continue

비지역 return과 유사하게, 루프를 감싸는 인라인 함수에 인자로 전달된 람다에서 breakcontinue 점프 표현식을 적용할 수 있어요.

fun processList(elements: List<Int>): Boolean {
    for (element in elements) {
        val variable = element.nullableMethod() ?: run {
            log.warning("Element is null or invalid, continuing...")
            continue
        }
        if (variable == 0) return true
    }
    return false
}

Reified 타입 매개변수

때로는 매개변수로 전달된 타입에 접근해야 할 때가 있어요.

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

여기서는 트리를 따라 올라가면서 리플렉션(reflection)으로 노드가 특정 타입인지 확인합니다. 전부 괜찮지만 호출 지점이 그렇게 깔끔하진 않네요.

treeNode.findParentOfType(MyTreeNode::class.java)

더 나은 해결책은 이 함수에 타입을 그냥 전달하는 거예요. 다음과 같이 호출할 수 있습니다.

treeNode.findParentOfType<MyTreeNode>()

이걸 가능하게 하기 위해 인라인 함수는 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"))
}

일반 함수(인라인으로 표시되지 않은)는 reified 매개변수를 가질 수 없습니다. 런타임 표현이 없는 타입(예: reified가 아닌 타입 매개변수나 Nothing 같은 가상의 타입)은 reified 타입 매개변수의 인자로 사용할 수 없어요.

인라인 속성 (Inline properties)

inline 수정자는 지원 필드(backing fields)가 없는 속성의 접근자에 사용할 수 있어요. 개별 속성 접근자에 애노테이션할 수 있습니다.

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

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

전체 속성에 애노테이션할 수도 있는데, 그러면 두 접근자가 모두 inline으로 표시됩니다.

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

호출 지점에서 인라인 접근자는 일반 인라인 함수처럼 인라인됩니다.

public API 인라인 함수의 제약

인라인 함수가 public 또는 protected이지만 private 또는 internal 선언의 일부가 아니라면, 그 함수는 모듈의 public API로 간주됩니다. 다른 모듈에서 호출될 수 있고, 그런 호출 지점에서도 인라인됩니다.

이것은 모듈이 인라인 함수의 변경을 일으켰을 때, 호출 모듈이 변경 후 다시 컴파일되지 않는다면 바이너리 비호환성(binary incompatibility) 위험을 초래합니다.

모듈의 non-public API 변경으로 인한 비호환성 위험을 없애기 위해, public API 인라인 함수는 본문에서 non-public-API 선언, 즉 privateinternal 선언 및 그 일부를 사용할 수 없습니다.

internal 선언은 @PublishedApi로 애노테이션할 수 있는데, 이렇게 하면 public API 인라인 함수에서 그 선언을 사용할 수 있어요. internal 인라인 함수가 @PublishedApi로 표시되면, 그 본문도 마치 public인 것처럼 검사됩니다.

더 알아보기 (Learn more)