[Kotlin] Scope function

chaeny·2024년 12월 20일

apply

호출된 객체를 this로 받아, 람다 블록 안에서 객체의 속성이나 메서드에 접근해 작업을 수행하고 객체 자체를 반환한다

`this` 값을 수신자로 하여 지정된 함수 [block]을 호출하고 `this` 값을 반환합니다.

inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this // 호출된 객체 자체 반환
}


fun Int.apply(block: Int.() -> Unit): Int {
    block() 
    return this
}


val result = 5.apply {
    println("number: $this")
}
println(result) 
// Int는 불변 객체(immutable)이므로, apply 내부에서 값을 변경할 수 없다


val result = mutableListOf(1, 2, 3).apply {
    add(4)
    remove(1) // 리스트를 수정하지만, 반환값은 Unit임
    println("Updated list: $this")
}
println(result) // [2, 3, 4] 
// mutableListOf(1, 2, 3)를 수정하고 호출된 객체 자체를 반환함

Unit : apply는 람다 블록이 아무 값도 반환하지 않아도 괜찮도록 설계되었다
람다 블록의 반환값은 무시되며, apply는 객체 자체(this)를 반환한다.

  • 람다 블록에서 this를 생략하고 객체 속성이나 메서드에 접근할 수 있다
  • 블록 내에서 어떤 작업을 하더라도, 최종적으로 호출된 객체 그 자체를 반환한다
  • 객체 초기화에 적합: 새 객체를 생성한 후, 속성을 설정하거나 메서드를 호출한 뒤 해당 객체를 반환하는 데 유용하다.
val person = Person().apply {
    name = "Alice"
    age = 30
}
println(person) // Person(name=Alice, age=30)
객체 생성 후 속성을 한 줄로 설정 가능.
apply 블록을 통해 객체 속성을 초기화하고, 최종적으로 해당 객체를 반환함

also

호출된 객체를 it으로 전달하여 람다 블록에서 부수 작업을 수행한 후, 객체 자체를 반환한다
주된 목적은 부수 작업을 수행하는 것이다

`this` 값을 인수로 사용하여 지정된 함수 [block]을 호출하고 `this` 값을 반환합니다.

inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this // 호출된 객체 자체 반환
}


fun Int.also(block: (Int) -> Unit): Int {
    block(this) // 호출된 객체(현재 Int)를 it으로 람다 블록에 전달함
    return this
}


val number = 10.also {
    println("number : $it")
}


val myList = mutableListOf(1, 2, 3).also {
    it.add(4)
    println("Updated list: $it")
}
println(myList) // [1, 2, 3, 4]

apply, also 차이

apply

  • 수신 객체(this)를 수정하는 데 초점
  • 객체의 속성 수정이나 초기화 작업에 적합
  • 객체를 직접 설정하거나 구성해야 할 때 사용

also

  • 부수 작업에 초점
  • 객체를 변경하지 않고, 로깅, 디버깅, 유효성 검사, 통계 기록 등 추가 작업을 수행
  • 객체를 그대로 반환하면서 부가적인 작업을 하려는 경우 사용
val userName = "John".also {
    println("User name: $it")
}
println(userName)
// apply를 사용하면 this로 접근해야 하고, 속성을 설정하거나 초기화하려는 느낌을 줄 수 있어 
// 부수 작업의 의도를 명확히 전달하지 못함


val myList = mutableListOf(1, 2, 3)
    .also { println("Before: $it") }
    .apply { add(4) }
println(myList) // [1, 2, 3, 4]
// apply만 사용하면 로깅과 수정을 혼합하게 되어 의도가 불분명


val number = 42.also {
    require(it > 0) { "Number must be positive" }
}
println(number) 
// apply를 사용하면 속성을 설정하려는 의도가 아닌데도 불구하고 this로 접근해야 함

let

어떤 객체를 잠시 빌려서 원하는 작업을 하고, 결과를 돌려주는 함수

`this` 값을 인수로 사용하여 지정된 함수 [block]을 호출하고 결과를 반환합니다.

inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}


fun let(block: (Int) -> Int): Int {
    return block(this) // this는 let을 호출한 객체
}


val number = 5
val result = number.let { it * 2 }
println(result)

// block 함수 : { it * 2 }
// this : number
// block(this) : 5 * 2

위 예제에서는 let이 빌린 객체는 number다.

  • 안전한 널 처리: ?.let { ... }를 사용하여 객체가 null이 아닐 때만 작업 수행.
  • 코드 간결화 : 여러 작업을 체이닝하거나 한 번에 처리
  • 스코프 제한 : 불필요한 임시 변수(it)를 블록 안으로 가둠
  • 가독성 향상 : 객체를 람다식 안에서 간단히 처리

run

호출된 객체를 this로 사용하여 작업을 수행하고, 그 결과를 반환하는 함수

지정된 함수 [block]을 호출하고 결과를 반환합니다.

inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}


fun Int.run(block: Int.() -> String): String {
    return block()
}


val result = 10.run {
    "The number is $this" // this는 10
}
println(result)
  • 객체 작업 후 특정 값을 반환해야 할 때 사용
  • 객체가 null일 수 있는 상황에서 안전하게 작업을 처리
  • 여러 작업을 그룹화할 때 사용
val person = Person("Alice", 25)
val info = person.run {
    println("Person name: $name")
    println("Person age: $age")
    "$name is $age years old"
}
println(info) // Alice is 25 years old

let, run 차이

run

  • this로 객체에 접근하여, 객체와 관련된 여러 작업을 묶어 결과 반환
  • 주로 객체 초기화나 설정 작업 후 결과 반환 시 사용

let

  • 객체를 임시 변수(it)처럼 사용하여 작업 후 결과 반환
  • 주로 null 안전성 처리 또는 객체 변환 시 사용

run, apply 차이

객체를 초기화하거나 작업을 수행할 때 사용하는데 둘이 차이점은 반환값이다.
그렇다면 각각의 사용목적은 뭘까?

run

객체 자체가 아니라 결과 메시지를 사용하려는 의도

val message = Character("Alice", 25).run {
    "$name, age $age, is ready for the game!"
}
println(message) // Alice, age 25, is ready for the game!

apply

초기화된 객체 자체가 중요. 캐릭터를 설정한 뒤, 그 객체를 다음 작업에서 사용할 것

val character = Character().apply {
    name = "Alice"
    age = 25
}
println(character) // Character(name=Alice, age=25)

With

객체를 전달받아 여러 작업을 묶어서 실행하고, (최종) 결과를 반환하는 함수
매개변수로 객체를 전달받는다

지정된 [receiver]를 수신자로 하여 지정된 함수 [block]을 호출하고 결과를 반환합니다.

inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}


fun with(receiver: Int, block: Int.() -> String): String {
    return receiver.block()
}


val result = with(10) {
    "The number is $this"
}
println(result) // The number is 10

// 10이 receiver로 전달
// this는 10

여러 UI 속성을 설정하는 작업을 하나로 묶어 처리할 때 사용

val button = Button(context)
with (button) {
    text = "Click Me"
    textSize = 16f
    setOnClickListener { println("Button clicked!") }
}

run, with 차이

작업 그룹화를 지원하는 것은 동일한데 각각의 사용목적은 뭘까?

run

  • 호출된 객체에서 직접 호출되므로, 체이닝 작업에서 사용
  • ?.run { ... } 형태로 nullable 객체를 안전하게 처리

with

  • with는 매개변수로 객체를 전달하므로, 코드에서 작업의 대상이 명확해진다
  • 객체를 수정하거나 설정할 때, 매개변수로 전달된 객체라는 의도가 더 잘 드러난다

객체 작업 그룹화에 초점이 맞춰져 있다면, with가 더 나은 선택이다

정리

apply랑 also는 객체 자체를 반환하고 also는 임시
run이랑 let은 결과를 반환하고 let은 임시
with은 작업을 묶어서 수행하고 결과를 반환
null 타입 검토는 run이랑 let만

apply와 also : 객체 자체를 반환
apply : 객체를 설정하거나 초기화한 후 반환. (this 사용)
also : 부수 작업(로깅, 디버깅 등)을 수행한 후 반환. (it 사용, 임시 작업에 적합)

run과 let : 작업 결과를 반환
run : 객체와 관련된 작업을 그룹화하고, 결과를 반환. (this 사용)
let : 임시로 객체를 사용하고, 결과를 반환. (it 사용, 임시 작업에 적합)

with : 작업을 묶어서 수행하고 결과를 반환
객체를 매개변수로 전달받아 작업을 묶어서 처리하고 결과를 반환. (this 사용)

run과 let만 null 타입 검토 지원
apply, also, with는 호출된 객체가 null일 경우 사용 불가

0개의 댓글