Backend/Kotlin

코틀린 with, apply, also, let, run

findmypiece 2021. 12. 23. 21:32
728x90

일단 모두 지정된 객체의 유효범위를 람다식 내부로 제한하는 기능을 한다. 즉 어떠한 객체를 특정 변수에 할당해서 활용한다고 생각하면 되는데 그 영역을 블록으로 제한한다고 생각하면 된다. 각각의 의미만 나열하면 오히려 어떤걸 활용해야 할 지 감이 안 올 수 있기 때문에 연관지어서 정리하도록 하겠다. 

 

제일먼저 with 는 가장 단순하다. 지정된 객체를 람다식 안에서 this 로 참조할 수 있고 객체를 수정할 수도 있으며 원하는 값을 리턴하거나 리턴하지 않을 수도 있다.

var map = mutableMapOf("A" to "aaa")

var map2 = with(map) {
    this["A"] = "bbb"
    this
}

var map3 = with(map) {
    this["A"] = "ccc"
}

println(map)
println(map2)
println(map3)

---
{A=ccc}
{A=ccc}
kotlin.Unit

 

 

apply, also 는 일단 무조건 수신객체를 리턴한다. 리턴값을 별도로 지정할 필요도 없다. 람다식 안에서 수신객체가 변경되었되면 변경값 값을 리턴한다. 단, with 와 다르게 apply, also 는 확장함수로 제공된다. 즉, 아래와 같다.

var map = mutableMapOf("A" to "aaa")

var map2 = map.apply {
    this["A"] = "bbb"
}

var map3 = map.also {
    it["A"] = "ccc"
}

println(map)
println(map2)
println(map3)

 

다른점이 있다면 수신객체의 alias를 apply 는 this 로 사용하고 also 는 it 로 사용한다는 것인데 also는 아래와 같이 이 alias 를 이름을 원하는 이름으로 변경할 수 있지만 apply 는 변경할 수 없다.

var map3 = map.also { test ->
    test["A"] = "ccc"
}

 

run 은 apply, also 와 달리 리턴값을 수신 객체 외에 다른 값으로 자유롭게 지정할 수 있다(unit 도 가능)

var map = mutableMapOf("A" to "aaa")

var map2 = map.run {
    this["A"] = "bbb"
    this
}

var str = map.run {
    this["A"] = "bbb"
    "test"
}

var unit = map.run {
    this["A"] = "bbb"
}

println(map2)
println(str)
println(unit)

 

그럼 마지막으로 let 는 뭔가? 기본적으로 run 동일한 기능을 하는데 apply, also 를 구분하던 것과 마찬가지로 run은 수신객체 alias를 무조건 this 로 사용해야 하고 let는 it 외에 다른 것도 자유롭게 지정할 수 있다.

 

적절히 사용하겠지만 가장 많이 볼 수 있는 예는 let 이다. 이는 수신객체가 null 아닐경우 수행할 로직을 람다식으로 정의한다. 수신객체가 null 일 경우 아무것도 수행하지 않는다. 일반적으로 아래와 같이 사용한다. apply, also 와 달리 수신객체가 아니라 원하는 값을 리턴할 수 있다.(unit 도 가능)

val str: String? = null

str?.let {
    println(it.reversed())
}

그런데 만약 이와 반대로 수신객체가 null 일 경우 처리할 로직은 어떻게 정의해야 할까? if(str == null) 과 같은 조건문을 사용해야 할까? 그렇지 않다. 아래와 같이 엘비스 연산자와 run 을 활용하면 더 깔끔하다.

val str: String? = null

str?:run{
    println("str is null")
}

아래와 같이 let 과 엘비스연산자+run 을 함께 조합하면 null 이 아닌 경우와 null 경우를 한번에 처리할 수 있다.

val str: String? = null

str?.let {
    println(it.reversed())
}?:run{
    println("str is null")
}

그런데 사실 엘비스연산자+run 조합은 let 없이 단독으로 쓰이면 오버스팩에 가깝다. 수신객체가 null 이기 때문에 람다식 안에서 수신객체가 활용될 일이 없는데 굳이 수신객체를 전달받는 람다식을 쓸 이유가 없다. 이 경우 if(str == null) 로 끝내는 것이 오히려 깔끔하다.

 

728x90