[Kotlin] 범위 지정 함수 (Scope Funciton) let, with, run, apply, also

Sio·2023년 1월 8일
1

Scope Function

코틀린은 범위 지정 함수가 존재합니다.

대상 객체에 대해 범위 지정 함수를 호출하면 임시 scope(* 여기선 block 입니다.)가 형성되는데, 이 scope 내에서 객체에 대해 할 작업을 정의할 수 있습니다.

특정 객체에 대한 작업을 block 내에 표현하게 되면 가독성이 증가하게 되고 코드가 훨씬 깔끔해진다는 장점이 있습니다.

범위 지정 함수의 종류로는
let, with, run, apply, also 5가지를 지원합니다.


수신객체 지정 람다

범위 지정 함수를 '수신객체 지정 람다 (함수)' 라고도 부릅니다.
* 확장함수를 실행하는 주체를 수신객체라고 합니다.

수신객체를 명시하지 않거나, it(익명)으로 호출하는 것만으도 람다 내부에서 수신객체의 메소드를 호출할 수 있도록 해주기 때문입니다.

실제 함수를 살펴봅시다.

also 에서의 block은 수신객체 T를 람다의 파라미터로 명시적으로 전달합니다.

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

apply 에서의 block은 수신객체 T를 receiver로 암시적으로 전달합니다.

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

함수들을 수신객체 전달 및 반환 값 기준으로 분류해보면 다음과 같습니다.


apply

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

apply를 활용하면 this를 통해 접근이 가능합니다.
보통 객체를 선언과 동시에 초기화 해줄 때 사용합니다.✨

val person = Person().apply {
	name = "Sio"
    age = 25
    temperature = 36.2f
}

만약 apply를 쓰지 않는다면, 프로퍼티를 설정할 때 person을 매번 호출해야 할 것입니다.

val person = Person()
perosn.name = "Sio"
person.age = 25
person.temperature = 36.2f

run

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

run은 apply와 똑같이 동작하지만 수신 객체 자체를 리턴하지 않고, run 블록의 마지막 라인을 return 합니다.
보통 수신객체에 대한 특정 동작을 수행한 후 결과 값을 리턴 받을 때 사용합니다.✨

예를들어, Person 객체의 체온을 체크해서 37.5 이상이면 아픈것으로(sick) 체크한다고 해봅시다.

data class Person(
	var name: String = "",
    var age: Int = 1,
    var temperature: Float: 36.5f
) {
	fun isSick(): Boolean = temperature > 37.5f
}
fun main() {
	val person = Person(name = "Sio", age = 25, temperature = 36.2f)
    val isPersonSick = person.run {
    	temperature = 37.8f
        isSick()
    }
    
    println("isPersonSick : $isPersonSick")
}

// 실행 결과
isPersonSick : true

📌 run은 수신객체 없이도 블록을 열어 동작할 수 있습니다만, run을 사용하면 내부에 수신객체를 명시해주어야 합니다.

val person = Person("Sio", 25, 36.2f)
val isPersonSick = run {
	person.temperature = 37.8f
    person.isSick()
}

with

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

with는 수신객체에 대한 작업 후 마지막 라인을 return 하며, run과 완전히 똑같이 동작합니다.

다른점은, run은 확장함수로 사용되지만, with는 수신객체를 파라미터로 받아 사용한다는 점 입니다.

fun main() {
	val person = Person("Sio", 26, 36.2f)
    val isPersonSick = with(person) {
    	temperature = 37.8f
        isSick()
    }

    println("isPersonSick : $isPersonSick")
}

// 실행 결과
isPersonSick : true

let

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

let은 수신객체를 이용하여 작업을 하고 마지막 줄을 return 할 때 사용합니다.
run이나 with와 비슷하지만, 수신객체에 접근할 때 it을 사용해야 한다는 점이 다릅니다.

null check 후 코드를 실행해야 할 때
nullable한 수신객체를 다른 타입의 변수로 변환해야 할 때

즉, nullalbe한 값을 처리할 때 let을 주로 사용합니다.✨

예를들어, 사람이 null이 아닐 때만 아픈지 체크해야 한다고 해봅시다.

fun main() {
	var person: Person? = null
    val isPersonSick = person?.let { it: Person -> 
    	checkPersonIsSick(it)
    }
}

null check 후 person이 null이 아닐 때만 블록 내부의 코드가 실행됩니다.


also

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

also는 apply와 마찬가지로 수신객체 자신을 반환합니다.

apply가 프로퍼티 셋팅 후 객체 자체를 반환할 때 사용된다면,
also는 프로퍼티 셋팅 뿐 아니라 객체에 대한 추가적인 작업을 한 후 객체를 반환할 때 사용합니다.✨

  1. number를 return한 후 해당 number를 증가시키고 싶을 때
var number = 3

fun getAndIncreaseNumber() = number.also {
	number++
}

fun main() {
	println("first number ${getAndIncreaseNumber()}")
    println("second number ${getAndIncreaseNumber()}")
}

// 실행 결과
first number 3
second number 4

저는 처음에, number를 증가시키고 -> 해당 값이 반환된다고 생각해서 4,5 를 예상했으나 also는 number를 리턴한 후 값을 증가시킵니다.

  1. 객체를 사용할 때 주소값을 return하는 것입니다. 따라서, 객체의 프로퍼티가 바뀌면 also에서 return하는 객체의 프로퍼티 또한 바뀌게 됩니다.
var person = Person("Sio", 25, 36.2f)

fun getAndIncreaseAge() = person.also {
	person.age = it.age + 1
}

fun main() {
	println("${getAndIncreaseAge()}")
    println("${getAndIncreaseAge()}")
}

// 실행 결과
Person(name=Sio, age=26, 36.2)
Person(name=Sio, age=27, 36.2)
  1. 따라서 copy를 사용해야 프로퍼티가 바뀌지 않은 본래의 객체가 리턴됩니다.
var person = Person("Sio", 25, 36.2f)

fun getAndIncreaseAge() = person.also {
	person = person.copy(age = it.age + 1)
}

fun main() {
	println("${getAndIncreaseAge()}")
    println("${getAndIncreaseAge()}")
}

// 실행 결과
Person(name=Sio, age=25, 36.2)
Person(name=Sio, age=26, 36.2)

이러한 문제때문에 also는 거의 사용되지 않고, 프로퍼티를 바꾸지 않으며 동작을 추가적으로 할 때(로깅) 등에 사용됩니다.

profile
나는 시오

0개의 댓글