본문 바로가기
CS/Kotlin

[공식문서 + 구글링] 코틀린 let, with, run, apply, also 차이 비교 정리

by upswp 2022. 3. 4.
안녕하세요 Samuel 입니다.

 

let, with, run, apply, also

코틀린에는 이렇게 생긴 확장함수가 있습니다. 객체를 사용할때 명령문을 블럭 { } 으로 묶어서 간결하게 사용할 수 있게 해주는 함수들입니다. 문제는 서로 비슷비슷해서 뭘 쓰든 어떻게든 동작하는 점이 어려워 이번에 한번 정리를 해보며 그 개념을 다지기로 했습니다.


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

let

컨텍스트 객체를 인수(it)로 사용할 수 있습니다. 반환값은 람다 결과입니다.

let은 하나 이상의 함수를 호출하는데 사용할 수 있습니다. 예를들어 다음코드를 보게 되면

val numvers = mutableListOF("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
pritnln(resultList)

>>> [5, 4, 4]

let을 사용할 때

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

코드 블록에 단일 함수가 인수로 포함된 경우, 람다 대신 메서드 참조(::)를 사용할 수 있습니다.

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

let은 종종 값이 아닌 코드블록만 실행하는데 사용됩니다.람다 안에서 let에 의해 불려지는 it은 Null이 아닌 객체에 대해 작업을 수행하려면 safe call operator인 ?. 를 사용합니다.

fun processNonNullString(str: String) {}

fun main() {
    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
    }
}

>>> let() called on Hello

let을 사용하는 또 다른 예는 코드 가독성을 개선하기 위해 제한된 범위의 지역 변수를 도입해서 쓰는겁니다. 컨텍스트 객체에 대한 새 변수를 정의하려면 기본값인 it 대신 사용할 수 있도록 해당 이름을 람다 인수로 제공합니다.

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

>>>
The first item of the list is 'one'
First item after modifications: '!ONE!'

with

비확장 함수 : 컨텍스트 객체가 인수로 전달되지만 람다 내부에서는 receiver(this)를 사용할 수 있습니다.

반환값은 람다 결과입니다.

람다 결과를 제공하지 않고 컨텍스트 객체의 함수를 호출하려면 'with'를 사용하는 것이 좋습니다. 코드에서 'with'는 "이 개체와 함께, 다음을 수행하십시오."로 읽을 수 있습니다.

fun main () {
		val numbers = mutableListOf("one", "two", "three")
		with(numbers) {
			println("'with' is called with argument $this")
			println("It contains $size elements")
		}
}

>>>
'with' is called with argument [one, two, three]
It contains 3 elements

with의 또 다른 사용 사례는 속성이나 함수가 값 계산에 사용될 컨텍스트 객체를 도입하는 것입니다.

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    val firstAndLast = with(numbers) {
        "The first element is ${first()}," +
        " the last element is ${last()}"
    }
    println(firstAndLast)
}

>>>
The first element is one, the last element is three

run

컨텍스트 객체는 receiver(this)로 사용할 수 있습니다. 반환값은 람다의 결과입니다.

‘run’은 with와 같으나 컨텍스트 객체의 확장함수로서 ‘let’을 호출합니다.

run은 람다에 객체 초기화 및 반환값 계산이 모두 포함되어 있을 때 유용합니다.

class MultiportService(var url:String, var port: Int) {
		fun prepareRequest(): String = "Default request"
		fun query(request: String): String = "Result for query '$request'"
}
fun main () {
		val service = MultiportService("<https://example.kotlinlang.org>", 80)

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

		val letResult = service.let{
				it.port = 8080
				it.query(it.prepareRequest() + "to port ${it.port}")
		}
		println(result)
		println(letResult)
}

>>>
Result for query 'Default request to port 8080'
Result for query 'Default request to port 8080'

receiver 객체에 대해 run을 호출하는 것 외에도 비확장 함수로 사용할 수 있습니다. 비확장 run을 사용하면 식이 필요한 경우 여러개의 블록을 실행할 수 있습니다.

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

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

    for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
        println(match.value)
    }
}

>>>
+123
-FFFF
88

apply

컨텍스트 객체는 receiver(this)로 사용할 수 있습니다. 반환 값은 객체 자체입니다.

값을 반환하지 않고 주로 receiver 객체의 멤버에서 작동하는 코드블록에는 apply를 사용합니다. apply의 일반적인 경우는 객체 구성입니다. 이러한 호출은 “다음 할당을 객체에 적용"하는것으로 읽을 수 있습니다.

data class Person(var name: String, var age: Int = 0, var city: String ="")

fun main() {
		val samuel = Person("samuel").apply {
				age = 30
				city = "Korea"
		}
		println(samuel)
}

>>>
Person(name=Samuel, age=30, city=Korea)

receiver를 반환값으로 사용하면 보다 복잡한 처리를 위해 콜 체인에 apply를 쉽게 포함할 수 있습니다.

also

컨텍스트 객체를 인수(it)으로 사용할 수 있습니다. 반환값은 객체 자체입니다.

또한 also는 컨텍스트 객체를 인수로 받아들이는 일부 동작을 수행하는데 유용합니다.

속성과 기능 대신 객체에 대한 참조가 필요한 작업이나 외부 범위에서 this 참조를 음영으로 표시하지 않으려면 also를 사용하십시오.

코드에서 also가 보이면 “also는 객체에 대해 다음작업을 수행할 수 있습니다”로 읽을 수 있습니다.

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

>>>
The list elements before adding new one: [one, two, three]

공식문서로는 100% 다 이해가 잘 되지 않아서 구글링을 통해서 추가적인 내용을 찾아봤습니다.

일반적인 적용

일반적으로 객체를 변경하는 방법은 아래와 같습니다.

data class Person(var name: String, var age: Int)

val person = Person("",0)
person.name = "Samuel"
person.age = 30
println("$person")

>>>
Person(name=Samuel,age=30)

let

fun <T,R> T.let(block: (T) -> R): R
val nameStr = person?.let { it.name } ?: "Defalut name"
val person = Person("", 0)
val resultIt = person.let {
    it.name = "James"
    it.age = 56
    it // (T)->R 부분에서의 R에 해당하는 반환값.
}

val resultStr = person.let {
    it.name = "Steve"
    it.age = 59
    "{$name is $age}" // (T)->R 부분에서의 R에 해당하는 반환값.
}

val resultUnit = person.let {
    it.name = "Joe"
    it.age = 63
    // (T)->R 부분에서의 R에 해당하는 반환값 없음
}

println("$resultIt")
println("$resultStr")
println("$resultUnit")

>>>
Person(name=James, age=56)
Steve is 59
kotlin.Unit

with

fun <T, R> with(receiver: T, block: T.() -> R): R
val person = Person("James", 56)
with(person) {
    println(name)
    println(age)
    //자기자신을 반환해야 하는 경우 it이 아닌 this를 사용한다
}

>>>
James
56

run

fun <T, R> T.run(block: T.() -> R): R
val person = Person("James", 56)
val ageNextYear = person.run {
    ++age
}

println("$ageNextYear")

>>> 57
fun <R> run(block: () -> R): R
val person = run {
    val name = "James"
    val age = 56
    Person(name, age)
}

apply

fun <T> T.apply(block: T.() -> Unit): T
val person = Person("", 0)
val result = person.apply {
    name = "James"
    age = 56
}

println("$person")

>>> 
Person(name=James, age=56)

also

fun <T> T.also(block: (T) -> Unit): T
val person = Person("", 0)
val result = person.also {
    it.name = "James"
    it.age = 56
}

println("$person")

>>>
Person(name=James, age=56)

[Kotlin] 코틀린 let, with, run, apply, also 차이 비교 정리

'CS > Kotlin' 카테고리의 다른 글

변수와 자료형, 연산자  (0) 2022.03.17
함수형 프로그래밍의 특징은?  (0) 2022.02.18