Kotlin

[Kotlin] inline에 대하여 - inline, noinline, crossinline, reified

728x90

 

 

 inline 키워드에 대해서는 처음 코틀린을 공부할 때 보긴 봤었지만, 정확히 어떤 상황에서 사용하는지 알지 못했었다. 이번에 코틀린 확장 함수를 정리하다가 inline 키워드에 대하여 자세히 알게 되었고, 관련 내용을 기술하려고 한다. 

 

 inline 키워드를 한마디로 설명한다면 다음과 같다.

고차 함수를 사용하면 런타임 패널티가 있기 때문에 함수 구현 자체를 코드에 넣음으로써 오버헤드를 없앨 수 있다.

 이게 대체 무슨 말일까?  예시를 통해 좀 더 쉽게 알아보자. 

 

 

inline

fun doSomething(action: () -> Unit) {
    action()
}

fun callFunc() {
    doSomething {
        println("문자열 출력!")
    }
}

 위와 같은 고차 함수가 있다고 하자. 이 코드를 자바로 변환한다면, 아래와 같이 된다.

public void doSomething(Function action) {
    action.invoke();
}

public void callFunc() {
    doSomething(System.out.println("문자열 출력!");
}

 그리고 이 자바 코드는 아래와 같이 변환된다.

public void callFunc() {
    doSomething(new Function() {
        @Override
        public void invoke() {
            System.out.println("문자열 출력!");
        }
    }
}

 여기서 내부적으로 객체 생성과 함께 함수 호출을 하게 되어 있어서, 이런 부분에서 오버헤드가 생길 수 있다. inline 키워드는 이런 오버헤드를 없애기 위해 사용한다.

inline fun doSomething(action: () -> Unit) {
    action()
}

fun callFunc() {
    doSomething {
        println("문자열 출력!")
    }
}

 위의 코드를 자바로 변환하면 아래와 같이 된다.

public void callFunc() {
    System.out.println("문자열 출력!");
}

 위와 같이 Function 인스턴스를 만들지 않고, callFunc() 내부에 삽입되어 바로 선언된다. 위의 코드가 컴파일될 때 컴파일러는 함수 내부의 코드를 호출하는 위치에 복사한다. 컴파일되는 바이트코드의 양은 많아지겠지만, 함수 호출을 하거나 추가적인 객체 생성은 없다.

 이와 같은 이유로 inline 함수는 일반 함수보다 성능이 좋다. 하지만 inline 함수는 내부적으로 코드를 복사하기 때문에, 인자로 전달받은 함수는 다른 함수로 전달되거나 참조할 수 없다.

inline fun doSomething(action1: () -> Unit, action2: () -> Unit) {
    action1()
    anotherFunc(action2) // error
}

fun anotherFunc(action: () -> Unit) {
    action()
}

fun main() {
    doSomething({
        println("1")
    }, {
        println("2")
    })
}

 위의 코드에서 doSomething()에 두 번째 인자로 넘겨받은 action2를 또 다른 고차 함수인 anotherFunc()에 인자로 넘겨주려 하고 있다. 이때 doSomething()은 inline 함수로 선언되어 있기 때문에 인자로 전달받은 action2를 참조할 수 없기 때문에 전달하는 것이 불가능하다. 이렇게 모든 인자를 inline으로 처리하고 싶지 않을 때 사용하는 것이 noinline 키워드다.

 

 

noinline

 인자 앞에 noinline 키워드를 붙이면 해당 인자는 inline에서 제외된다. 따라서 noinline 키워드가 붙은 인자는 다른 함수의 인자로 전달하는 것이 가능하다.

inline fun doSomething(action1: () -> Unit, noinline action2: () -> Unit) {
    action1()
    anotherFunc(action2)
}

fun anotherFunc(action: () -> Unit) {
    action()
}

fun main() {
    doSomething({
        println("1")
    }, {
        println("2")
    })
}

 

 

crossinline

 아래의 코드는 View의 클릭 이벤트를 보다 쉽게 연결해주기 위한 확장 함수다.

inline fun View.click(block: (View) -> Unit) {
    setOnClickListener { view ->
        block(view) // error
    }
}

 함수를 인자로 받아 setOnClickListener 내부에서 호출해야 하는데 위의 코드는 동작하지 않는다. inline 함수는 인자로 받은 함수를 다른 실행 컨텍스트를 통해 호출할 때는 함수 안에서 비-로컬 흐름을 제어할 수 없다. 이럴 때 사용하는 것이 crossinline 키워드다.

inline fun View.click(crossinline block: (View) -> Unit) {
    setOnClickListener { view ->
        block(view) 
    }
}

 

 

reified

 위의 예시에서 보인 확장 함수를 제네릭을 사용해서 좀 더 확장해보자.

inline fun <T: View> T.click(crossinline block: (T) -> Unit) {
    setOnClickListener { view ->
        block(view as T) 
    }
}

 위의 코드는 제네릭을 사용해서 block의 인자로 View가 아닌 T를 넣어준다. 예를 들어, TextView.click으로 사용한다고 하면 인자로 TextView를 받기 위함이다. 위의 코드에서는 오류는 아니지만 경고 메시지가 뜬다.

Unchecked cast: View! as T

 view를 T로 캐스팅하려고 할 때 발생하는 경고 메시지이다. 이는 inline 함수에서 특정 타입을 가졌는지 판단할 수 없기 때문이다. 이럴 때 reified 키워드를 사용한다.

inline fun <reified T: View> T.click(crossinline block: (T) -> Unit) {
    setOnClickListener { view ->
        block(view as T) 
    }
}

 타입 파라미터에 reified 키워드를 붙여주면 마치 클래스처럼 타입 파라미터에 접근할 수 있다. 참고로 reified는 inline이 아닌 일반 함수에서는 사용할 수 없다.

 

 

참고

medium.com/harrythegreat/kotlin-inline-noinline-%ED%95%9C%EB%B2%88%EC%97%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1d54ff34151c

codechacha.com/ko/kotlin-inline-functions/

728x90