본문 바로가기

카테고리 없음

Kotlin scope operation: apply, run, let, also

apply, run, let, also.

왜 코틀린에는 비슷비슷한 역할을 가진 Extension이 있을까요?

 

결론부터 얘기하면, 코틀린에서 익스텐션이나 람다를 쓰기 위한 예시로서 코틀린 개발진이 "다른 확장함수를 만들 때 이걸 보고 공부하세요"라는 의미라고 저는 생각합니다.

 

특히, let을 주목할 필요가 있는데, 람다에 파라메터를 전달하는 예시이기 때문입니다.

 

그럼 상세한 내용을 알아봅시다.

 

코틀린 공식 가이드에는 각 operation을 어떤 경우에 써야 하는지에 대한 가이드가 있는데요,

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

널체크를 위해서: let
표현식을 변수처럼 이용하기 위해서 : let
오브젝트의 설정값: apply
오브젝트를 설정하고 그 결과를 계산하기 위해: run
하나의 표현식이 필요한 특정 구문을 수행하기 위해: extension 이 아닌 run
추가적인 처리를 위해: also
특정 오브젝트에 대해서 여러 함수를 실행하기 위해: with

 

처음 이 Scope operation을 접했을 때 헷갈리는 부분은, 위와 같은 사용법이 막 그렇게 절대적인 것은 아니고, 서로 바꿔서 써도 별 문제 없다는 점입니다. 간단하게 그냥 널체크하고 특정 로직을 수행한 뒤 값을 반환할 필요 없이 끝날 때는, ?.let {} / ?.run{} / ?.apply{} / ?.also{} 에 별다른 차이가 없습니다. 그런데 왜 이렇게 여러가지 익스텐션을 만들어 놓은 것일까요?

 

먼저, 표를 이용해서 정리해서 보면 좀 더 쉽게 이해할 수 있습니다.

 

 

1. 입력값이 리시버인 경우

리시버: 이 스코프 오퍼레이션이 사용되는 대상.

출력 값 \ 람다안에 전달되는 값 리시버 파라메터
괄호안에서 수행된 결과(람다)를 리턴 run (람다에 this 입력, block result 출력) let (람다에 it 입력, block result 출력)
자기자신(this)를 리턴 apply (람다에this 입력, this 출력) also (람다에 it 입력, this 출력)

이렇게 살펴보면, 입력 / 출력 값의 종류에 따라 종류별로 하나씩 만들어 놓은것이 보이시죠?

 

상세 코드:

더보기
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

 

 

2. 그 외

run : 코드를 실행해서 결과를 리턴

with: 리시버를 따로 입력받아서, 람다 안의 파라메터로 그 리시버를 전달하고 블록의 결과 값을 리턴.

 

상세코드:

더보기
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}



예:

val aaa: Int = run {
10
}



@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

 

let, 람다에 어떻게 파라메터를 전달했나?

 

자바에서 코틀린으로 넘어왔을 때 굉장히 낯설게 느껴질 수 있는 부분이 이 부분이라고 생각합니다. 파라메터, block을 잘 주목하시기 바랍니다.

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

비슷한 다른 예를 먼저 봅시다.

 

Safe let: 널체크해야 하는 변수가 여러 개 인 경우 쓸 수 있는 let

stackoverflow.com/a/35522422/850347

inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
    return if (p1 != null && p2 != null) block(p1, p2) else null
}

// 예시:
val risk = safeLet(person.name, person.age) { name, age ->
  // do something
}   

 

뭔가 감이 오시나요?

 

 

다시 let으로 돌아와서,  여기서 주목해야 할 부분은 

let 의  block : (T) -> R

 

입니다. 이 표현식의 뜻은,

- 이 파라메터의 이름은 block.

- 그 타입은 람다인데,  T 라는 타입의 값을 입력받아 -> R 이라는 타입을 리턴하는 람다

라는 뜻입니다.

그래서 마지막 라인에서 return block(this) 와 같은 형태로 block에 this 를 넣어서 리턴하고 있습니다. (말하자면 T == this)

 

safelet의 경우에는, 아래와 같은 형태로 block 을 입력받는데요, 

block : (T1, T2) -> R

그래서 마지막 라인에서 return block (p1, p2) 와 같은 형태로 블록 (람다)에 파라메터 2개를 넣어서 리턴하고 있습니다.

* safelet은 여러 변수를 입력받기 때문에 T.let() 과 같이 T의 확장함수를 구현하는 대신 단순히 파라메터를 입력받았습니다. 그래서 각 변수 역시 별도의 파라메터로 입력 받았습니다.

 

비교를 위해 run 의 block을 봅시다.

block: T.() -> R

this 의 extension 을 하나 만들어 실행하는 것과 같은 효과인데,

그래서 run 의 block은 return block() 이런 식으로 단순하게 리턴되지만, 람다 내에 자동으로 this 가 T의 this로 지정됩니다.

 

 

 

Reference:

medium.com/@fatihcoskun/kotlin-scoping-functions-apply-vs-with-let-also-run-816e4efb75f5
medium.com/@limgyumin/코틀린-의-apply-with-let-also-run-은-언제-사용하는가-4a517292df29

blog.mindorks.com/using-scoped-functions-in-kotlin-let-run-with-also-apply