Search

Inline functions

고차 함수를 사용하면 몇 가지 런타임 비용이 발생한다.
각 함수는 객체로 취급되며 클로저(함수 본문에서 접근할 수 있는 변수의 범위)를 캡처한다.
함수 객체와 클래스에 대한 메모리 할당, 가상 호출이 런타임 오버헤드를 유발한다.
하지만 많은 경우 이러한 오버헤드는 람다 표현식을 인라인화함으로써 제거할 수 있다.

인라인화(inline)

lock(l) { foo() }
Kotlin
복사
lock() 함수를 호출된 위치에서 쉽게 인라인화 할 수 있다.
l.lock() try { foo() } finally { l.unlock() }
Kotlin
복사
매개변수에 대해 함수 객체를 생성하고 호출하는 대신 컴파일러는 이러한 코드를 생성한다.
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }
Kotlin
복사
inline 수정자는 함수 자체와 전달된 람다에도 영향을 미치며 모두 호출된 위치에 인라인된다.
인라인화는 생성된 코드의 크기를 증가시킬 수 있지만 너무 큰 함수를 인라인화하지 않도록 적절하게 사용하면 루프 내부의 메가모픽 호출 사이트에서 성능 이점이 있다.

노인라인화(noinline)

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
Kotlin
복사
모든 람다가 인라인되기를 원하지 않는 경우 일부 함수 매개변수에 noinline 수정자를 사용하라.
인라인화 가능한 람다는 인라인 함수 내에서 호출되거나 인라인화 가능한 인수로 전달될 수 있지만 noinline 람다는 필드에 저장되거나 자유롭게 전달될 수 있다.
인라인 함수에 인라인화 할 수 없는 함수 매개변수나 재확인(reified) 타입 매개변수가 없으면 컴파일러는 경고를 발생시킨다.

비지역 반환(Non-local returns)

fun foo() { ordinaryFunction { return // 오류: `foo`에서 반환할 수 없습니다. } }
Kotlin
복사
명명된 함수나 익명 함수에서만 일반적인, 비정규화된 return을 사용하여 반환할 수 있다.
람다 내에서는 기본적인 return이 금지되며 람다에서 벗어나려면 label을 사용해야 한다.
람다는 외부 함수를 반환할 수 없기 때문이다.
fun foo() { inlined { return // OK: 람다가 인라인됩니다. } }
Kotlin
복사
하지만 람다로 전달된 함수가 인라인화된 경우 return도 인라인화되기 때문에 return이 허용된다.
fun hasZeros(ints: List<Int>): Boolean { ints.forEach { if (it == 0) return true // hasZeros 함수에서 반환됩니다. } return false }
Kotlin
복사
이러한 반환을 비지역 반환이라고 부른다.
이 구조는 보통 루프 내에서 발생하며 인라인 함수는 자주 루프를 포함한다.
inline fun f(crossinline body: () -> Unit) { val f = object: Runnable { override fun run() = body() } // ... }
Kotlin
복사
일부 인라인 함수는 전달된 람다를 함수 본문에서 직접 호출하는 것이 아니라 로컬 객체나 중첩된 함수와 같은 다른 실행 컨텍스트에서 호출할 수 있다.
이러한 경우 람다에서는 비지역 흐름 제어가 허용되지 않는다.
인라인 함수의 람다 매개변수가 비지역 반환을 사용할 수 없음을 나타내려면 crossinline 수정자를 사용하라.
breakcontinue는 아직 인라인 람다에서 사용할 수 없지만 지원할 계획이다.

구체화된 타입 매개변수(Reified Type Parameters)

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? }
Kotlin
복사
때때로 매개변수로 전달된 타입에 접근해야 할 때가 있다.
여기서는 트리를 올라가며 특정 타입의 노드가 있는지 리플렉션을 사용하여 확인한다.
treeNode.findParentOfType(MyTreeNode::class.java)
Kotlin
복사
코드는 문제가 없지만 호출 영역은 그렇게 아름답지 않다.
treeNode.findParentOfType<MyTreeNode>()
Kotlin
복사
더 나은 해결책은 이 함수에 타입을 간단히 전달하는 것이다.
inline fun <reified T> TreeNode.findParentOfType(): T? { var p = parent while (p != null && p !is T) { p = p.parent } return p as T? }
Kotlin
복사
인라인 함수는 reified 타입 매개변수를 지원하므로 위처럼 작성할 수 있다.
타입 매개변수를 reified 수정자로 한정하여 함수 내부에서 접근할 수 있게 한다.
이 함수는 인라인화되므로 리플렉션이 필요 없고 !isas 와 같은 일반적인 연산자를 사용할 수 있다.
myTree.findParentOfType<MyTreeNodeType>()
Kotlin
복사
호출 영역에선 위처럼 호출이 가능하다.
inline fun <reified T> membersOf() = T::class.members fun main(s: Array<String>) { println(membersOf<StringBuilder>().joinToString("\n")) }
Kotlin
복사
리플렉션이 필요 없는 경우가 많겠지만 reified 타입 매개변수를 사용하여 리플렉션을 사용할 수 있다.
인라인으로 표시되지 않은 일반 함수는 reified 매개변수를 가질 수 없다.
런타임 표현이 없는 타입(non-reified 매개변수 또는 Nothing과 같은 가상 타입)은 reified 타입 매개변수의 인수로 사용할 수 없다.

인라인 프로퍼티(Inline Properties)

val foo: Foo inline get() = Foo() var bar: Bar get() = ... inline set(v) { ... }
Kotlin
복사
inline 수정자는 백킹필드가 없는 프로퍼티의 접근자에 사용할 수 있다.
개별 프로퍼티 접근자에 어노테이션을 추가할 수 있다.
inline var bar: Bar get() = ... set(v) { ... }
Kotlin
복사
또는 전체 프로퍼티에 어노테이션을 추가할 수도 있으며 이 경우 접근자 둘 다 인라인된다.
호출 영역에서는 인라인 접근자가 일반 인라인 함수처럼 인라인된다.

공용 API 인라인 함수에 대한 제한

인라인 함수가 public 또는 protected 상태이면서 private 또는 internal 선언의 일부가 아닐 경우 이 함수는 모듈의 공용 API로 간주된다.
이는 다른 모듈에서도 호출 가능하며 해당 호출 사이트에서도 인라인된다.
이 경우 모듈이 선언한 인라인 함수의 변경으로 인해 호출하는 모듈이 다시 컴파일되지 않은 경우 이진 호환성 문제를 일으킬 수 있다.
모듈의 비공용 API가 변경될 때 이러한 비호환성 위험을 없애기 위해 공용 API 인라인 함수는 비공용 API 선언을 사용할 수 없다.
internal 선언은 @PublishedApi 로 어노테이션을 추가하면 공용 API 인라인 함수에서 사용할 수 있다.
@PublishedApi 로 표시된 내부 인라인 함수는 마치 공용 함수처럼 본문이 검사된다.