이번 절에서는 자바의 람다에는 없는 코틀린 람다의 독특한 기능을 살펴본다. 그 기능은 바로 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있게 하는 것이다. 그런 람다를 수신 객체 지정 람다(lambda with receiver)라고 부른다.

with 함수

fun alphabet(): String {
    val result = StringBuilder()

    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\\nNow I know the alphabet!")
    return result.toString()
}

fun main() {
    println(alphabet())
}

ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!

이 예제에서 result에 대해 다른 여러 메소드를 호출하면서 매번 result를 반복 사용했다. with을 이용해 반복을 피하면 다음과 같이 작성할 수 있다.

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) { // 메소드를 호출하려는 수신 객체 지정
        for (letter in 'A'..'Z') {
            this.append(letter) // "this"를 명시해서 앞에서 지정한 수신 객체의 메소드를 호출
        }
        append("\\nNow I know the alphabet!") // "this"를 생략하고 메소드를 호출
        this.toString() // 람다에서 값을 반환, 여기서도 this 생략 가능
    }
}

with문은 파라미터가 2개 있는 함수다. 첫 번째 파라미터는 stringBuilder이고, 두 번째 파라미터는 람다다. 람다를 괄호 밖으로 빼내는 관례를 사용함에 따라 전체 함수 호출이 언어가 제공하는 특별 구문처럼 보인다.

with 함수는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다. 인자로 받은 람다 본문 안에서는 this를 사용해 그 수신 객체에 접근할 수 있다.

수신 객체 지정 람다와 확장 함수 비교

확장 함수 안에서 this그 함수가 확장하는 타입의 인스턴스를 가리킨다. 그리고 그 수신 객체 this의 멤버를 호출할 떄는 this. 를 생략할 수 있다.

어떤 의미에서는 확장 함수를 수신 객체 지정 함수라 할 수도 있다.

일반 함수 일반 람다
확장 함수 수신 객체 지정 람다

람다는 일반 함수와 비슷한 동작을 정의하는 한 방법이다. 수신 객체 지정 람다는 확장 함수와 비슷한 동작을 정의하는 한 방법이다.

앞의 alphabet 함수를 더 리팩토링해서 불필요한 stringBuilder 변수를 없앨 수도 있다.

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\\nNow I know the alphabet!")
    toString()
}

식을 본문으로 하는 함수가 됐다. StringBuilder의 인스턴스를 만들고 즉시 with에게 인자로 넘기고, 람다 안에서 this를 사용해 그 인스턴스를 참조한다.

메소드 이름 충돌

with에게 인자로 넘긴 객체의 클래스와 with를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메소드가 있으면 무슨 일이 생길까? 그런 경우 this 참조 앞에 레이블을 붙이면 호출하고 싶은 메소드를 명확하게 정할 수 있다.

class OuterClass {
    
    private val outerLists = mutableListOf<Char>()
    
    fun append(item: Char) {
        outerLists.add(item)
    }
    
    fun alphabet() = with(StringBuilder()) {
        for (letter in 'A'..'Z') {
            this.append(letter)
            [email protected](letter)
        }
        append("\\nNow I know the alphabet!")
        toString()
    }
}