본문 바로가기
Development/Kotlin

코틀린(Kotlin) 문법 뽀개기 - 7. 범위 지정 함수, let, also, apply, run, with

by du.it.ddu 2020. 8. 10.
반응형

이번 포스팅은 개인적으로 코틀린을 쓰면서 가장 좋아하는 함수인 범위 지정 함수에 대해 포스팅하겠다.

가장 좋아하고 자주 쓰지만 잘 사용하는 것은 어려운 것 같고 올바른 사용법은 아직도 잘 모르겠다.

코틀린의 공식 문서인 https://kotlinlang.org/docs/reference/scope-functions.html를 참고하는 것을 추천한다.


범위 지정 함수에 대해 공식 문서는 다음과 같이 설명하고 있다.

"The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions."

구글번역기의 도움을 받아 해석해보자.

간단하게 설명하면 어떤 객체의 범위 내에서 어떤 코드 블록(람다식)을 실행하는 확장 함수이다.

그리고 제목에 나와있듯 이 함수들은 let, also, run, apply, with 다섯가지가 존재한다.

다섯가지에 각각에 대해 알아보자.


1. let

fun main() {
    val myCar = Car("My Car", Random().nextInt(100))

    // let
    val letResult: Boolean = myCar.let {
        println("Name :: ${it.name}")
        println("Number :: ${it.number}")

        it.someProcess()
    }
}

class Car(
        var name: String,
        var number: Int
) {
    private var isProcessing: Boolean = false

    fun someProcess(): Boolean {
        isProcessing = true
        return isProcessing
    }
}

임의로 Car 객체를 만들고 let 함수 뒤에 람다를 작성하였다.

람다의 내부에서 myCar 객체는 it 이라는 이름으로 접근 가능하다.

    val letResult: Boolean = myCar.let { car ->
        println("Name :: ${car.name}")
        println("Number :: ${car.number}")

        car.someProcess()
    }

위와 같이 car 라는 이름을 주어 사용할 수도 있다.

또한 let의 결과가 Boolean 인 것이 보이는가? car.someProcess()의 반환형이 Boolean 이기 때문이다.

let 함수의 결과는 람다의 마지막 수행 결과가 된다.


2. also

fun main() {
    val myCar = Car("My Car", Random().nextInt(100))
    
    ...
    
    val alsoResult: Car = myCar.also {
        println("Name :: ${it.name}")
        println("Number :: ${it.number}")

        it.someProcess()
    }
}

이번엔 also이다. 동일하게 람다식을 작성하고 let 처럼 it으로 해당 객체를 접근하고 있다.

하지만 alsoResult의 자료형이 Car 인 것이 보이는가? also의 반환형은 람다를 실행한 객체의 자료형이며 그 객체를 반환한다.


3. apply

fun main() {
    ...
    
    val applyResult: Car = Car("New Car", Random().nextInt(100)).apply {
        name = "Apply name to new car"
        number += 20
        someProcess()
    }
}

이번엔 apply이다. 동일하게 람다식을 사용하였으나, 이번엔 새로운 Car 객체를 생성하였다.

그런데 let,also와는 달리 it이나 어떤 이름이 보이지 않지만 객체의 프로퍼티 혹은 메서드에 접근하고 있다.

apply는 람다를 실행한 객체를 내부에 전달받으며 이를 아래와 같이 this로 접근한다.

fun main() {
    ...
    
    val applyResult: Car = Car("New Car", Random().nextInt(100)).apply {
        this.name = "Apply name to new car"
        this.number += 20
        this.someProcess()
    }
}

그리고 람다 내부 연산을 모두 수행하여 적용된 객체를 반환하게 된다.

즉, applyResult는 이름은 "Apply name to new car", number는 생성할 때 할당받은 값에 20을 더한 값, someProcess() 를 수행한 객체가 된다.


4. run

fun main() {
    val myCar = Car("My Car", Random().nextInt(100))
    ...

    val runResult: Boolean = myCar.run {
        name = "my Car is Running"
        someProcess()
    }
}

이번엔 run이다. apply와 마찬가지로 람다를 수행한 객체를 내부에 전달받고 this로 접근한다.

하지만 반환형이 Boolean이다. run은 let처럼 람다의 마지막 실행 결과를 반환한다.


5. with

fun main() {
    val myCar = Car("My Car", Random().nextInt(100))
    ...
    
    val withResult: Boolean = with(myCar) {
        name = "With my car"
        someProcess()
    }
}

마지막으로 with이다. 위의 함수들과는 달리 with 함수에 객체를 파라미터로 전달한 후에 람다가 오고 있다. 

파라미터로 전달 된 객체를 람다 내부로 전달받고 this로 접근한다.

반환형은 람다의 마지막 실행 결과를 반환하고 있다.


지금까지 범위 지정 함수를 알아보았다.

하지만 뭔가 비슷하면서도 조금씩은 다른 구성과 동작을 하고 있다. 차이점을 정리하면 아래와 같다.

1. 객체의 내부를 it 또는 명시적인 이름으로 접근하는지, this로 접근하는지

2. 람다의 결과를 반환하는지 객체를 반환하는지

3. with는 파라미터로 객체를 전달받는다.

각각의 차이점이 있으니 분명 사용에도 차이점이 있을 것이다. 코틀린의 권장 사용법을 알아보자.


1. let

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

null이 아닌 객체들에 대하여 람다를 실행할 때 혹은 지역 변수의 범위를 제한하고 싶을 때 이다.

위 상황 외에도 람다 내의 결과물을 반환하고 싶을때도 사용할 수 있겠다.

여기서 null이 아닌 객체에 대하여 사용할 때, 아래와 같이 사용할 수 있다.

    var nullableValue: Int? = 5
    nullableValue?.let {
        // ...
    }

위와 같이 작성하면 객체가 null이 아닌 경우 let 내부의 람다를 수행한다.

하지만 불변 변수(immutable variable)에 사용하면 오히려 좋지 않다. 디컴파일을 해 보면 내부에 쓸데없는 변수가 생성된다. 이 경우는 if (a != null) {} 과 같이 작성하는 것이 더 낫다.


2. also

  • Additional effects

also는 람다의 결과가 수신 객체이다. 그렇기 때문에 내부에서 객체의 속성을 변경하지 않고 객체의 값을 검증한다거나 어떤 동작을 하는 경우 사용하도록 한다.


 

3. apply

  • Object configuration

apply는 수신 객체를 그대로 반환한다. 주로 객체를 생성하면서 프로퍼티에 접근하여 초기화할 때 사용할 수 있다.


4. run

  • Object configuration and computing the result: run
  • Running statements where an expression is required: non-extension run

run은 이미 생성된 객체의 프로퍼티에 접근하여 변경하거나 어떤 연산을 할 때 사용할 수 있다. 

또는 여러 지역 변수의 범위를 제한하고 변수들을 통해 수행한 람다의 결과를 얻을 때 사용할 수 있다.


5. with

  • Grouping function calls on an object:

with는 객체의 여러 함수 호출을 묶는 데 사용할 수 있다. 주의점은 객체는 null이 아니어야 한다.


아마 이 글만 봐서는 범위 지정 함수에 대해 이해가기 어려울 것이다.

이를 이해하기 위해서 코틀린의 공식 문서를 한번 쭉 보고 직접 코드를 작성해보며 구글링의 다양한 글들을 보는 것을 권장한다.

반응형