오늘도 더 나은 코드를 작성하였습니까?

kotlin 수신 객체 지정 람다(apply, with, let, also, run) 본문

Kotlin in Action/코틀린 기초

kotlin 수신 객체 지정 람다(apply, with, let, also, run)

hik14 2021. 3. 9. 12:22

apply, with, let, also, run 함수는 작동 방식 및 결과가 유사하여 헷갈리기 때문에 공통점과 차이점을 구별하여 적재적소에 사용하는 방법을 알아보자

 

위 5개의 함수는 공통적으로 수신객체 및 그 객체에 적용되는 람다 함수를 가진다. 

 

- 수신객체

- 수신객체 지정람다

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()
}

 

receiver(수신객체),  block(수신객체 지정람다)

 

T.( )의 값을 반환

즉, 수신객체를 가지고 함수 및 프로퍼티 연속된 접근을할 때 코드를 간결하고 읽기 쉽게 해준다.

class Person(val name: String, val age: Int)

fun main() {

    val person = Person("kotlin", 14)

//    println(person.name)
//    println(person.age)

    with(person){
        println(name)
        println(age)
    }
}

 also

@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
}

 

함수 호출시 수신객체의 전달 방법의 차이점

 

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

with - 수신 객체가 매개 변수 T 타입으로 직접 전달한다. 이를 명시적으로 제공된 수신 객체라고 한다.

 

fun <T> T.also(block: (T) -> Unit): T

also  - T 의 확장함수로 수신 객체가 암시적으로 제공된다.

 

함수에 전달된 수신객체가 다시 수신 객체 지정 람다에 어떻게 된달는지 차이점

 

receiver.block()

with 는 수신 객체 지정 람다가 T 의 확장함수 형태로 코드 블록 내에 수신 객체가 암시적으로 전달 됩니다.

 

block(this)
    return this

also 는 수신 객체 지정 람다 에 매개변수 T 로 코드 블록 내에 명시적으로 전달 됩니다.

 

함수 의 최종적인 반환 값의 차이점

 

: R

with 는 람다를 실행한 결과를 반환 합니다.

: T

also 는 코드 블록 내에 전달된 수신객체를 그대로 다시 반환 합니다.

 

also는 수신 객체 지정 람다에 명시적으로 T를 받았기 때문에 it을 사용해 프로퍼티에 접근한다.

객체의 속성을 전혀 사용하지 않거나 변경하지 않고 사용하는 경우에 also를 사용한다.

객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 사용한다.

class Person(val name: String, val age: Int){

}

fun main() {

    val person = Person("kotlin", 24)

    val kids = person.also {
        println(it.name)
        if (it.age > 14)
            println("어린이가 아니다")
    }
}

 

apply, with, let, also, run 정의 및 차이점

 fun <T, R> T.let(block: (T) -> R): R
  
 fun <T, R> T.run(block: T.() -> R): R
 
 fun <R> run(block: () -> R): R
 
 fun <T, R> with(receiver: T, block: T.() -> R): R
 
 fun <T> T.apply(block: T.() -> Unit): T
 
 fun <T> T.also(block: (T) -> Unit): T

 

 

apply ( fun <T> T.apply(block: T.() -> Unit): T)

확장함수(암시적 전달),  수신 객체 람다(암시적 전달),  수신 객체 반환

 

수신 객체 람다 내부에서 수신 객체의 함수를 사용하지 않고 수신 객체 자신을 다시 반환 하려는 경우에 apply를 사용한다.

수신 객체의 함수를 호출하는것이 불가능한것은 아니다.  프로퍼티를 초기화 하거나 , 변경 하려고 할때 사용한다.

 

  recyclerView.apply {
                layoutManager = LinearLayoutManager(requireActivity())
                adapter = TestAdapter
     }

also  ( fun <T> T.also(block: (T) -> Unit):  T )

확장함수(암시적 전달) 수신 객체 람다(명시적 전달),  수신 객체 반환

 

수신 객체 람다가 전달된 수신 객체를 전혀 사용 하지 않거나 수신 객체의 속성을 변경하지 않고 사용하는 경우

객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 사용한다.

또는 객체를 할당하기전에 부수적인 확인 및 효과를 검사 해야될 경우 사용한다.

 

let  ( fun <T, R> T.let(block: (T) -> R):  R )

확장함수(암시적 전달), 수신 객체 람다(명시적 전달), 수신 객체 람다 수행 결과 반환

 

수신 객체가 null 이 아닌 경우에 코드를 실행해야 하는 경우 안전호출 연산자인 ?. 과 같이 사용된다.

수신객체가 null이면 수신 객체 람다의 it은 Noting 타입이다.

Nullable 객체를 다른 Nullable 객체로 변환하는 경우.

 

T ?.let { } 형태에서의 let 블럭 안에는 non-null 만 들어올 수 있어 non-null 체크 시에 유용하게 쓸 수 있다.

객체 할당할때 또한 상황일 경우에는 elvis operator(?:) 를 사용해서 기본값을 지정해준다.

class Person(val name: String? = null, val age: Int){

    fun printName() = println(name)
}

fun main() {

    var person:Person? = Person( age= 12)
    
    val name = person?.let { it.name } ?: "no name"
    println(name)
}

 

Run  fun <T, R> T.run(block: T.() -> R):  R

확장함수(암시적 전달), 수신 객체 람다(암시적 전달), 수신 객체 람다 수행 결과 반환

 

명시적으로 받은  매개 변수 수신객체를 통해  암시적 호출하기 위해 변환하려는 경우에도 run ()을 사용한다.

fun printAge(person: Person) = person.run {
    print(age)
}

 

Run  fun <R> run(block: () -> R):  R

단순히 람다를 인자로 받아서 실행해 주는 역활을 하며 실행 결과를  반환.

정확히 말하면 위의 수신 객체 지정 람다와는 성격이 다르다.

fun main() {

    val plus = {x :Int, y: Int -> x + y}

    println(
        run {
            plus(1,2)
        }
    )
}

특정 값을 계산해야하거나 여러 지역 변수의 범위를 제한하려면 run () 함수를 사용한다.

val inserted: Boolean = run { 
    val person: Person = getPerson()
    val personDao: PersonDao = getPersonDao()
    personDao.insert(person)
}

 

여러 범위 지정 함수 결합

코드 가독성을 향상시키기 위해 범위 지정 기능을 분리하여 어떻게 사용할 수 있는지 보여주었습니다.

하나의 코드 블록 내에서 여러 범위 지정 함수를 중첩 하려는 경우가 종종 있습니다.

그러나 범위 지정 함수가 중첩되면 코드의 가독성이 떨어지고 파악하기 어려워 집니다.

원칙적으로 중첩은 하지 않는 것이 좋습니다.

 

수신객체 지정 람다 에 수신 객체가 암시적으로 전달되는 apply, run, with 는 중첩하지 마십시오.

 

apply, run, with은 수신 객체를 this 또는 생략하여 사용(암시적으로 람다에 전달)하며, 수신객체의 이름을 다르게 지정할수 없기 때문에 중첩될 경우 혼동 하기 쉬워집니다.

 

also 와 let 을 중첩 해야만 할때는 암시적 수신 객체 를 가르키는 매개 변수 인 it 을 사용하지 마십시오. 대신 명시적인 이름을 제공하여 코드 상의 이름이 혼동되지 않도록 해야 합니다.

 

범위 지정 함수를 호출 체인에 결합 할 수 있습니다.

중첩 과는 달리 범위 지정 기능을 호출 체인에 결합하면 코드의 가독성이 향상됩니다.

호출 체인에서 범위 지정 함수를 결합하는 예 를 살펴 보겠습니다.