[Kotlin]스코프 함수

hyejin·2022년 4월 12일
1

표

목적

  • 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

코틀린 스코프 함수는 객체의 context내에서 코드를 실행하는 유일한 목적을 갖고 있습니다
람다식을 갖고있는 객체에대해 이런한 함수를 call할 때, 임시적인 스코프로 형성합니다.스코프 안에서 객체를 객체를 표현하는 이름 없이 접근 할 수 있습니다. 이런 함수를 스코프 함수라 부르고 종류는 5개입니다.

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)

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

스코프 함수의 유사한 특성때문에 이들 사이의 주요 차이점을 아는 것이 중요합니다
주요 차이점은 두가지가 있습니다.

  1. context 객체를 참조하는 방법
  2. 리턴 값

Context Object: this or it

람다 스코프 함수안에 contex 객체는 그것의 실제 이름을 사용하는 대신에 짧은 참조를 이용될 수 있습니다. 각 스코프 함수는 context object에 접근하는 방법인 두개 중 하나를 사용합니다.
this: 람다 수신자
it : 람다 변수

각각 같은 기능을 제공합니다. 이제 각각 상황에 대한 장 단점을 설명하고, 그것들을 추천사용법을 알려드리겠습니다.

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

The string's length: 5
The string's length is 5

this

run, with 그리고 apply는 context this 키워드로 부터 객체를 lamda 수신자로 참조 합니다.
그러므로 람다안에서 객체는 일반 클래스 함수안에서 사용 할 수 있습니다.
대부분의 경우 수신객체의 멤버에 접근할때 this를 제거할 수 있습니다. 그러나 this는 수신객체와 외부 객체 또는 함수를 구별 하기 어렵습니다. 그래서 context object를 수신객체인 this로 갖는 것은 주로 객체의 멤버를 계산하는 람다식(객체의 함수의 호출, 또는 프로퍼티에 값 할당)에 추천합니다.

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

Person(name=Adam, age=20, city=London)

    data class Apple(var weight: Int)
        class AppleTree(val appleTree: List<Apple>) {
            fun pick(): Apple {
                return appleTree[0]
            }
        }
//여기서 apply를 사용하는 경우 this는 가려지게 되고 this.weight은 fruteBasket이 아닌 apple을 참조한다.
        class FruitBasket {
            private var weight = 0

            fun addFrom(appleTree: AppleTree) {
                val apple = appleTree.pick().let { it ->
                    this.weight += it.weight
                    add(it)
                }

            }

            fun add(apple: Apple) {

            }
        }

it

결국 let, also는 context 객체를 lamda 변수로 갖습니다. 만약 변수이름이 명시되지 않았다면, 객체는 암시적 기본 이름인 it으로 접근할 수 있습니다.
it은 this보다 더 짧고 it을 갖을 표현은 읽기 편합니다. 하지만 객체의 함수나 프로퍼티들을 호출할 때 this처럼 암시적으로 이용할 수 있는 객체가 없습니다. 그러나 context 객체를 it으로 갖는 것은 객체가 함수 호출 안에서 변수로 사용될때 더 좋 습니다. it은 또한 코드 블록의 다양한 변수를 사용할때 더 좋습니다.

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

val i = getRandomInt()
println(i)

INFO: getRandomInt() generated value 99
99

추가적으로 context obejct를 변수로 전달할때 스코프 안에서 context object의 custom name을 전달 할 수 있습니다.

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

val i = getRandomInt()
println(i)

return value

스코프 함수는 리턴되는 값에 따라 다릅니다

  1. apply, also는 context 객체를 리턴
  2. let, run, 그리고 with은 lamda의 결과를 리턴

Context Obejct

apply와 also는 자기자신의 context 객체를 리턴합니다. 그러므로 그것은 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()

또한 context 객체를 리턴하는 함수의 문으로 사용될 수 있습니다.

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

val i = getRandomInt()

INFO: getRandomInt() generated value 55

Lamda result

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

There are 3 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")
}

First item: one, last item: three

Funtion

일반적이 사용 스타일을 정의하는 규칙을 아래 예제에서 확인 가능 합니다

let

호출 체인의 결과에 대해 하나 이상의 함수를 호출할 수 있습니다. 예를 들어, 다음 코드는 컬렉션에 대한 두 가지 작업의 결과를 출력 합니다.

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

[5, 4, 4]

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
} 

[5, 4, 4]

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
}

let() called on Hello

또한 코드 가독성을 높이기 위해서 제한된 범위의 로컬변수로도 사용합니다. 이를 위해서는 it대신 lambda 변수 이름으로 제공해야 합니다.

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

The first item of the list is 'one'
First item after modifications: '!ONE!'

with

비확장 함수 : context 객체는 인수로 전달되지만 lambda 내부에 수신기로 사용할 수있습니다. 반환 값은 lambda 결과 입니다. 람다 결과를 제공하지 않고 컨텍스트 객체의 함수, 프로퍼티를 호출하려면 사용하는 것이 좋습니다. 코드에서는 "이 개체를 사용하여 다음을 수행합니다"로 읽습니다.

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

'with' is called with argument [one, two, three]
It contains 3 elements

또 다른 사용 사례는 값을 계산하는 데 사용할 프로퍼티 또는 함수를 사용하는 도우미 개체를 도입하는 것입니다.

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

The first element is one, the last element is three

run

컨텍스트 객체를 lambda 수신기로 사용할 수 있습니다. 반환 값은 람다 결과입니다. 실행은 let과 동일한 기능을 수행하지만 컨텍스트 개체의 확장 함수로 let을 호출합니다.(추가적인 연산을 진행하고 그값을 반환하는..) 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}")
}

Result for query 'Default request to port 8080'
Result for query 'Default request to port 8080'

확장자가 아닌 함수로 사용할 수 있습니다. 비확장 실행을 사용하면 식이 필요한 경우 여러 문의 블록을 실행할 수 있습니다.

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

+123
-FFFF
88

apply

값을 반환하지 않고 주로 수신기 객체의 멤버에서 작동하는 코드 블록에 대해 apply를 사용합니다. 일반적인 경우는 객체 구성입니다. 이러한 호출은 "객체에 다음 할당을 적용"으로 읽을 수 있습니다. 또한 chaing도 적용할 수 있습니다.

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

Person(name=Adam, age=32, city=London)

also

객체를 인수로 사용하고 객체를 반환 합니다. 객체의 속성 및 기능 대신 참조 객체의 참조가 필요한 작업이나 외부 범위에서 this에 의해 가려지지 않을려면 사용합니다. 코드에서도 "객체에 대해 다음 작업을 수행할 수도 있습니다."라고 읽을 수 있습니다.

l 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]

참고

https://kotlinlang.org/docs/scope-functions.html#function-selection

0개의 댓글