본문 바로가기
코틀린/코틀린 공식문서

[코틀린 공식문서] Scope functions

by 측면삼각근 2023. 9. 15.
728x90
반응형

들어가기 전에

해당 블로깅은 코틀린 공식문서의 Scope functions를 번역하며 학습한 내용입니다.

학습 중임에 따라 이해하는데 도움이 되는 부분들을 추가되고 의역된 부분이 있습니다. 혹시 잘못된 설명이 있다면 얼마든지 제보해 주세요.


코틀린 표준 라이브러리에는 객체 콘텍스트 내에서 코드 블록을 실행하는 것이 유일한 목적인 함수가 있다.

람다 식이 제공된 객체에서 이러한 함수를 호출하면 임시 범위가 형성된다.
이 범위에서는 이름 없이 개체에 액세스 할 수 있고, 이러한 함수를 범위함수(scope Function)이라고 한다.
범위 함수에는 let, run, with, apply, also가 있다.

이 함수들은 모두 객체에 대한 코드 블록을 실행하는 동일한 작업을 수행한다.
그러나 객체를 블록 내에서 사용할 수 있게 되는 방법과 전체 표현식의 결과가 무엇인지가 다르다.

다음은 범위함수를 사용하는 예시이다.

Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

/*
Person(name=Alice, age=20, city=Amsterdam)
Person(name=Alice, age=21, city=London)
*/

let 없이 동일하게 작성하면 새 변수를 선언하고 사용할 때마다 여러 번 호출해야 한다.

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

범위 함수는 새로운 기능을 넣는 것이 아니지만, 코드를 가독성 있게 작성할 수 있도록 한다.

범위함수는 범위 함수 간 유사성이 많기 때문에, 사용 사례에 따라 적합한 기능을 선택하는 것이 까다로울 수 있다.
선택은 보통 프로젝트의 의도와 사용 일관성에 따라 다르다.

Function selection

scope Function의 차이점

Function Object reference Return value Is extension function
let it Lambda result O
run this Lambda result O
run - Lambda result X, context object없이 호출
with this Lambda result X, context object를 인수로 사용
apply this object Context O
also it object Context O

목적에 따른 scope function의 활용

  • nullable이 아닌 객체에 대해 람다 실행 : let
  • 로컬 범위의 변수로 표현식 소개 : let
  • object구성: apply
  • object 구성 및 결과 계산: run
  • 표현식이 필요한 명령문 실행(비확장): run
  • 추가 효과: also
  • 객체에 대한 그룹화 함수 호출: with

범위 함수는 코드를 더 간결하게 만들 수 있지만, 가독성이 떨어지고 오류를 유발할 수 있으므로 주의해야 한다.

또, 중첩을 피하고, `this`나 `it`의 값에 혼동하기 쉽기 때문에 여러 메서드를 사용할 때 주의가 필요하다.

728x90

스코프 함수의 구분

스코프 함수는 콘텍스트 참조방식반환값의 두 가지 차이점이 있다.

Context object: it or this

스코프 함수로 전달된 개체는 it이나 this로 사용할 수 있다.
둘 다 동일한 기능을 제공하므로, 서로 다른 사용 사례에 대한 장단점을 설명하고, 사용 권장사항을 제공한다.

fun main() {
    val str = "Hello"
    // this
    str.run {
        println("The string's length: $length")
        //println("The string's length: ${this.length}") // does the same
    }

    // it
    str.let {
        println("The string's length is ${it.length}")
    }
}

this

run, with apply가 this로 참조한다.
reciever개체의 멤버에 액세스 할 때 코드를 더 간결하게 만들 수 있다. (this.length -> length처럼)

반면, this가 생략되는 경우 this context인지, 외부의 인자인지 구분하기 어려울 수 있으므로 `this`로 사용하는 것은 함수를 호출하거나 속성에 값을 할당하여 객체의 멤버에 주로 작동하는 람다에 권장된다.

val adam = Person("Adam").apply { 
    age = 20                       // same as this.age = 20
    city = "London"
}
println(adam)

it

let, alow가 it으로 참조된다. 인수 이름을 지정하지 않으면 암시적 기본 이름인 it으로 개체에 액세스 한다.

그러나, 객체의 함수나 속성을 호출할 때 this처럼 암시적으로 사용할 수 있는 객체가 없다. 따라서 콘텍스트 객체에 액세스하는 것은 대부분 함수가 호출에서 인수로 사용될 때 더 좋다. 코드 블록에서 여러 변수를 사용하는 경우에도 더 좋다.

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
}

val i = getRandomInt()
println(i)

return value 반환값

  • apply, also:  컨텍스트 객체(Context Object) 반환
  • let, run, with: 람다 입력 결과 반환

Context object

apply와 also의 반환값은 컨텍스트 객체 자체이다. 따라서 체이닝 함수로 연결하여 사용할 수 있다.

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
    .apply {
        add(2.71)
        add(3.14)
        add(1.0)
    }
    .also { println("Sorting the list") }
    .sort()

Lambda result: 람다 결과

let, run, with는 람다 결과를 반환한다. 따라서 결과를 변수에 할당하고 결과에 대한 작업을 연결할 때에 사용할 수 있다.

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

반환 값을 무시하고 지역에 대한 임시 범위를 만들 수도 있다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    val firstItem = first()
    val lastItem = last()        
    println("First item: $firstItem, last item: $lastItem")
}

Functions

let

  • context object: it
  • return: lambda result

체인 결과에 대해 하나 이상의 함수를 호출하는 데 사용할 수 있다.

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

// let을 이용한 위와 동일 코드
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
// 단일 함수인 경우 메서드 참조 사용 가능

null이 아닌 값을 포함하는 코드 블록을 사용하는데 자주 사용된다.

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'")

with

  • context object: this
  • return: lambda result

확장 함수가 아니기 때문에 콘텍스트 개체는 인수로 전달되지만, 람다 내부에서는 this로 사용할 수 있다

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)

run

  • context object: this
  • return: lambda result

run은 with와 동일하지만, 확장함수로 구현된다.
let처럼 점 표기법으로 콘텍스트 개체에서 호출할 수 있다. 람다에서 객체를 초기화하고 반환 값을 계산할 때 유용하다.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = servce.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}")
}

run을 비확장 함수(non-extension function)로 호출할 수도 있다.

이 경우 콘텍스트 개체가 없지만 여전히 람다 결과를 반환한다. 비확장함수로 사용하면 표현식이 필요한 여러 문의 블록을 실행시킬 수 있다.

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)
}

apply

  • context object: this
  • return: 객체 자신

컨텍스트 객체 자체를 반환하므로, 값을 반환하지 않고 주로 수신자 객체의 멤버에 대한 코드를 작성하는 데 사용하는 것이 좋다
가장 일반적인 사용 사례는 객체 구성이다.

이러한 호출은 "객체에 다음 할당을 적용한다."로 읽을 수 있다.

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

다른 사용 사례로는 보다 복잡한 처리를 위해 호출 체이닝에 사용할 수도 있다.

also

  • context object: it
  • return: 객체 자신

also는 컨텍스트 개체를 인수로 사용하는 작업을 할 때 유용하다.
해당 속성 함수 및 함수 대신 개체에 대한 참조가 필요한 작업에 사용하거나 외부 범위에서 참조를 숨기고 싶지 않을때 사용한다.

코드로 보면 "그리고 객체로 다음도 수행한다" 라고 읽을 수 있다.

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