이전에 Scope Function에 정리했지만 아직 정확하게 어디에 쓰여지는지 제대로 알지못한 거 같아 다시 정리하기로 했다. 그럼 먼저 간단하게 Scope Function이 무엇인지 알고 넘어가자
객체의 범위 내에서 작업을 수행하고, 코드를 더 간결하고 가독성 있게 작성할 수 있도록 도와준다. ScopeFunction는 let, run, with, apply, also 5개 함수가 있다.
그럼 각 ScopeFunction에 대해 알아보자
inline fun <T, R> T.let(block: (T) -> R): R
val value = ...
value?.let {
... // execute this block if not null
}
nullable인 value 변수가 null이 아니라면 블록 내의 코드가 실행이 된다. null이 아닌 경우 해당 객체를 it의 매개변수로 블록 내에서 사용할 수 있다. 그리고 나서 let 자신이 반환된다.
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에 안전하게 처리할 수 있고 코드의 가독성과 안전성을 높일 수 있다.
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의 범위를 블록 내로 제한하기 때문에 외부 범위에 영향을 주지 않는다.
inline fun <T, R> T.run(block: T.() -> R): R
val len = text?.run {
println("length $this")
length
} ?: 0
let과 비슷하지만 다른 부분이 있다면 let은 it을 생략할 수 없다면 run은 this를 생략될 수 있는 것을 알 수 있다.
val text = "hello, world"
val result = text.run {
toUpperCase()
}
println(result)
text가 수신객체가 되고 run 블록 내에 toUpperCase() 메서드를 호출하여 text를 대문자로 변환 후 반환하게 된다. 이 처럼 run은 어떠한 것을 변환할 때 사용될 수 있다. 또한, 객체의 상태를 변환할 뿐 아니라 객체의 속성을 변경하거나 다른 속성을 계산하는 작업에도 쓰일 수있다.
inline fun <T> T.also(block: (T) -> Unit): T
val num = 1234.also {
log.debug("the function did its job!")
}
num 변수에 어떤 것을 할당하고 콘솔에 로그를 찍는 코드이며 Kotlin coding conventions에 설명을 보면 log를 찍을 때 사용하는 것을 권장하고 있다.
val obj = SomeClass().also {
// 객체 초기화 작업 수행
it.property1 = value1
it.property2 = value2
it.initialize()
}
also는 let, run과는 다르게 수신 객체를 반환이 된다. 이로 인해 프로퍼피를 초기화 할 수 있다.
fun getThatBaz() = calculateBaz().also { baz = it }
also는 수신 객체를 리턴하는 특징을 가지기 때문에 객체에서 다른 프로퍼티의 값이 계산 후 다시 할당할 수 있다.
inline fun <T> T.apply(block: T.() -> Unit): T
val person = Person().apply {
name = "Jane"
age = 30
}
apply의 궁극적 목적은 초기화라고 할 수 있다. also도 객체를 초기화할 때 사용하지만 also는 무조건 it을 사용해야하지만 apply는 this를 사용하기에 생략할 수 있어 조금 더 깔끔한 코드를 작성할 수 있다.
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 처럼 사용되는 메소드를 정의하기에 매우 유용하다.
inline fun <T, R> with(receiver: T, block: T.() -> R): R
with는 이전의 4개 함수와는 다르게 확장함수가 아니다.
Lambda Result 반환한다는 점에서 let, run과 같지만 apply와 비슷하다고 한다. 하지만 with와 apply의 차이점은 with는 마지막 줄이 반환되고, apply는 람다 결과 객체가 반환된다는 점이다.
val s: String = with(StringBuilder("init")) {
append("some").append("thing")
println("current value: $this")
toString()
}
with는 객체를 확인된 범위 내에서만 사용하고 싶을 때 사용한다
위의 코드를 보면 인스턴스 자체를 외부 범위에 노출하지 않고 StringBuilder에 대한 호출들을 wraaping하는데 사용하고 있다.
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