조용한 담장

코틀린(Kotlin) : 인라인 함수 (Inline Functions) 본문

kotlin

코틀린(Kotlin) : 인라인 함수 (Inline Functions)

iosroid 2020. 1. 2. 17:39

코틀린(kotlin) 의 인라인 함수 (inline function) 을 살펴보자.

원문 https://kotlinlang.org/docs/reference/inline-functions.html 을 보며 정리.

고차함수(higher-order functions) 를 사용한다는 것은 어느정도의 런타임 패널티를 가지게 되는 셈이다.

고차함수에서 사용되는 각 함수는 object 가 되며 closure 를 가지고 있게 되는데 이는 함수의 body 내에서 접근되는 변수들도 가지고 있다는 말이 된다.

함수 오브젝트와 클래스를 위해 메모리를 할당하고 가상 호출(virtual call) 이 일어나는 것은 런타임 오버헤드를 가진다.

하지만 많은 경우에 이런 오버헤드는 람다 표현식을 인라인 방식으로 적용함으로 써 제거될 수 있다.

아래 예제와 같은 경우 lock() 같은 함수는 호출 위치에서 쉽게 인라인(inline) 될 수 있을 것이다.

lock(l) { foo() }

파라미터의 foo() 를 위해 함수 오브젝트를 만들고 호출하는 동작을 만드는 대신에 컴파일라가 아래와 같은 코드로 변경해 주면, 동일한 동작에 대해 오버헤드가 줄어드는 결과를 얻을 수 있을 것이다.

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

inline 수정자(modifier) 를 사용하면 컴파일러가 위와 같은 동작을 하도록 할 수 있다.

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

inline 은 함수 자신과 인자로 전달된 람다에 모두 적용된다. 모두 호출되는 위치에 인라인 될 것이다.

noinline

단지 몇개의 람다만 인라인 함수로 전달하고자 할때는 해당하는 함수 파라미터에만 noinline modifier 를 쓸 수 있다.

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

인라인 가능한 람다 는 인라인 함수 내에서만 호출 되거나 또는 인라인 가능한 argument 로써만 전달될 수 있다.

하지만 noinline 함수는 다양하게 방식으로 쓰일 수 있다.

인라인 함수가 인라인 가능한 (inlinable) 함수 파라미터가 없거나 reified type parameters 가 아닐때는 컴파일러는 경고를 발생시킬 것이며, 이는 그런 함수를 인라인 함으로써 얻을 수 있는 이득이 매우 적기 때문이다.

그런 경고에도 써야 한다면 @Suppress("NOTHING_TO_INLINE") 표시를 사용할 수 있다.

Non-local returns

람다를 종료하기 위해서는 라벨 을 반드시 써야 하며 람다는 둘러싼 함수(enclosing function) 에서 리턴을 할 수 없기 때문에 return 만 쓰는것은 금지된다.

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

람다가 인라인 되기 위해 전달된 경우에는 리턴도 인라인 되기 때문에 return 사용이 가능해 진다.

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

람다 안에 있지만 둘러싼 함수에 존재하는 리턴을 non-local return 이라 한다.

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

어떤 인라인 함수는 함수 바디에서 바로 파라미터로 전달된 것이 아닌, 로컬 오브젝트(local object) 나 중첩 함수(nested function) 같은 또다른 실행 문맥 으로부터 파라미터로 전달된 람다를 호출할 수도 있다.

이런 경우에는 non-local flow 또한 람다에서는 허용되지 않는다.

이것을 명시하기 위해서 람다 파라미터는 crossinline modifier 로 표시되어야 한다.

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ...
}
break and continue are not yet available in inlined lambdas, but we are planning to support them too.

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

이건 tree 를 거슬로 올라가 reflection 을 사용하여 해당 노드가 특정 타입을 가지는지 체크한다.

호출하는 부분이 별로 이쁘지 않다.

treeNode.findParentOfType(MyTreeNode::class.java)

실제로 아래처럼 타입을 함수로 전달하는 방법이 간단할 것이다.

treeNode.findParentOfType<MyTreeNode>()

위의 내용이 reified type parameters 을 지원하는 인라인 함수를 통해 아래처럼 사용될 수 있다.

 

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

reified modifier 가 사용됨으로써 타입 파라미터 T 는 마치 일반 클래스 처럼 함수 내에서 접근이 가능해 진다.

함수는 인라인 되기 때문에 reflection 은 필요하지 않고, !is, as 같은 일반 오퍼레이터가 동작이 가능해 진다.

또한 myTree.findParentOfType<MyTreeNodeType>() 도 호출이 가능해 진다.

대부분의 경우에는 reflection 이 필요하지 않지만 reified type parameter 와 함께 사용할수는 있다.

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

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

인라인이 아닌 일반 함수는 reified parameter 를 가질 수 없다.

run-time representation 을 가지지 않은 type 은 reified type parameter 를 위한 argument 로 쓰일 수 없다.

Inline properties (since 1.1)

inline modifier 는 backing field 를 가지지 않은 프로퍼티의 접근자에 쓰일 수 없다.

각각의 프로퍼티 접근자에는 쓸 수 있다.

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

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

접근자 두개를 다 가진 프로퍼티 전체를 위해서는 쓸 수 있다.

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

호출되는 위치에서 인라인 접근자는 일반 인라인 함수처럼 처리된다.

Restrictions for public API inline functions

인라인 함수가 public 이거나 protected 이고 private 이나 internal 선언의 일부가 아니면, module 의 public API 로 간주된다.

이것은 다른 모듈에서 호출될 수 있고 호출되는 위치에 인라인 될 수도 있다.

코드 변경 후 재컴파일 되지 않은 모듈을 호출하는 경우, 인라인 함수를 선언하는 그 모듈의 변경사항으로 인해 binary incompatibility(비호환) 의 위험을 가지게 되는 문제가 있다.

위와 같은 문제를 가진 모듈의 non-public API 때문에 public API 인라인 함수는 non-public-API 선언을 사용하는것이 금지된다.

internal 선언은 public API 인라인 함수에서 사용이 가능한 @PublishedApi 과 함께 표시될 수 있다.

internal 인라인 함수가 @PublishedApi 로 표시되면 그 함수의 body 도 public 인 것 처럼 체크된다.

Comments