본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item 46. 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라

  • inline 한정자의 역할은 컴파일 시점에 '함수를 호출하는 부분'을 '함수의 본문으로 대체' 하는 것
  • inline 한정자 사용의 장점
    • 타입 아규면트에 reified 한정자를 붙여서 사용할 수 있음
    • 함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작
    • 비지역(non-local) 리턴을 사용할 수 있음

타입 아규먼트를 reified로 사용할 수 있다

  • 제네릭의 경우 컴파일 타임에 타입정보가 사라짐
  • 따라서 타입 파라미터에 대한 연산에 오류가 발생

  • 함수를 인라인으로 만들면, 이러한 제한을 무시할 수 있음
  • 함수 호출이 본문으로 대체되므로, reified 한정자를 지정하면, 타입 파라미터를 사용한 부분이 타입 아규먼트로 대체

함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작한다

  • inline 한정자를 붙이면 함수 호출과 리턴을 위해 점프하는 과정과 백스택을 추적하는 과정이 없기 때문에 조금 더 빠르게 동작하는 것
  • 함수 파라미터를 가지지 않는 함수에서는 inline 한정자 유무에 따른 차이가 큰 차이를 발생시키지 않는다.
  • 함수 리터럴을 사용해 만들진 종류의 객체는 어떤 방식으로든 저장되고 유지되어야 함
  • 코틀린/JVM 에서는 JVM 익명 클래스 또한 일반 클래스를 기반으로, 함수를 객체로 만들어 낸다
class LambdaExample {
    val lambda: () -> Unit = {
        print("lambda")
    }
}

위의 예제는 아래와 같이 컴파일 된다.

  • 함수 본문을 객체로 wrapping 하기 때문에 코드의 속도가 느려진다.

인라인 함수와 인라인 함수가 아닌 함수의 중요한 차이는 함수 리터럴 내부에서 지역 변수를 캡쳐할 때 확인할 수 있다

  • 인라인이 아닌 람다 표현식에서는 표현식 바깥의 지역 변수를 직접 사용할 수 없다.
    • 람다 표현식이 객체로 랩핑 되기 때문
  • 따라서 지역 변수는 컴파일 과정 중에 레퍼런스 객체로 랩핑되고, 람다 표현식 내부에서 이를 사용
class Repeat {
    private fun repeat(action: () -> Unit) {}

    fun test() {
        var l = 1L
        repeat {
            l += 1
        }
    }
}

비지역적 리턴을 사용할 수 있다

  • 함수 리터럴이 컴파일될 때, 함수가 객체로 wrapping 되기 때문에 내부에서 리턴을 사용할 수 없다
  • 함수가 다른 클래스에 위치하기때문에 return을 사용해서 main으로 돌아올 수 없기 때문
  • inline을 사용하면 함수가 main 함수 내부에 박히기 때문에 return을 사용할 수 있다.

inline 한정자의 비용

  • 인라인 함수는 재귀적으로 동작할 수 없다
  • 인라인 함수는 더 많은 가시성 제한을 가진 요소를 사용할 수 없다
    • public 인라인 함수 내부에서는 private과 internal 가시성을 가진 함수와 프로퍼티를 사용할 수 없다
  • inline 한정자를 남용하면 코드의 크기가 쉽게 커진다

crossinline과 noinline

함수를 인라인으로 만들고 싶지만, 어떠한 이유로 일부 함수 타입 파라미터는 inline으로 받고 싶지 않은 경우가 있을 수 있다

  • crossinline
    • 아규먼트로 인라인 함수를 받지만, 비지역적 리턴을 하는 함수는 받을 수 없게 만든다
    • 인라인으로 만들지 않은 다른 람다 표현식과 조합해서 사용할 때 문제가 바생하는 경우 활용
  • noinline
    • 아규먼트로 인라인 함수를 받을 수 없게 만든다
    • 인라인 함수가 아닌 함수를 아규먼트로 사용하고 싶을 때 활용
class InlineKeywordExample {
    inline fun requestNewToken(
            hasToken: Boolean,
            crossinline onRefresh: () -> Unit,
            noinline onGenerate: () -> Unit
    ) {
        if (hasToken) {
            httpCall("get-token", onGenerate)
        } else {
            httpCall("refresh-token") {
                onRefresh()
                onGenerate()
            }
        }
    }

    fun httpCall(url: String, callback: () -> Unit) {

    }
}