Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

DOing

[kotlin] Scope functions 정리 (let, also, apply, run, with) 본문

Java & Kotlin

[kotlin] Scope functions 정리 (let, also, apply, run, with)

mangdo 2021. 12. 25. 23:48

  자바에는 없지만, 코틀린에서는 자주 사용되는 함수들로 let, also, apply, run, with이 있다. 이들은 모두 범위 지정함수들이다. 서로 유사한 점이 많아서 무엇을 골라 사용해야하는 것인지 헷갈릴때가 많았다. 이번 포스팅에서는 이들의 차이를 알아보고자 한다.


수신 객체 지정 람다 (Lamdas with Receivers) vs 일반 람다

  Scope functions 을 분석하기 전에 다시 짚고 넘어가야하는 개념이다.

  확장함수에서 수신객체를 사용하여 블록내로 객체를 넘겼듯이 수신객체 지정 람다도 수신객체를 사용하여 객체를 전달한다. 따라서 수신객체 지정람다에서는 수신객체를 this로 대신할 수 있으며, this를 생략할수도 있다. 하지만 this를 생략하게되면 수신객체인지 외부의 객체 혹은 함수인지 헷갈리게 될 수 있다. 그래서 this는 주로 객체 멤버(함수 혹은 프로퍼티)에 대해 작동하는 람다에만 사용할 것을 권장한다.

 일반 람다는 인자로 객체를 전달한다. 람다의 매개변수가 하나뿐이고 컴파일러가 타입 추론이 가능하다면 객체는 기본 매개변수인 it으로 받을 수 있다.

수신 객체 지정 람다    :    T.() -> R
일반 람다                  :    (T) -> R

 

Scope function?

  코틀린 표준 라이브러리에는 객체에 대한 코드 블록을 실행하는 것이 유일한 목적인 함수들이 있다. 제공된 람다 식을 사용하여 객체에 이러한 함수들을 호출하면 임시적으로 Scope(범위)가 설정된다. 이 범위에서는 해당 객체의 이름 없이 접근할 수 있다. Scope function은 새로운 기술 기능을 도입하지 않지만 코드를 더 간결하고 읽기 쉽게 만든다. 하지만 남용하게 되면 오히려 코드 가독성이 떨어지고 오류가 발생할 수 있다. 중첩 범위 함수를 피하고 연결시 주의해야한다. 현재의 Context object와 this와 it의 값을 혼동하기 쉽기 때문이다.

 

 

Scope function 비교

  Scope function은 letrunwithapply, also가 있는데 이들은 사실 비슷하기 때문에 선택하는 것이 까다로울 수 있다. 선택은 주로 개발자의 의도와 프로젝트 사용의 일관성에 달려있다.

 

Function Object reference
(객체 전달 방법)
반환 값 확장함수 여부
let it 람다의 실행값 o
run this 람다의 실행값 o
run x 람다의 실행값 x
(객체없이 호출하기도 함)
with this 람다의 실행값 x
apply this Context object o
also it Context object o

 

[참고] 코틀린 공식 문서에서 제안한 각 함수의 사용 목적

   - null이 아닌 객체에서 람다 실행: let

   - 지역 범위에서 변수로 표현식 사용 : let

   - 객체 초기화 : apply

   - 객체 초기화 및 결과 계산 : run

   - 표현식이 필요한 명령문 실행 : run (non-extension)

   - 추가 효과 : also

   - 객체에 대한 그룹화 함수 호출 : with

 


1. run

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • run 함수는 확장 함수이기 때문에 context object를 receiver(this)로 이용할 수 있다.
  • run 함수는 반환 결과가 람다의 결과이다.
  • run 함수는 객체의 초기화와 리턴 값의 계산을 람다가 포함할 때 유용하다.
  • 주로 어떤 값을 계산할 필요가 있거나 여러 개의 지역변수 범위를 제한할 때 사용한다.
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// 같은 코드를 let()을 사용하면 :
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

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

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

Non-extension run()은 확장 함수도 아니고, 블록에 입력값도 없다.

단지 표현식이 필요한 여러 명령문의 블록을 실행할 수 있게한다.

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

 

2. with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
  • with()는 확장 함수가 아니기 때문에 context object를 argument로 전달한다. 그러나, 람다의 내부에는 확장함수로 적용되어서 this로 사용가능하다.
  • with()는 반환 결과가 람다의 결과이다.
  • with()는 수신 객체는 non-nullable이다.
  • with()는 람다의 결과가 필요하지 않고 객체에 대한 함수를 호출해야할 때 사용할 것을 권장한다. 코드에서 with 는 " with this object, do the following"이라고 읽으면 된다.
val member = Member("Luna", 24)
with(member) {
    println("This member name is $name") // this.name
    println("This member age is $age")   // this.age
}

 

3. apply

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • apply()는 확장 함수이기 때문에 context object를 receiver(this)로 이용할 수 있다.
  • apply()는 반환 결과가 객체 자신이다. Builder 패턴과 동일한 용도로 사용된다.
  • apply()는 주로 값을 반환하지 않고 객체의 프로퍼티 만을 사용하며, 대표적인 사례는 객체의 초기화이다.
  • 코드에서 apply는 "apply the following assignments to the object."라고 읽을 수 있다.
val member = Member("Luna").apply{
    age = 24                // this.age
}

println(member)             // Member(name=Luna, age=24)

 

4. also

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
  • also 함수는 확장 함수이기 때문에 context object를 receiver(this)로 전달한다. 그러나, 코드 블럭 내에서 this를 파라미터로 입력하기 때문에 it을 사용해 프로퍼티에 접근할 수 있다.
  • also 함수는 반환 결과가 객체 자신이다.
  • also 함수는 객체의 속성을 전혀 사용하지 않거나 변경하지 않고 사용하는 경우에 유용하다.
  • 예를 들면, 객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 적합하다.
val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("original list: $it") }
    .add("four")

 

5. let

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
  • let 함수는 확장 함수이기 때문에 context object를 receiver(this)로 전달한다. 그러나, 코드 블럭 내에서 this를 파라미터로 입력하기 때문에 it을 사용해 프로퍼티에 접근할 수 있다.
  • let 함수는 반환 결과가 람다의 결과이다.
  • let 함수는 지정된 값이 null이 아닌 경우에 코드를 실행해야 하는 경우, Nullable 객체를 다른 Nullable 객체로 변환하는 경우, 단일 지역 변수의 범위를 제한하는 경우에 유용하다.
val length = str?.let {
    println("this str is not null")
    it.length
} ?: 0

 

 

 

 

 

 

 

Reference :

https://kotlinlang.org/docs/scope-functions.html