[Kotlin] Scope Function (2)

hegleB·2023년 6월 15일
0
post-thumbnail

이전에 Scope Function에 정리했지만 아직 정확하게 어디에 쓰여지는지 제대로 알지못한 거 같아 다시 정리하기로 했다. 그럼 먼저 간단하게 Scope Function이 무엇인지 알고 넘어가자

Scope Function

객체의 범위 내에서 작업을 수행하고, 코드를 더 간결하고 가독성 있게 작성할 수 있도록 도와준다. ScopeFunction는 let, run, with, apply, also 5개 함수가 있다.
그럼 각 ScopeFunction에 대해 알아보자

let

inline fun <T, R> T.let(block: (T) -> R): R
  • <T, R> : 제너릭 타입 매개변수를 선언하는 부분이고 T는 객체의 타입, R은 블록의 결과 타입을 나타낸다
  • block: (T) -> R : Lambda function에 인자를 넘겨주기 때문에 it을 사용해야한다.
  • block의 반환 값은 Lambda Result, 즉 let 자신이다

어떻게 사용하나?

1. if (object != null)의 대체

val value = ...

value?.let {
    ... // execute this block if not null
}

nullable인 value 변수가 null이 아니라면 블록 내의 코드가 실행이 된다. null이 아닌 경우 해당 객체를 it의 매개변수로 블록 내에서 사용할 수 있다. 그리고 나서 let 자신이 반환된다.

2. null이 아닌경우 nullable 값의 mapping

val myMap: Map<String, String?> = mapOf("key1" to "value1", "key2" to null, "key3" to "value3")

val nullableValue: String? = myMap["key2"]

nullableValue?.let { value ->
    // nullableValue가 null이 아닌 경우에만 실행되는 블록
    // value 변수를 사용하여 작업을 수행
    println(value.length) // value의 길이 출력
}

let함수는 변형을 위해 사용되기도 한다. Map의 값은 nullable 값이 될 수 있기 때문에 Map에 안전하게 처리할 수 있고 코드의 가독성과 안전성을 높일 수 있다.

3. 변수 또는 계산의 범위를 제한

val name: String? = "John Doe"

val length = name?.let { 
    // Confined scope within the let block
    println("Processing name: $it")
    it.length
}

name의 길이를 계산하고 그 값을 length 변수에 저장한다. let을 사용하여 변수 it의 범위를 블록 내로 제한하기 때문에 외부 범위에 영향을 주지 않는다.

run

inline fun <T, R> T.run(block: T.() -> R): R
  • <T, R> : 제너릭 타입 매개변수를 선언하는 부분이고 T는 객체의 타입, R은 블록의 결과 타입을 나타낸다
  • block: T.() -> R : let과는 다르게 수신객체(T)의 메서드와 프로퍼티에 접근하기 때문에 this를 사용한다.
  • block의 반환 값은 Lambda Result, 즉 run 자신이다

어떻게 사용하나?

1. if (object != null) 의 대체

val len = text?.run {
    println("length $this")
    length
} ?: 0

let과 비슷하지만 다른 부분이 있다면 let은 it을 생략할 수 없다면 run은 this를 생략될 수 있는 것을 알 수 있다.

2. Transformation

val text = "hello, world"

val result = text.run {
    toUpperCase()
}

println(result)

text가 수신객체가 되고 run 블록 내에 toUpperCase() 메서드를 호출하여 text를 대문자로 변환 후 반환하게 된다. 이 처럼 run은 어떠한 것을 변환할 때 사용될 수 있다. 또한, 객체의 상태를 변환할 뿐 아니라 객체의 속성을 변경하거나 다른 속성을 계산하는 작업에도 쓰일 수있다.

also

inline fun <T> T.also(block: (T) -> Unit): T
  • : 제너릭 타입 매개변수를 선언하는 부분이고 T는 수신 객체를 나타낸다.
  • (T) -> Unit : 수신객체를 인자로 받기 때문에 it을 사용하고 반환 값이 없는 함수이다.
  • block의 반환 값은 Object reference이다

어떻게 사용할까?

1. 수신객체가 블록 안에서 사용되지 않을 때

val num = 1234.also {
    log.debug("the function did its job!")
}

num 변수에 어떤 것을 할당하고 콘솔에 로그를 찍는 코드이며 Kotlin coding conventions에 설명을 보면 log를 찍을 때 사용하는 것을 권장하고 있다.

2. object를 초기화 할 때

val obj = SomeClass().also {
  // 객체 초기화 작업 수행
  it.property1 = value1
  it.property2 = value2
  it.initialize()
}

also는 let, run과는 다르게 수신 객체를 반환이 된다. 이로 인해 프로퍼피를 초기화 할 수 있다.

3. 계산된 값을 할당 할 때

fun getThatBaz() = calculateBaz().also { baz = it }

also는 수신 객체를 리턴하는 특징을 가지기 때문에 객체에서 다른 프로퍼티의 값이 계산 후 다시 할당할 수 있다.

apply

 inline fun <T> T.apply(block: T.() -> Unit): T 
  • : 제너릭 타입 매개변수를 선언하는 부분이고 T는 수신 객체를 나타낸다.
  • T.() -> Unit : also와는 다르게 수신객체(T)의 메서드와 프로퍼티에 접근하기 때문에 this를 사용하고 생략이 가능하다
  • block의 반환 값은 Object reference이다

어떻게 사용할까?

1. 객체 초기화

val person = Person().apply {
  name = "Jane"
  age = 30
}

apply의 궁극적 목적은 초기화라고 할 수 있다. also도 객체를 초기화할 때 사용하지만 also는 무조건 it을 사용해야하지만 apply는 this를 사용하기에 생략할 수 있어 조금 더 깔끔한 코드를 작성할 수 있다.

2. Unit을 리턴하는 Builer 스타일의 메소드의 사용

data class FooBar(var a: Int = 0, var b: String? = null) {
  fun first(aArg: Int): FooBar = apply { a = aArg }
  fun second(bArg: String): FooBar = apply { b = bArg }
}

fun main(args: Array<String>) {
  val bar = FooBar().first(10).second("foobarValue")
  println(bar)
}

unit을 리턴하는 메소드를 래핑할 때 사용된다고 한다.
class가 Builder style API를 클라이언트에 노출하려고 하기 때문에 여기서 apply는 setter 처럼 사용되는 메소드를 정의하기에 매우 유용하다.

with

  inline fun <T, R> with(receiver: T, block: T.() -> R): R
  • <T, R> : 제너릭 타입 매개변수를 선언하는 부분이고 T는 수신 객체, R은 블록의 결과 타입을 나타낸다
  • T.() -> R : 수신객체(T)의 메서드와 프로퍼티에 접근하기 때문에 this를 사용하고 생략이 가능하다.
  • block의 반환 값은 Lambda Result이다

with는 이전의 4개 함수와는 다르게 확장함수가 아니다.
Lambda Result 반환한다는 점에서 let, run과 같지만 apply와 비슷하다고 한다. 하지만 with와 apply의 차이점은 with는 마지막 줄이 반환되고, apply는 람다 결과 객체가 반환된다는 점이다.

어떻게 사용할까?

1. 확인된 scope에서 객체를 적용할 때

  val s: String = with(StringBuilder("init")) {
    append("some").append("thing")
    println("current value: $this")
    toString()
}

with는 객체를 확인된 범위 내에서만 사용하고 싶을 때 사용한다
위의 코드를 보면 인스턴스 자체를 외부 범위에 노출하지 않고 StringBuilder에 대한 호출들을 wraaping하는데 사용하고 있다.

2. member extensions of a class

object Foo {
  fun ClosedRange<Int>.random() =
      Random().nextInt(endInclusive - start) + start
}

// random() can only be used in context of Foo
with(Foo) {
  val rnd = (0..10).random()
  println(rnd)
}

만약 Foo 객체가 멤버 확장 함수 random()을 가지고 있고, 이 함수를 해당 객체의 범위 내에서만 사용하고 싶다면, with 함수를 사용하여 해결할 수 있다. 이러한 접근 방식은 특정 확장 기능을 의미있게 그룹화해야 할 때 권장한다

참고

https://mashup-android.vercel.app/mashup-10th/heejin/KotlinScopeFunction/ScopeFunction/
https://kotlinlang.org/docs/coding-conventions.html#using-scope-functions-applywithrunalsolet

profile
성장하는 개발자

0개의 댓글

관련 채용 정보