범위 지정 함수
1. 범위 지정 함수
특정 객체에 대한 작업을 블록 안에 넣어 실행할 수 있도록 하는 함수.블록은 특정 객체에 대해 할 작업의 범위가 됨. let, run, apply, also, with 등 5가지 기본적인 범위 지정 함수를 지원함. 범위 지정 함수는 수신 객체 지정 람다 함수라고 부름
2. apply
수신 객체 내부 프로퍼티를 변경한 다음 수신 객체 자체를 반환하기 위해 사용되는 함수. 객체 생성 시에 다양한 프로퍼티를 설정해야 하는 경우 사용됨. apply의 block은 람다식의 수신 객체로 apply의 수신 객체를 지정하기 때문에 람다식 내부에서 수신 객체에 대한 명시를 굳이~ 하지 않고 함수를 호출할 수 있게 됨.
코드 안에 보면 … 이게 맞는지는 모르겠지만 T.apply 형식으로 사용하며 내부의 block 람다식은 반환식이 Unit임. 근데 .apply 자체의 반환은 람다식을 모두 적용한 스스로의 객체가 반환됨을 알 수 있음. 그래서 type도 T고, 마지막에 return this를 사용함.
3. run
run은 apply와 같이 동작하지만! 수신 객체를 return하지 않고, run block의 마지막 줄을 Return하는 범위 지정 함수임. 수신 객체에 대해 특정한 동작을 수행한 후 결과값을 리턴 받아야 하는 경우 사용. 왜 마지막 줄이 Return 되냐면 block()이 람다식인데 return block()을 하면 람다식의 가장 마지막 표현식이 리턴되기 때문.
아래는 실습 예시로, Person data class의 함수로 isAdult()를 두었음. 그리고 Person 객체 p1에 범위 지정 함수 run을 사용해서 age 설정해주고 isAdult()를 실행해서 그 결과를 다른 변수에 저장하는 작업을 함. 그러니까 수신 객체에 대해 특정한 동작인 age 설정을 해주고 isAdult()에 대한 결과값을 리턴한 run의 사용 예시임.
data class Person(
var name: String = "",
var age: Int = 0
) {
fun isAdult(): Boolean {
if(this.age > 19)
return true
else
return false
}
}
fun main() {
val p1 = Person()
var isp1Adult = p1.run {
age = 13
isAdult()
}
println("p1 age 13 is Adult: $isp1Adult")
isp1Adult = p1.run {
age = 23
isAdult()
}
println("p1 age 23 is Adult: $isp1Adult")
}
4. run vs apply
data class Person(
var name: String = "",
var age: Int = 0
)
fun main() {
val p1 = Person()
val p2 = p1.run {
name = "dd"
age = 10
"23"
}
val p3 = p1.apply {
name = "ej"
age = 23
}
println("p2: ${p2.toString()}") //p2: 23
println("p1: ${p1.toString()}") //p1: Person(name=ej, age=23)
println("p3: ${p3.toString()}") //p3: Person(name=ej, age=23)
//p2는 p1.run을 수행한 결과인데 .run은 수신 객체를 반환하는게 아니고
//run 블록의 가장 마지막 라인을 반환하게 됨
//따라서 23이 반환됨. 만약 "23"이 없다면 kotlin.Unit을 반환하게 됨
p3.apply {
name = "not ej"
age = 13
}
println("p1: ${p1.toString()}") //p1: Person(name=not ej, age=13)
println("p3: ${p3.toString()}") //p3: Person(name=not ej, age=13)
//apply가 자기 자신 객체를 반환하므로 p3를 고치면 p1도 같은 객체를
//참조하므로 같이 변경된 결과를 확인할 수 있음
}
5. with
with는 수신 객체에 대한 작업 후 마지막 라인을 return함. run과 완전히 똑같이 동작함. 다른 점 하나는 run은 확장 함수로 사용되지만 with는 수신 객체를 파라미터로 받아 사용한다는 점임. run을 사용하는게 더 깔끔해서 with는 잘 사용되지 않음.

아래의 코드가 위의 2번에 있는 run example과 동일하게 작동하지만 가독성에 좋지 않다고 함…
data class Person(
var name: String = "",
var age: Int = 0
) {
fun isAdult(): Boolean {
if(this.age > 19)
return true
else
return false
}
}
fun main() {
val p1 = Person()
var isp1Adult = with(p1) {
age = 13
isAdult()
}
println("p1 age 13 is Adult: $isp1Adult")
isp1Adult = with(p1) {
age = 23
isAdult()
}
println("p1 age 23 is Adult: $isp1Adult")
}
6. let
수신 객체를 이용해 작업을 한 후 마지막 줄을 return 할 때 사용함. run, with와 달리 수신 객체를 접근할 때 it을 사용해야 한다는 점만 다르고 나머지는 같음. 하지만 실제 사용할 때 조금씩 다름.
블로그에서는 사용하는 경우가 다음과 같다고 명시해둠.
- null check 후 코드를 실행해야 하는 경우
- nullable한 수신 객체를 다른 타입의 변수로 변환해야 되는 경우
그러나 null check할 때 쓰지 말라는 다른 블로그 글들도 있음. 일단 null check해서 사용하는 예시는 아래와 같음. 그리고 null check할 때 쓰라는 블로그에서는 nullable한 값에는 let을 사용할 수는 있지만 되도록이면 run을 사용하라고 써놓음
data class Person(
var name: String = "",
var age: Int = 0
) {
fun isAdult(): Boolean {
if(this.age > 19)
return true
else
return false
}
}
fun main() {
var p1: Person? = null
val isp1Adult = p1?.let { it ->
it.isAdult()
}
println(isp1Adult.toString()) //null
}
7. also
also는 apply와 마찬가지로 수신 객체 자신을 반환함. apply가 프로퍼티 세팅을 한 후 객체 자체를 반환하는데만 사용된다면 also는 프로퍼티 세팅 + 객체에 대한 추가적인 작업을 한 후 객체를 반환하고 싶을 때 사용함.
also에서의 블락은 람다식의 파라미터로 (T)를 지정하므로 내부에서 수신 객체를 사용할 때는 it을 사용해야 함. 아래 also 구현부 코드를 보면 block 람다식의 파라미터로 T가 존재. 따라서 it을 사용. 사용하지 않고 내부 객체에 대해 접근하려고 하면 오류 발생!

p1은 copy를 사용해야 함. copy를 하면 바뀌지 않은 객체가 return 됨을 보장할 수 있음. 근데 also 잘 안 쓴다고 합니다…
8. 요약
this로 접근 : 왜냐 수신객체를 따로 람다식의 파라미터로 주지 않고 람다식의 수신객체로 설정하기 때문 |
it으로 접근 : 왜냐 람다식의 파라미터로 수신객체를 주기 때문 |
|
수신객체 자체가 리턴됨 | apply | also |
Block 마지막 표현식이 할당 | run with |
let |
📌 출처
[Kotlin] apply, run, with, let, also 차이 한 번에 정리하기
코틀린 의 apply, with, let, also, run 은 언제 사용하는가?