[Kotlin] Scope Functions

Hwichan Ji·2021년 2월 2일
0

Kotlin

목록 보기
11/11
post-thumbnail

Scope Functions

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

Scope Function의 일반적인 형태는 다음과 같습니다.

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

만약 Scope Function을 사용하지 않는다면 위의 코드는 아래와 같이 작성되어야 합니다.

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

Scope Function은 새로운 기술 기능을 도입하지는 않지만 코드를 보다 간결하고 읽기 쉽게 만들 수 있습니다.

Kotlin에서 제공하는 Scope Function으로는 let, run, with, apply 그리고 also가 존재합니다. 기본적으로 이 함수들은 객체의 코드 블럭을 실행한다는 공통점을 갖습니다. 차이점은 코드 블럭 내에서 객체에 어떻게 접근하는지와 블럭 내 코드의 결과가 무엇이냐입니다.

Context object: this or it

Scope Functionlambda expression 내부에서 컨텍스트 객체는 실제 이름 대신 짧은 참조를 통해 사용가능합니다. 이 짧은 참조는 lambda receiverthis 혹은 lambda argumentit입니다.

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

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

this

run, with 그리고 apply는 컨텍스트 객체를 this를 통해 사용합니다. 그래서 객체를 일반적인 클래스 함수 내부에서 사용하는 것처럼 사용할 수 있습니다. 대부분의 경우 객체 멤버에 할당을 할 때 this를 생략할 수 있습니다. 다만 외부 객체나 함수와 구분하기 힘들어지므로 this를 사용하는 것을 권장합니다.

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

it

letalso는 컨텍스트 객체를 lambda argument처럼 가지고 있습니다. 따라서 it을 통해 컨텍스트 객체에 접근할 수 있습니다. itthis보다 짧고 it을 사용한 표현식은 보통 읽기 쉽습니다. 하지만 객체의 함수나 프로퍼티를 호출할 때 this처럼 암시적으로 사용할 수는 없습니다.

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

추가로 컨텍스트 객체를 argument로 전달한다면 컨텍스트 객체에 이름을 부여할 수 있습니다.

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

Return value

Scope Function은 코드 블럭이 반환하는 결과에서 차이를 보입니다. 따라서 Scope Function을 실행한 뒤 어떤 작업을 하냐에 따라 사용할 Scope Function을 결정합니다.

Context object

applyalso는 해당 코드 블럭의 컨텍스트 객체를 반환합니다. 따라서 이 두 함수는 같은 객체에 대한 call chain을 가질 수 있습니다.

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

또한 이들은 return 문에 사용될 수 있습니다.

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

Lambda result

let, run 그리고 withlambda result를 반환합니다. 따라서 이 두 함수의 결과는 변수에 할당될 수 있습니다.

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

추가적으로 lambda result는 무시될 수 있으며, Scope Function을 변수에 대한 임시 범위를 생성하는데 사용할 수 있습니다.

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

Functions

let

let은 컨텍스트 객체를 argument(it)처럼 사용할 수 있고 lambda result를 반환합니다. 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
} 

만약 코드 블럭이 it을 인자로 가지는 함수 하나만 담고 있다면 lambda expression 대신 ::을 사용할 수 있습니다.

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

let은 종종 non-null 객체에 코드 블럭을 생성할 때 사용합니다. 또 코드 가독성을 높이기 위해 코드 블럭에 지역 변수를 제공할 때 사용합니다.

with

with는 컨텍스트 객체를 argument로 전달하지만 내부에선 this를 통해 사용가능합니다. 그리고 lambda result를 반환합니다. with는 코드 블럭 내에서 lambda result 반환없이 함수를 호출할 때 사용할 것을 추천합니다.

val numbers = mutableListOf("one", "two", "three")
// with this object, do the following.
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

run

run은 컨텍스트 객체를 this를 통해 사용하며, lambda result를 반환합니다. 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}")
}

apply

apply는 컨텍스트 객체를 this를 통해 사용하며, 컨텍스트 객체를 반환합니다. apply는 코드 블럭이 값을 반환하지 않고 주로 컨텍스트 객체의 멤버들로 동작하는 경우 사용합니다. 이에 해당하는 예시로는 객체 설정이 있습니다.

// apply the following assignments to the object.
val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

also

also는 컨텍스트 객체를argument(it)처럼 사용할 수 있고, 컨텍스트 객체를 반환합니다. also는 컨텍스트 객체를 인자처럼 사용하는 액션에 유용합니다. 또 객체의 프로퍼티나 함수에 대한 참조가 아니라 객체 자체에 대한 참조가 필요한 액션에 유용합니다.

val numbers = mutableListOf("one", "two", "three")
// and also do the following with the object.
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

정리

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

takeIf and takeUnless

추가적으로 Scope Function에는 takeIftakeUnless도 존재합니다. 이 함수들은 call chain에 객체의 상태를 확인하는 작업을 추가하는 함수입니다. takeIf는 조건문을 만족하면 해당 객체를 반환하며, 그렇지 않으면 null을 반환합니다. takeUnlesstakeIf와 반대로 동작합니다. 이 두 함수는 컨텍스트 객체를 argument(it)처럼 사용합니다.

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

두 함수가 null을 반환할 수 있기 때문에 반환된 값을 사용하려는 경우 null check를 해야합니다. 또 두 함수는 다른 Scope Function과 함께 사용할 때 유용합니다.

fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}

📖 Reference

Kotlin Language - Scope Functions

profile
안드로이드 개발자를 꿈꾸는 사람

0개의 댓글