본문 바로가기

Kotlin

[Kotlin] Scope Functions (let, with, run, apply, also)

 

틀린의 주요 특징 중 Scope function들의 개념이 너무 복잡해서 직접 정리를 하려고 한다.

 

관련 내용들과 예제들의 아래 코틀린 사이트를 참조 했다.

https://kotlinlang.org/docs/reference/scope-functions.html

 

https://kotlinlang.org/docs/reference/scope-functions.html

 

kotlinlang.org

 

1. let

The context object is available as an argument (it). The return value is the lambda result.

(컨텍스트 오브젝트는 인수 (it)으로 이용가능하다. 리턴값은 람다의 결과이다.)

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 *
 * For detailed usage information see the documentation for [scope functions]
 * (https://kotlinlang.org/docs/reference/scope-functions.html#let).
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
 

let 함수는 해당 함수를 호출한 객체를 이어지는 함수 블록의 인자로 전달을 한다.

( (T)로 되어 있는 형태가, 함수의 인자로 전달한다는 내용 )

때문에 block 내에서 it 을 이용하여 전달된 객체를 참조할 수 있다.

 

아래와 같은 코드가 있을 때,

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)    
 
결과 : [5, 4, 4]
 

이를 let함수를 써서 아래와 같이 변경할 수 있다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 
 

불필요한 resultList 변수의 선언을 제거했다. 이와 같이 let 함수를 사용하면 불필요한 변수 선언을 방지할 수 있다.

 

또한, 더블콜론(::)을 사용해서 아래와같이 it을 없앨 수도 있다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
 

하지만 개인적인 생각으로는 가독성이 많이 떨어지는 것 같다.

 

추가적으로, Null 이 아닌 경우를 체크한 뒤 바로 특정 작업을 수행할 때에도 let을 사용할 수 있다.

이 때 함 수 블록으로 전달된 객체는 null이 아님이 보장된다.

val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}
 

만약 str 이 null 일경우엔 length엔 null 이 저장된다.

무엇인가를 반환하려면 람다식 맨 끝에 반환하고자 하는 값을 써주면된다.

 

let을 사용하는 또다른 케이스는 코드의 가독성을 향상키기 위해 제한된 범위의 지역변수를 소개하는 것이다.

즉, it 대신에 람다식의 특정 이름을 명명하여 이를 사용할 수 있다.

    val numbers = listOf("one", "two", "three", "four")
    val modifiedFirstItem = numbers.first().let { firstItem ->
        println("The first item of the list is '$firstItem'")
        if (firstItem.length >= 5)
            firstItem
        else
            "!" + firstItem + "!"
    }.toUpperCase()
    println("First item after modifications: '$modifiedFirstItem'")
 

2. with

let과 유사하지만 인자 전달 방식과 참조 하는 방식이 다름.

A non-extension function: the context object is passed as an argument, but inside the lambda, it's available as a receiver (this). The return value is the lambda result.

(with는 비확장함수다 : 컨텍스트 객체를 인자로 넘긴다. 그러나 람다 내부에서는 수신 객체로써 이용가능하다. 리턴 값은 람다의 결과이다.)

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 *
 * For detailed usage information see the documentation for [scope functions]
 * (https://kotlinlang.org/docs/reference/scope-functions.html#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()
}
 

let 과 달리 with는 객체를 인자로 넘겨주는 것을 볼 수 있다. 또한 객체의 확장함수처럼 동작한다.

공식 사이트에는 다음과 같이 나와있다.

 

We recommend with for calling functions on the context object without providing the lambda result. In the code, with can be read as “with this object, do the following.

람다의 결과를 제공하는것 없이 수신객체를 호출하는 함수에서 with를 사용하는 것을 추천 한다. 코드 내에서 with는 다음과 같이 읽을 수 있다. "이 객체로 다음을 수행하라"

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}
 
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
        " the last element is ${last()}"
}
println(firstAndLast)
 

with 의 경우 전달되는 객체가 null될 수 있기 때문에 null 안정성이 보장되진 않는다.

 

with는 null이 아닌 객체를 이용해서 리턴값이 필요없는 작업을 수행할 때 사용하면 유용할 것으로 보인다.

3. run

The context object is available as a receiver (this). The return value is the lambda result.

run does the same as with but invokes as let - as an extension function of the context object.

run is useful when your lambda contains both the object initialization and the computation of the return value.

 

컨텍스트 객체는 리시버 (this)로 이용가능하며, 리턴값은 람다의 결과이다.

run은 with와 동일하지만 let 처럼 동작한다. - 컨텍스트 객채의 확장함수처럼 동작한다.

run은 람다에 객체 초기화와 리턴값의 계산이 포함되어 있을 때 유용하다.

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 *
 * For detailed usage information see the documentation for [scope functions]
 * (https://kotlinlang.org/docs/reference/scope-functions.html#run).
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
 

하는동작은 with와 동일한데, let 처럼 동작하는 함수이다. (점점더 헷갈린다 ㅋㅋㅋ)

 

함수 형태만 보면, let과 거의 동일해보인다. 차이가 있다면 let은 컨텍스트 객체를 block 의 인자로 전달했다는 점이고, run은 컨텍스트 객체의 확장함수 형태로 사용한다는 것이다.

 

다음은 같은 코드를 run과 let 으로 했을 때의 차이점이다.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}
 

아래와 같이 어떤 변수를 초기화 하는데, 그 과정에서 복잡한 계산을 위해 여러 임시변수가 필요하다면 run 함수를 이용할 수 있다. 해당 함수 내부에서 사용되는 변수는 블럭 외부에 노출되지 않는다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}
 

4. apply

The context object is available as a receiver (this). The return value is the object itself.

컨텍스트 객체는 receiver(this)로 이용가능하다. 리턴 값은 객체 자신이다.

(위 let,with,run 과 다른 점이 드디어 나왔다. 리턴값이 전달된 객체 자신이다.)

Use apply for code blocks that don't return a value and mainly operate on the members of the receiver object. The common case for apply is the object configuration. Such calls can be read as “apply the following assignments to the object.

값을 리턴하지 않거나, 주로 수신된 객체의 멤버를 동작한다면 apply 를 사용해라. apply를 위한 주된 케이스는 객체 configuration 이다.이런 호출을 다음과 같이 부를 수 있다. " 다음의 할당을 객체에 적용해라."

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 *
 * For detailed usage information see the documentation for [scope functions]
 * (https://kotlinlang.org/docs/reference/scope-functions.html#apply).
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
 

예제는 다음과 같다.

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
 

객체의 값들을 할당하고 해당 객체를 리턴받아 바로 adam 에 적용했다.

 

Having the receiver as the return value, you can easily include apply into call chains for more complex processing.

수신자를 리턴 값으로 설정하면보다 복잡한 처리를 위해 콜 체인에 쉽게 적용 할 수 있습니다.

 

5. also

The context object is available as an argument (it). The return value is the object itself.

컨텐스트 객체는 인수 it 으로 이용가능하다. 리턴값은 객체 자신이다.

 

also is good for performing some actions that take the context object as an argument. Use also for additional actions that don't alter the object, such as logging or printing debug information. Usually, you can remove the calls of also from the call chain without breaking the program logic.

When you see also in the code, you can read it as “and also do the following”.

(also는 파라미터로써 컨텍스트 객체를 취한 몇가지 액션을 수행하는 것에 좋다. also는 로깅이나 디버그 정보들을 출력하는 등의 객체를 변경하지 않는 추가적인 동작해서 사용한다. 일반적으로, 프로그램 로직을 중단하지 않고 also의 호출을 제거할 수 있다.

코드에서 slao를 볼 때, 다음과 같이 읽을 수 있다. 그리고 또한 아래의 것도 수행해라.

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 *
 * For detailed usage information see the documentation for [scope functions]
 * (https://kotlinlang.org/docs/reference/scope-functions.html#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
}
 

let 처럼 객체를 매개변수 it으로 받아서 사용한다.

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")
 

 

Function
간단한 가이드
사용 예
let
Executing a lambda on non-null objects
null 인 아닌 객체에서 람다를 실행할 때


Introducing an expression as a variable in local scope
로컬 범위에서 변수로 표현식 소개

apply
Object configuration
객체 구성

run
Object configuration and computing the result
객체 구성 및 결과 계산


Running statements where an expression is required: non-extension
표현식이 필요한 실행문 : 비확장

also
Additional effects
추가 효과

with
Grouping function calls on an object
객체에 대한 함수 호출을 그룹화

 

하지만 결국 함수 별로 사용 사례가 겹치는 경우가 있기 때문에, 프로젝트 혹은 팀 내에서 사용하는 규칙에 따라 사용하면 될 것 같다.

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 개발노트  (0) 2022.01.13
[Kotlin] 람다 표현식  (0) 2022.01.13
[Kotlin][Kotlin in action] 1. 코틀린이란?  (0) 2020.11.25