Nuke Olaf - Log Store

[Kotlin] 코틀린 범위 지정 함수 let, with, run, apply, also 본문

Language/[Kotlin]

[Kotlin] 코틀린 범위 지정 함수 let, with, run, apply, also

NukeOlaf 2020. 3. 31. 01:19

코틀린 표준 라이브러리는 간결하고 명료한 코드를 작성하는 데 도움을 주는 확장 함수(extension function)들을 제공한다.
확장함수는 람다(lamda) 를 인자로 받아 동작하며, 확장함수를 실행하는 주체수신자 또는 수신자 객체라고 한다.

확장 함수에는 범위 지정 함수(scope-functions) let, with, run, apply, also 가 있다.
람다식이 제공된 객체에서 범위 지정 함수를 호출하면 임시 범위가 형성되는데, 이 범위에서 이름없이 객체에 접근할 수 있다.
이러한 기능을 가진 함수를 범위 지정 함수라고 한다.

이 다섯가지 범위 지정 함수들은 기본적으로 동일한 기능을 수행한다.
객체에서 코드 블럭을 실행하는 기능이다.

let, with, run, apply, also 의 주요 차이점은 다음과 같다.
(1) 객체가 블록 내에서 사용 가능한지 여부 (Context 객체를 참조하는 방법)
(2) 전체 표현식의 결과 (반환 값)

이 다섯가지 범위 지정 함수를 각각의 특징에 맞게 적재적소에 사용하는것이 좋다.

 

1. apply 함수

(1) apply 함수란?

apply 란 "적용하다" 라는 뜻이다. 즉, apply 함수의 의 람다에 작성된 코드를 수신자 객체에 "적용"하는 함수라고 생각할 수 있다. apply the following assignments to the object.

apply 함수를 사용하면, apply 함수를 호출한 수신자 객체를 구성하기 위해 apply 의 람다에 포함된 수신자 함수들을 연속적으로 호출한다. 그 다음, 람다의 실행이 끝나면 구성된 수신자 객체가 반환된다. 이렇게 수신자 객체를 구성(object configuraion)하는데 사용하기 때문에 apply 함수를 '구성함수' 라고 생각할 수 있다.

(2) apply 함수를 사용하는 방법

apply 함수의 선언은 다음과 같다

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

T 라는 객체를 통해서 apply 함수가 호출된다. T.() 는 람다 리시버라고 하는데,
위의 apply 함수 선언에서 람다 리시버는 수신자 객체의 타입 T를 block 함수의 입력인 T.() 로 전달한다. 

apply 함수는 반환값이 수신자 객체 자기 자신이다.

그래서 값을 반환하지 않고 수신자 객체의 멤버(프로퍼티)에서 작동하는 코드 블록에 apply 를 사용한다.

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

 

(3) apply 함수를 사용하는 이유

apply 함수는 이러한 경우에 주로 사용한다

  • 우리가 사용할 객체를 구성할때 반복되는 코드 양을 줄이기 위해
  • 특정 객체를 생성함과 동시에 초기화 해줄 때

 

2. let 함수

(1) let 함수란?

let 함수는 let 함수를 호출하는 수신자 객체를 블록의 인자로 넘기고, 블록의 결과값을 반환하는 함수이다.

(2) let 함수를 사용하는 방법

let 함수의 선언은 다음과 같다

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

T 라는 객체를 통해서 let 함수가 호출된다.
let 함수를 호출한 자기자신, 수신자 객체 T 를 받아서 람다 실행의 결과값인 R 을 반환하는 block 을 입력으로 받는다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 
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
}
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'")

(3) let 함수를 사용하는 이유

let 함수는 이러한 경우에 주로 사용한다

  • 호출 체인(call chain) 결과에서 하나 이상의 함수를 호출하고 싶을 때
  • null 체크 후 코드를 실행하기 위해 (지정된 값이 null 이 아닌 경우에 코드 실행)
  • 단일 지역 변수의 범위를 제한하고 싶을 때
  • 블록 내의 결과물을 반환하고 싶을때

 

3. with 함수

(1) with 함수란?

with 함수는 비 확장함수이다. context 객체를 인자로 직접 전달 받고, 람다 내부에서 이를 수신자로 사용한다.
run 함수와 유사하지만, 리시버로 전달할 객체가 어디있는지 다르며, Null 안정성 안전한 호출(?.)을 지원하지 않는다.

with this object, do the following.

(2) with 함수를 사용하는 방법

with 함수의 선언은 다음과 같다.

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

객체(reciever) 를 직접 입력 받고, 객체를 사용하기 위한 block 함수를 두 번째 매개변수로 받는다.
람다 리시버는 첫 번째 매개변수로 받은 receiver 의 타입을 block 함수의 입력인 T.() 로 전달한다.
그러면 block 함수에서는 receiver 로 받은 객체에 this 를 사용하지 않고 직접 접근할 수 있다.
with 함수는 block 함수의 반환값을 그대로 반환한다.

공식 문서에 의하면, 람다 결과값을 제공하지 않고 컨텍스트 객체에서 함수를 호출하는 것이 좋다. 

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)

(3) with 함수를 사용하는 이유

with 함수는 이러한 경우에 주로 사용한다.

  • Non-Nullable 객체여야 할 때
  • 결과 값이 필요하지 않은 어떤 동작을 해야할 때

 

4. run 함수

(1) run 함수란?

어떤 값을 계산할 필요가 있거나, 여러개의 지역 변수의 범위를 제한하기 위해 사용하는 함수이다.

apply 함수와 유사하나, apply 함수는 객체를 생성함과 동시에 연속된 작업을 수행하고자 할 때 사용하고,
run 함수는 이미 생성된 객체의 메서드나 필드를 연속적으로 호출할 때 사용한다.

(2) run 함수를 사용하는 방법

run 함수의 선언은 다음과 같다.

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

 

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

(3) run 함수를 사용하는 이유

run 함수는 이러한 경우에 주로 사용한다.

  • 람다에 객체 초기화와 반환 값 계산이 모두 포함될 때
  • 이미 생성된 객체의 메서드나 필드를 연속적으로 호출할 때

 

5. also 함수

(1) also 함수란?

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

and also do the following

(2) also 함수를 사용하는 방법

also 함수의 선언은 다음과 같다.

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

 

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

(3) also 함수를 사용하는 이유

also 함수는 이러한 경우에 주로 사용한다.

  • 디버그 정보 로깅 또는 인쇄와 같이 오브젝트를 변경하지 않는 추가 조치에 사용

 

참고 사이트 >>>

코틀린 공식 doc : https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/#functions

코틀린 공식 doc (범위함수) : https://kotlinlang.org/docs/reference/scope-functions.html#scope-functions

https://tourspace.tistory.com/110

https://beomseok95.tistory.com/95

https://jepark-diary.tistory.com/69

https://black-jin0427.tistory.com/169

https://medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%98-apply-with-let-also-run-%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-4a517292df29

Comments