Kotlin - Scope Function

Park Suyong·2022년 2월 24일
0

Kotlin

목록 보기
3/7

Kotlin Scope Function

Kotlin Scope Functionlet, run, with, apply, also가 있다.

정의

Scope Function은 객체의 컨텍스트 내에서 코드 블록을 실행할 수 있게 하는 함수이다. 람다식이 제공된 객체에서 함수를 호출하면 해당 함수는 일시적인 범위를 형성하게 된다. 일시적인 범위에서는 객체의 이름 없이 객체에 접근할 수 있게 된다.

이러한 Scope Function은 새로운 기술적 능력이 아니다. 다만, Scope Function을 사용하면 코드를 좀 더 가독성있게 만들 수 있다는 장점을 갖는다.

각 스코프는 기능이 유사해 어떤 스코프를 선택할 지 헷갈릴 수 있으나, 차이점은 아래에서 확인하도록 한다.

종류 설명

Scope Function은 코드 블럭 내에서 객체에 어떻게 접근하는지, 코드 블럭 내의 결과가 무엇인지에 따라 아래 표로 구분할 수 있다.

FunctionObject referenceReturn valueIs extension function
letitLambda resultYes
runthisLambda resultYes
run-Lambda resultNo: called without the context object
withthisLambda resultNo: takes the context object as an argument
applythisContext objectYes
alsoitContext objectYes

공식 문서는 각 Scope Function을 다음과 같은 경우에 사용한다고 명시했다.

  • Executing a lambda on non-null objects: let
  • Introducing an expression as a variable in local scope: let
  • Object configuration: apply
  • Object configuration and computing the result: run
  • Running statements where an expression is required: non-extension run
  • Additional effects: also
  • Grouping function calls on an object: with

하지만 위 글을 봐도 감은 오지 않는다. 예시로 확인한다.

예시

with

Grouping function calls on an object: with

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

코드로 확인할 수 있듯이 람다식 내에서 this 키워드를 통해 객체 참조를 할 수 있다. 또한, 참조 과정에서 dot notation 없이 스코프 내에서 코드를 간략히 쓸 수 있다.

뿐만 아니라 람다 함수의 특성상 마지막 줄이 리턴되는데, with는 객체를 인자로 받기 때문에 이미 생성된 객체에 여러 작업을 일괄적으로 진행한 후 리턴하는 데에 용이하다.

run
  • Object configuration and computing the result: run
  • Running statements where an expression is required: non-extension run
val service = MultiportService("https://example.kotlinlang.org", 80)

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

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}
val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

아래 코드를 보면 run Scope Function은 그저 로컬 스코프를 갖는 코드 블럭이다. 위의 코드를 보면 확장 함수 형태로 사용되고 있다. 사실상 with와 동일하다.

위의 코드를 보면, service 객체를 생성하는 시점에 변수를 할당하지 않고 먼저 사용 가능하며, apply 혹은 also 같은 경우 context object를 리턴하지만, run에서는 람다함수의 리턴값이 사용되므로 개발자가 임의로 생성한 값이나 객체를 리턴할 수 있다. 즉, 빌더 패턴에 가장 적합함을 알 수 있다.

let
  • Executing a lambda on non-null objects: let
  • Introducing an expression as a variable in local scope: let

before let

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList) 

after let

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
    println("The first item of the list is '$firstItem'")
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

let 또한 앞서 with 거의 비슷하게 사용할 수 있다. 다만, let에서는 it으로 객체를 사용할 수 있다. 예제를 보면 알겠지만 with와는 다르게 람다 함수의 인자를 일일히 사용해 줘야 한다. 따라서, with가 사용 가능한 상황에서 굳이 let를 사용할 필요는 없다.

다만, let는 null 체크를 편하게 하기 위해 사용한다. 아래에서 2번째 코드를 보면, str?.let { } 로 작성했다. 이는 str 값이 null이면 코드 블록이 실행되지 않음을 의미한다.

이 뿐만 아니라, context object를 다른 값으로 변환할 때 local scope로 묶어서 깔끔하게 처리할 수 있다. 맨 아래 코드를 보면 람다 함수 인자의 firstItem은 코드 블록 내에서만 유효하므로 다른 코드에서는 modifiedFirstItem만 신경쓰면 된다.

apply

Object configuration: apply

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

apply 같은 경우에는 context object를 리턴한다. 따라서, 람다 함수 블럭 내에서 리턴값을 생각할 필요가 없다. 또한, context object를 사용하기 때문에 객체는 this로 참조할 수 있다. 즉, with와 동일하게 코드 블록 내에서 객체를 명시적으로 표시할 필요 없이 생략하고 객체의 메서드 혹은 인스턴스 변수를 사용할 수 있다.

하지만, with의 경우 인자로 context object인 객체를 넘겨 주지만 apply는 확장 함수의 형태이다. 따라서, adam 변수에 객체 Person이 할당되기 전에 apply 코드 블록 내의 코드가 모두 실행되고 나서 객체에 할당되게 된다. apply는 이런 식으로 객체의 생성 시점에 초기화 하는 것으로 많이 사용된다.

also

Additional effects: also

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

also는 apply와 유사하게 확장 함수로 정의되는 람다 함수이다. 람다 함수의 리턴값은 없으며, context object를 리턴한다. apply와 동일하게 람다 함수 내에서 리턴값을 생각할 필요는 없다. 그렇다면 apply와 무엇이 다른가?

apply는 람다 함수에 인자를 넘겨 주지 않지만 also는 context object를 넘겨주기 때문에 it으로 사용하거나 명시적인 이름으로 객체를 참조할 수 있다. apply 보다 가독성을 높일 수 있고 간편해 진다는 장점이 있다.

결론

스코프 함수들은 서로 매우 유사하다. 따라서, 어떤 스코프 함수를 사용하던 실행에는 차이가 없다. 가독성과 코드 작성에 간결함을 더할 수 있는 스코프 함수를 상황에 따라 선택하면 될 것이다.

  • null 체크 ==> let
  • 객체를 생성하며 초기화 후 할당 ==> apply
  • 생성된 객체의 메서드 여러 개를 한 번에 사용하는 경우 혹은 속성을 한 번에 설정하는 경우 ==> with
  • 객체의 생성 시점, 생성한 객체를 사용할 때, 명시적으로 객체를 참조하는 작업이 필요할 때 ==> also
  • 빌더 패턴 사용 ==> run

References

Kotlin Scope Function

Kotlin: 스코프 함수들(Scope functions) let, run, with, apply, also

profile
Android Developer

0개의 댓글