Scope Function

sumi Yoo·2022년 10월 9일
0

스코프 함수란?

코틀린은 기본적으로 표준 스코프 함수 라는 것을 제공한다.
스코프 함수는 특정 객체의 컨텍스트 내에서 특정 동작 (프로퍼티 초기화, 활용 등) 을 실행하기 위한 목적만을 가진 함수다. 람다 식을 사용하여 객체에 이러한 함수들을 호출하면 임시적으로 스코프(범위)가 설정된다. 이 범위에서는 해당 객체의 이름 없이 접근할 수 있다.

총 5가지의 함수가 존재한다.

let run with apply also

이 5개의 함수는 기본적으로 같은 일을 한다.

객체에 붙어 있는 코드 블록을 실행
그렇다면 이 5개의 함수의 차이점은 무엇일까?

해당 객체를 어떻게 코드 블록 내에서 사용 가능하게 만드는가?
전체 표현식의 결과가 무엇인가?

5가지 함수의 정의는 아래와 같다.

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

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

inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

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

run

  • run 함수는 확장 함수이기 때문에 context object를 receiver(this)(: 수신객체)로 이용할 수 있다.
  • run 함수는 반환 결과가 람다의 결과이다.
  • 객체의 초기화와 리턴 값의 계산을 람다가 포함할 때 유용하다.
  • 확장함수이기 때문에 safe call(.?)을 붙여 non-null 일 때에만 실행할 수 있다.
  • 주로 어떤 값을 계산할 필요가 있거나 여러 개의 지역변수 범위를 제한할 때 사용한다.
data class Member(val name: String, var age: Int)

val member = Member("Wangi", 26)
val nextYearHisAge = member.run {
    ++age               // this.age
}

println(nextYearHisAge) // 27

이미 만들어진 인스턴스의 값 혹은 그를 이용한 특정 계산 결과를 필요로 하는 경우, run을 활용해 이를 반환 받아볼 수 있다.

위의 run과 다른 형태의 run도 존재한다.

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

이 run은 확장 함수도 아니고, 블록에 입력값도 없다. 따라서 객체를 전달받아서 속성을 변경하는 형식에 사용되지 않는다.

이 함수는 단지 어떤 객체를 생성하여 명령문을 블록 안에 적음으로써 가독성을 높이는 역할을 한다.

val member = run {
    val name = "Wangi"
    val age = 26
    Member(name, age)
}

with

  • with 함수는 확장 함수가 아니기 때문에 context object를 argument로 전달한다. 그러나, 람다의 내부에는 확장함수로 적용되어서 this로 사용가능하다.
  • 반환 결과가 람다의 결과이다.
  • 수신 객체는 non-nullable이고, 결과가 필요하지 않은 경우에 유용하다.
val member = Member("Wangi", 26)
with(member) {
    println("This member name is $name") // this.name
    println("This member age is $age")   // this.age
}

apply

  • apply 함수는 확장 함수이기 때문에 context object를 receiver(this)로 이용할 수 있다.
  • 반환 결과가 객체 자신이다. Builder 패턴과 동일한 용도로 사용된다.
  • 객체의 프로퍼티 만을 사용하는 경우가 많으며, 대표적인 사례는 객체의 초기화이다.
val member = Member("Wangi").apply{
    age = 26                // this.age
}

println(member)             // Member(name=Wangi, age=26)

인스턴스를 새로 생성하고 특정 변수에 할당하기 전에 초기화 작업을 해줄 수 있는 스코프를 만들어 준다.
따라서 apply 함수 내의 모든 명령이 수행되고 나면 명령들이 적용되어 새로 생성된 인스턴스를 반환한다는 특징을 갖고 있다.

also

  • also 함수는 확장 함수이기 때문에 context object를 receiver(this)로 전달한다. 그러나, 코드 블럭 내에서 this를 파라미터로 입력하기 때문에 it을 사용해 프로퍼티에 접근할 수 있다.
  • also 함수는 반환 결과가 객체 자신이다. Builder 패턴과 동일한 용도로 사용된다.
  • also 함수는 객체의 속성을 전혀 사용하지 않거나 변경하지 않고 사용하는 경우에 유용하다. 예를 들면, 객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 적합하다.
class Membership(member: Member) {
    val member = member.also {
        requireNotNull(it.age)
        println(it.name)
    }
}

also를 사용하지 않는 동일한 코드는 아래와 같다.

class Membership(val member: Member) {
    init {
        requireNotNull(member.age)
        println(member.name)
    }
}

let

  • let 함수는 확장 함수이기 때문에 context object를 receiver(this)로 전달한다. 그러나, 코드 블럭 내에서 this를 파라미터로 입력하기 때문에 it을 사용해 프로퍼티에 접근할 수 있다.
  • let 함수는 반환 결과가 람다의 결과이다.
  • let 함수는 지정된 값이 null이 아닌 경우에 코드를 실행해야 하는 경우, Nullable 객체를 다른 Nullable 객체로 변환하는 경우, 단일 지역 변수의 범위를 제한하는 경우에 유용하다.
getMember()?.let {
                    // null이 아닐때만 실행
    println(it)     // it: member
}

val length = str?.let {
    println("this str is not null")
    it.length
} ?: 0

// str이 "Wangi"일 경우 length = 5
// str이 null일 경우 length = 0

apply/run vs also/let

fun main() {
    val price = 99999999

    var a = Book("해로의 모험", 1000).apply {
        name = "[폭탄세일중]" + name
        discount()
    }

    a.run {
        println("상품명 : ${name}, 가격 : ${price}")
    }
}

상품명 : [폭탄세일중]해로의 모험, 가격 : 99999999
main() 스코프 내에 인스턴스 프로퍼티와 이름이 같은 변수가 있어서 이를 출력해버렸다.
run 에서 상위 스코프인 main() 스코프의 동명의 변수를 참조한 것이다.

also와 let은 이와 같은 혼란을 방지하기 위해서 it 이라는 키워드를 제공해준다.

class Book(var name: String, var price: Int) {
    fun discount() {
        price -= 200
    }
}
fun main() {
    val price = 99999999

    var a = Book("해로의 모험", 1000).apply {
        name = "[폭탄세일중]" + name
        discount()
    }

    a.let {
        println("상품명 : ${it.name}, 가격 : ${it.price}")
    }
}

상품명 : [폭탄세일중]해로의 모험, 가격 : 800
main() 스코프의 price가 아닌 a 인스턴스의 프로퍼티를 출력하게 되었다.
it 키워드에 참조 연산자를 통해 프로퍼티 및 함수를 접근하면 된다.

위의 스코프 함수들은 새로운 기술이 아니라, 코드들을 더욱 간결하고 가독성 좋게 하자는 의도로 사용한다.

https://mycool0905.github.io/kotlin/2020/12/15/kotlin-scope-function.html
https://haero.tistory.com/21

0개의 댓글

관련 채용 정보