본문 바로가기
Kotlin

Scope functions

by oncerun 2023. 7. 15.
반응형

Kotlin 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하는 것이 유일한 목적인 여러 함수가 포함되어 있습니다.

 

let, run, with, apply and also와 같은 함수는 모두 객체에서 코드 블록을 실행하는 동일한 작업을 수행하는데요.

 

각 함수의 차이점은 블록 내에서 이 객체를 사용할 수 있게 되는 방식과 표현식의 결과입니다.

 

 

몇 가지의 유형 별 예제를 통해 어떻게 scope function을 사용하는지 알아봅니다.

 

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

 

만약 동일한 코드를 let 키워드 없이 사용한다면 새로운 변수를 선언하고, 반복해서 이를 사용해야 합니다.

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

 

scope function은 새로운 기술적인 기능을 도입하지는 않지만 코드를 간결하고 가독성 있게 만들 수 있습니다.

 

 

scope function 간의 유사점이 많이 있기 때문에, 어떤 경우에 어떤 함수를 사용해야 하는지 고민하는데 어려움이 있을 수 있습니다. 

 

그 선택은 주로 프로젝트의 사용 의도와 일관성에 따라 달라집니다.

 

 

다음 표는 scope funtion와 해당 규칙의 차이점에 대한 자세한 설명을 제공합니다.

 

FunctionObject referenceReturn valueIs extension function

Funtion Object reference Return value Is extenstion function
let it Lambda result Yes
run this Lambda result Yes
run - Lambda result No: called without the context object
with this Lambda result No: takes the context object as an argument.
apply this Context object Yes
also it Context object Yes

 

 

다음은 사용 목적에 따라 범위 함수를 선택하기 위한 간단한 가이드입니다.

 

 

Null이 아닌 객체인 경우 람다를 실행하는 경우 

val name: String? = "John"

name?.let { 
    // name이 null이 아닌 경우에만 실행되는 람다
    println("Name: $it")
}

 


 로컬 범위에서 표현식을 변수로 도입하기.

val result = someExpression.let { calculatedResult ->
    // calculatedResult 변수로 표현식의 결과를 도입
    // 계산된 결과를 이용하여 다양한 작업 수행
    println("The result is: $calculatedResult")
    // ...
    // calculatedResult 변수의 값을 반환
    calculatedResult
}

 

someExpression은 어떤 계산식이나 표현식을 나타냅니다. let 함수에 의해 표현식이 계산되고 결과가 calculatedResult 변수에 할당됩니다. 

 

표현식을 변수로 도입하는 경우 변수의 범위가 코드 블록 내로 제한되기 때문에 변수를 블록 외부에서 참조하지 않아도 됩니다. 

 


객체의 속성을 설정하거나 구성하는 경우 apply를 고려합니다. 

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

컨텍스트 객체의 속성을 구성하고 컨텍스트 내에서 코드 블록을 실행하고 결과를 반환할 때 run을 권장합니다.

val person = Person("John", 30)

val greeting = person.run {
    "Hello, my name is $name and I'm $age years old."
}

 

구문이 실행될 때 표현식이 필요한 경우 run을 권장합니다. 이유는 특정 컨텍스트 없이 코드 블록을 실행할 수 있는 함수이기 때문입니다.

    val result = run {
        val x = 10
        val y = 20
        x + y
    }
    println(result)

추가적인 효과를 부여하는 경우 also를 권장합니다.

 

함수 이름에서도 느껴지듯이. 추가적인 작업이 필요한 경우 사용합니다.

val person = Person("John", 30)

val modifiedPerson = person.also {
    it.age = 31
}

with는 객체를 인자로 전달받아 코드 블록 내에서 객체의 속성에 직접 접근합니다. 객체에 대한 함수 호출을 그룹화 할 때 권장합니다.

 val person = Person("John", 30)

with(person) {
    println("Name: $name")
    println("Age: $age")
    celebrateBirthday()
}

fun Person.celebrateBirthday() {
    age++
    println("Happy birthday! Now you are $age years old.")
}

가독성적인 측면에서 함수 호출을 그룹화하려는 의도입니다.

 

 

그리고 다음과 같이 주의사항 및 선택에 도움을 주는 말이 있습니다.

 

The use cases of different scope functions overlap, so you can choose which functions to use based on the specific conventions used in your project or team.

 

여러 scope function의 사용 사례가 겹칠 수 있으니, 프로젝트나 팀에서 컨벤션을 정하여 선택할 수 있다.


Although scope functions can make your code more concise, avoid overusing them: it can make your code hard to read and lead to errors. We also recommend that you avoid nesting scope functions and be careful when chaining them because it's easy to get confused about the current context object and value of this or it.

 

scope funtion은 코드를 더욱 간결하게 만들어 줄 수 있다. 다만 과도한 간결함은 피하는 것이 좋다. 

이는 코드를 읽기 어렵게 만들며 에러 발생의 원인이 될 수 있다.

 

권장사항으로 중첩 scope function를 최대한 피해야 합니다.  그 이유는 현재 컨텍스트 객체나 값인 this or it에 대해 혼동할 수 있기 때문입니다.

 

 

 

Context object: this or it

 

Scope Function에 전달된 람다 내에서 컨텍스트 객체는 실제 이름대신 short reference로 참조할 수 있습니다.

 

각 스코프 함수는 컨텍스트 객체를 참조하기 위한 하나 혹은 두 가지의 방법이 있습니다. 

 

as a lambda receiver( this ) or as a lambda argument( it )입니다. 

 

이 두가지는 동일한 기능을 제공하지만 각 사용 사례에 따라 장단점에 따라 구별하여 사용합니다. 

 

 

this

 

run, with and apply에서는 컨텍스트 객체를 참조하기 위해 this, lambda receiver를 사용합니다. 

그렇기에 람다 내부에서는 객체의 ordinary class functions 들을 이용할 수 있습니다. 

대부분의 케이스에서 코드를 줄이기 위해 this 키워드를 생략할 수 있습니다. 

하지만 this 키워드를 생략한다면, 람다 내에서 receiver member 인지, 외부 객체, 함수인지 구별하기가 어려워집니다. 

 

따라서 컨텍스트 객체를 receiver로 사용하는 것은 주로 객체의 함수를 호출하거나 프로퍼티에 값을 할당하여 객체의 멤버에 대해 작업하는 람다에 권장됩니다. 

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

 

 

주로 receiver의 멤버를 호출하거나, 값을 할당하는 람다에서는 this가 권장된다는 이야기다.

 

 

it

 

let and also 는 컨텍스트 객체를 lambda argument로 참조할 수 있습니다. 

만약 인자의 이름이 명시되지 않는 경우, 그 인자에는 명시적인 기본 이름인 it를 사용하여 접근할 수 있습니다. 

 

다만 객체의 함수나 속성을 호출할 때는 this와 달리 암묵적으로 객체에 접근할 수 없습니다. 

따라서 함수 호출의 인자로 사용하거나 코드 블록에서 여러 변수를 사용하는 경우 it을 통해 컨텍스트 객체에 접근하는 것이 권장됩니다.

val person = Person("John", 30)

person.run {
    this.sayHello() // `this`를 명시적으로 사용하여 함수 호출
    // 이 경우 `it`을 사용할 수 없습니다.
    // 컨텍스트 객체인 `person`에 접근하기 위해 `this`를 사용해야 합니다.
}

 

함수의 호출인자로 사용한다는 것은 다음과 같을 것 입니다.

val numbers = listOf(1, 2, 3, 4, 5)

val filteredNumbers = numbers.filter {
    it % 2 == 0 // `it`을 사용하여 객체의 속성에 접근하고 조건을 체크합니다.
}

 

 

Retun Value

 

스코프 함수들은 반환하는 결과가 다릅니다. 

 

apply, also는 컨텍스트 개체를 반환하고 let, run, with는 람다의 결과를 반환합니다. 

 

 

Context Object를 반환하는 경우.

 

apply and 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()

 

컨텍스트 객체를 반환하는 특성 때문에 반환 값에도 사용될 수가 있습니다.

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

val i = getRandomInt()

 

 

Lambda의 결과를 반환하는 경우.

 

let, run and 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.")

 

추가적으로 반환되는 값을 무시하고 로컬 변수를 위한 임시 스코프를 만들어 사용할 수도 있습니다.

 

 

 

 

지금까지 scope functions들의 특징과 장단점, 사용사례에 따른 권장사항을 알아보았습니다. 

 

스코프 함수는 객체의 컨텍스트 내에서 동작하는 코드 블록을 실행하는 것이 목적입니다. 

 

각 함수마다 권장되는 사용사례가 있으며, 필요한 경우 컨벤션을 만들어 사용한다면 

 

더 간결하고 가독성 있는 코드를 작성할 수 있을 것입니다.

 

 

 

실제 공식문서에는 각 스코프 함수마다 사용사례에 관한 좋은 코드 가이드를 더 제공해 줍니다. 

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

 

Scope functions | Kotlin

 

kotlinlang.org

 

반응형

댓글