Post

(Kotlin) 수신 객체 지정 람다 - with / apply / let / run / takeif / also

수신 객체 지정 람다

with : prefix없이 접근하고 싶을 때

with는 원래 파라미터가 2개인 함수다. 그러나 두 번째 인자인 람다를 밖으로 빼서 원래 언어가 지원하는 구문인 것 처럼 사용할 수 있다. 보기 깔끔해진다. with의 람다 내에서는 전달된 객체에 prefix없이 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun alphabet(): String {
    val stringBuilder = StringBuilder()
    
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            this.append(letter)
            append("!")    // this 생략 가능
        }
        
        this@OuterClass.somFunc()    // 바깥쪽 클래스 멤버 접근
        this.toString()    // return
    }
}

또는 이렇게 리팩토링 할 수 있다.

1
2
3
4
5
6
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    toString()
}

내부에서 널체크를 수행해주어야 한다는 단점이 있음. 이런 경우 let을 고려한다.

apply : 객체를 만들면서 인스턴스를 초기화하고 싶을 때

apply함수는 with와 거의 비슷한데, 확장 함수로 정의되어 있으며 자신에게 전달된 객체를 리턴한다는 차이점이 있다. 위 함수를 apply를 이용해 리팩토링하면 다음과 같다.

1
2
3
4
5
fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
}.toString()

apply함수는 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 할 때 유용하다. apply 내에서 호출하는 메소드나 프로퍼티는 수신 객체의 메소드나 프로퍼티를 의미한다.

1
2
3
4
5
6
fun createViewWithCustomAttributes(context: Context) =
    TextView(context).apply {
        text = "Sample"
        textSize = 20.0
        setPadding(10, 0, 0, 0)
    }

위 코드를 풀어 쓰면 이렇게 된다.

1
2
3
4
5
6
7
fun createViewWithCustomAttributes(context: Context) {
        val t = TextView(context)
        t.text = "Sample"
        t.textSize = 20.0
        t.setPadding(10, 0, 0, 0)
        return t
    }

[!tip] apply vs Builder - apply가 더 낫다.

  1. cmd + B로 따라갔을 때 해당 필드가 바로 보이고,
  2. apply 구문 내에서 복잡한 초기화도 가능하다.
let : 널이 될 수 있는 타입 인자로 넘기기

bar?.method()는 메소드 호출에 사용할 수 있지만, 함수에 f(bar)를 넘길 때는 사용할 수 없다. 이런 경우 let을 사용할 수 있다.

1
2
3
4
getTheBestPerson()?.let { sendEmailTo(it.email) }    // 아래 구문과 동일

val person: Person? = getTheBestPerson()
if (person != null) sendEmailTo(person.email)

변수를 할당하지 않아도 된다는 장점이 있지만 let을 중첩하면 가독성이 떨어진다는 단점이 있다.

run 활용
1
2
3
adapter ?: run { . . . }

if (adapter == null) { . . . }
takeif 활용

takeif는 조건이 참일 때 this를 리턴하고, 참이 아니면 null을 리턴한다.

1
2
3
4
5
6
// Original code
if (mBluetoothAdapter == null) { A }
if (!mBluetoothAdapter.isEnabled()) { B }

// Improved code
mBluetoothAdapter?.takeIf{ it.isEnabled() }?.run { B } ?: A
run let with apply also의 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline fun <T, R> T.run(block: T.() -> R): R = block()
inline fun <T, R> T.let(block: (T)  -> R): R = block(this)
inline fun <T, R> T.with(receiver: T, block: T.() -> R): R = receiver.block()
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}
inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

// use { }는 open하는 자원에 연결 시 close()를 자동으로 호출해준다.
// try-with-resources 에 대응한다고 보면 된다.
runCatching

try-catch-finally 대신 runCatching 도 사용 가능함. catch문 결과를 반환해주기 때문에 indent depth를 줄일 수 있음.

1
2
3
4
5
6
7
8
runCatching { 
    get() 
}.fold(
    onSuccess = { onSuccess(it) },
    onFailure = { onFailure(it) }
).also { 
    finally() 
}

언제 사용하면 좋은지? (let vs run)

https://kotlinlang.org/docs/scope-functions.html#function-selection

Here is a short guide for choosing scope functions depending on the intended purpose:

  • Executing a lambda on non-nullable objects: let
  • Introducing an expression as a variable in local scope: let
  • Object configuration: apply
  • Object configuration and computing the result: run
  • Running statements where an expression is required: non-extension run
  • Additional effects: also
  • Grouping function calls on an object: with
This post is licensed under CC BY 4.0 by the author.