본문 바로가기

코틀린

코틀린: 확장 함수와 수신 객체를 갖는 함수 타입(3/3)

■ 확장 함수 타입은 왜 필요할까요?
아래 예제에서 함수 addItems()의 형식인자 action은 일반 함수 타입을  갖습니다. 이 함수 타입을 갖는 변수 newItem을 addItems()의 실인자로 전달할 수 있습니다. 코드는 직관적이고 간단하지만, 람다 식 블록에서 매번 키워드 it를 사용해 StringBuilder 타입 객체를 참조해야 합니다. 그렇다고 it를 생략할 수는 없습니다. 

fun addItems(action: (StringBuilder) -> Unit): String {
    val sb = StringBuilder()
    action(sb)
    return sb.toString()
}

fun main() {
    val newItem: (StringBuilder) -> Unit = {
        it.append("Hong ")
        it.append("25 ")
        it.append("Incheon ")
    }
    val row = addItems(newItem)
    println(row) // Hong 25 Incheon
}

 

아래처럼 수신 객체를 갖는 함수 타입, 즉 확장 함수 타입으로 고치면 어떨까요? 확장 함수 타입을 갖는 람다 식 블록에서는  action(sb)가 아니라 sb.action()으로 함수 action()을 호출합니다. action() StringBuilder 클래스에서 정의한 함수가 아님에도 불구하고, 확장 함수를 호출할 때와 똑같은 방식으로 호출할 수 있습니다. 

fun addItems(action: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.action()
    return sb.toString()
}

fun main() {
    val newItem: StringBuilder.() -> Unit = {
        this.append("Hong ")
        append("25 ")       // this는 생략 가능.
        append("Incheon ")
    }
    val row = addItems(newItem)
    println(row) 
}

함수 addItems()는 범위 함수 apply()를 적용하면 코드를 확~ 줄일 수 있습니다. 함수 타입 형식 인자 action()을 apply()의 형식인자로 전달할 수 있기 때문입니다. 범위 함수 apply()는 자신의 수신 객체 StringBuilder()를 함수 action()의 형식 인자로 사용합니다.

fun addItems(action: StringBuilder.() -> Unit): String =
    StringBuilder().apply(action).toString()

아래에 보인 것처럼 확장 함수 타입을 갖는 변수(newItem)를 StringBuilder 타입 객체 sb의 확장 함수로 호출할 수 있습니다. 또, 객체 sb의 진짜(?) 멤버 메소드 append()의 인자로 확장 함수를 전달할 수도 있습니다.

fun addItems(action: StringBuilder.() -> Unit): String =
    StringBuilder().apply(action).toString()


fun main() {
    val newItem: StringBuilder.() -> Unit = {
        this.append("Hong ")
        append("25 ")
    }
    val sb = StringBuilder()
    sb.newItem()
    sb.append(addItems { append("Incheon ") })
    println(sb.toString())
}

 

■ 범위 함수(scope function)는 확장 함수
run, also, apply, let 등의 범위 함수는 모두 확장 함수입니다. 범위(scope)란 용어를 사용하는 것은 적용 범위를 확장 함수의 형식 인자인 람다 식 블록으로 제한하기 때문입니다. 범위 함수의 구조는 수신_객체.범위_함수 { ... } 입니다. 예들 들면, run() 함수의 형식 인자가 람다 식 { ... } 입니다. 람다 식 블록에 수신 객체(this) 또는 형식 인자(it)가 전달되는 형태입니다. 범위 함수에 관한 자세한 설명은 4.16절을 참고하기 바랍니다. 

이제 라이브러리에서 정의한 범위 함수를 이해할 준비를 모두 마쳤습니다. 아래 코드에서 알 수 있는 것처럼 apply()는 수신 객체의 확장 함수로 선언되어 있습니다. 또, 형식인자 block도 확장 함수 타입으로 선언되어 있음을 알 수 있습니다. 람다 식 블록에서 수신 객체는 this로 참조할 수 있습니다. 코드에서 알 수 있는 것처럼 apply()는 수신 객체를 반환합니다.

inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

아래 코드를 보면 범위 함수 with()가 확장 함수가 아님을 쉽게 알 수 있습니다. with()는 2개의 형식인자를 갖고 있습니다. 첫 번째 형식인자는 수신 객체 receiver이며, 두 번째 형식인자는 확장 함수 타입으로 선언한 함수(=람다 식) block입니다. with()는 함수를 호출해 실행한 결과를 반환합니다.

inline fun <T, R> with (receiver: T, block: T.() -> R ): R =
    receiver.block()