Kotlin - with와 apply란? (수신 객체 지정 람다)
오늘 다루어볼 내용은 코틀린의 with와 apply 함수이다. 바로 예제로 들어간다.
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()
}
위 코드는 알파벳을 출력해주는 함수이다. 뭔가 StringBuilder를 생성하여 특정 변수에 담고, 해당 변수를 사용해 함수를 호출하고 뭔가 군더더기가 많이 붙어있는 느낌이다. 해당 코드를 with 함수를 통해 리팩토링이 가능하다.
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I Know the alphabet!")
toString()
}
코드가 뭔가 훨씬 깔끔해졌다. 눈에 띄는 것이 with 함수이다. 간단하게 with 함수는 하나의 인자를 받는 함수 같지만, 실제로는 2개의 인자를 받는 함수이다.
- 첫번째 인자 : 수신객체
- 두번째 인자: 첫번째에 받은 인자가 수신 객체인 람다
람다에서는 첫번째 인자로 받은 StringBuilder의 인스턴스가 수신객체가 되기때문에 람다안에서 마치 StringBuilder의 내부 함수처럼 사용이 가능하다.(this 키워드로 참조가 가능하지만, 겹치는 함수 이름이 없어 this를 생략하였다.) 그리고 해당 with 함수의 반환값은 람다의 반환값이 된다. with 함수의 내부를 살펴보자.
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
우선 with 함수는 T 타입을 인자로 받고 R타입을 리턴하는 함수이다. 내부 코드를 보면 조금 특이하다. 두번째 인자로 받은 람다가 마치 첫번째로 받은 인자 타입이 수신객체인 확장함수처럼 동작을 하고 있는 것이다. 이런 with 함수를 이용하면 특정 상황에서 훨씬 깔끔한 코드가 나올 수 있다.
apply함수
with 함수는 람다의 반환값이 with의 반환값이 된다. 하지만, 람다는 람다대로 수행하고 실제 수신 객체가 함수의 리턴값이 되길 바랄때가 있는데 이럴때 apply 함수를 이용한다.
fun alphabetUsingApply() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I Know the alphabet!")
}.toString()
위 예제는 with 예제와는 다르게, 우선 람다대로 수행하고 리턴값이 수신 객체의 인스턴스 자기 자신이 되는 것이다.
- with : 람다의 마지막 식(결과)가 with함수의 반환값이 된다. 즉, with의 두번째 인자 람다의 반환은 특정 객체를 리턴해야하는 것이다.
- apply : 람다는 람다대로 수행하고, 수신 객체 자기자신을 반환한다. 즉, apply의 인자인 람다의 반환은 Unit(void)이다.
apply의 함수 내부를 살펴보자.
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
apply 함수 내부를 보면, apply를 호출한 인스턴스 타입이 수신 객체인 확장 함수 람다를 인자로 받는다. 해당 람다의 반환은 Unit이다. 실제 코드 내부에서는 해당 람다를 수행하고 자기자신(this)를 리턴하고 있다. 실제 apply를 활용하여 코틀린이 구현한 유용한 함수의 예제로 아래와 같다.
/**
* buildString은 StringBuilder를 만들어주는 것과 toString을 호출하는것을 대신해준다.
*/
fun alphabetUsingBuildString() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I Know the alphabet!")
}
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
여기까지 코틀린의 with와 apply에 대해 간단히 다루어보았다.