본문으로 바로가기

Kotlin의 유용한 함수들 (scope functions)

category 개발이야기/Kotlin 2017. 11. 21. 16:38
반응형


Kotline에서는 내부적으로 편리한 function들을 사용합니다.


따라서 java에서 코딩하는것보다 좀더 심플하게 코드를 작성할 수 있게 도와주죠.

사용성이 높은 함수중에 미묘한? 차이로 사용법이 헷깔리는 함수둘에 대한 정리를 해보고자 합니다.


Scoping functions

정식 명칭은 아닙니다만, 아래 함수들은 사용법이 약간 헷깔립니다.

run, with, T.run, T.let, T.also and T.apply


fun test() {

    var mood = "I am sad"


    run {

        val mood = "I am happy"

        println(mood) // I am happy

    }

    println(mood)  // I am sad

}


위와 같이 사용한다면 test() 안에서 run으로 또다른 scope을 정의할 수 있습니다.

사실 위 예제만 봐서는 run{}은 그다지 쓸모가 없어 보입니다.

그저 scope만 나누는것 처럼 보이기 때문이죠.


하지만 run{}은 블록안에서 처리되고 난 마지막 객체를 리턴하도록 되어 있습니다.

따라서 아래와 같이 두번 호출없이 심플하게 코드 작성이 가능합니다.

run {

        if (firstTimeView) introView else normalView

    }.show()


3 attributes of scoping functions

scoping function을 세가지 속성으로 나눠볼 수 있는데요, 아래와 같습니다.


1. Normal vs. extension function

withT.run 함수는 사실상 거의 비슷해 보입니다.

with(webview.settings) {

    javaScriptEnabled = true

    databaseEnabled = true

}


// similarly

webview.settings.run {

    javaScriptEnabled = true

    databaseEnabled = true

}

위 두가지는 똑같은 코드 입니다.

다른점이라 하면 with는 일반적인 함수(normal function)이고 T.run은 확장함수(extension function)이라는 겁니다.

쓰는 형태의 차이인거죠.


하지만 webview.settings가 null이라면 어떨까요?

// Yack!

with(webview.settings) {

      this?.javaScriptEnabled = true

      this?.databaseEnabled = true

   }

}


// Nice.

webview.settings?.run {

    javaScriptEnabled = true

    databaseEnabled = true

}

extension function인 T.run을 사용할 경우, 실제 코드 수행전에 nullability를 체크할 수 있습니다.


2. This vs. it argument

T.runT.let에 대해서 비교해 보겠습니다.

사실 이 두개는  뭘써도 상관없기도 하지만, 사용할 때 두개의 차이가 뭔지 헷깔리기도 합니다.

stringVariable?.run {

      println("The length of this String is $length")

}


// Similarly.

stringVariable?.let {

      println("The length of this String is ${it.length}")

}

T.run의 signature를 보면 block: T.() 로 정의되어  일반적인 extension function 형태 입니다. 

따라서 블럭 내부에서 T를 호출하려면 this 를 사용해야 하죠. 단! this는 대부분 생략하고 씁니다.

위 코드의 run block내부의 $length${this.length}로 바꿔쓸수 있습니다.


말 그대로 this를 블럭내부에 argument로 넘기는 셈입니다.


하지만 T.let의 signature를 보면 block: (T) 로 정의되어 lambda에서 parameter를 넘기는 것처럼 블럭안으로 T(자기자신)를 넘깁니다.

따라서 it으로 받아서 쓸수 있습니다.


정리하면 let은 블럭내부에 it을 argument로 넘깁니다.


이것만 보면 사실 run이 let보다 좀더 명확하다는 점에서 더 좋아 보입니다만, let은 아래와 같은 장점을 가지고 있습니다.^^

- let은 it을 사용하기 때문에 block내부의 function/member를 좀더 명확하게 구분할 수 있습니다. (외부 클래스와 function과 member와 명확하게 구분 가능)

- this를 생략할수 없어서 parameter로 넘겨야 하는경우 this보다는 it이 좀더 짧게 쓸수 있습니다.

- let은 block 내부에서 it을 좀더 명확한 이름으로 치환해서 쓸수 있습니다. 람다처럼 말이죠!

stringVariable?.let {

      nonNullString ->

      println("The non null string is $nonNullString")

}


3. Return this vs. other type

다음으로 비교해 볼 항목은 T.letT.also 입니다.

이 둘은 block 내부만을 본다면 완전 동일합니다.

stringVariable?.let {

      println("The length of this String is ${it.length}")

}


// Exactly the same as below

stringVariable?.also {

      println("The length of this String is ${it.length}")

}

이 두개의 코드는 완벽하게 동일하게 동작합니다만 좀더 자세히 본다면 return하는 값이 다릅니다.

let은 마지막 수행된 코드의 결과를 리턴하고, also는 블럭안 코드 수행 결과와 상관없이 T를(this) 리턴하는거죠.

따라서 이 특성을 잘이용하면 function을 chaining 하면서 좀더 유용하게 쓸 수 있습니다.

val original = "abc"

// Evolve the value and send to the next chain

original.let {

    println("The original String is $it") // "abc"

    it.reversed() // evolve it as parameter to send to next let

}.let {

    println("The reverse String is $it") // "cba"

    it.length  // can be evolve to other type

}.let {

    println("The length of the String is $it") // 3

}


// Wrong

// Same value is sent in the chain (printed answer is wrong)

original.also {

    println("The original String is $it") // "abc"

    it.reversed() // even if we evolve it, it is useless

}.also {

    println("The reverse String is ${it}") // "abc"

    it.length  // even if we evolve it, it is useless

}.also {

    println("The length of the String is ${it}") // "abc"

}


// Corrected for also (i.e. manipulate as original string

// Same value is sent in the chain 

original.also {

    println("The original String is $it") // "abc"

}.also {

    println("The reverse String is ${it.reversed()}") // "cba"

}.also {

    println("The length of the String is ${it.length}") // 3

}


이 예제만 보면 T.also는 별로 사용성이 없어 보입니다만,

1. object에 대해서 어떤 동작을 완벽하게 분리시킬수 있다는 점을 활용할 수 있습니다.

2. 또한 build pattern 처럼 function chaining을 쓰는경우 object가 사용되기 전에 가공시킬수 있다는 점에서 굉장히 유용합니다.


말이 어렵긴 하지만. "object에 대한 어떤 단위의 동작들을 분리시 킬수 있다.", "builder pattern을 만들기에 적합하다"로 정리해 볼수 있겠습니다.

그리고 이 두가지를 잘 활용한다면 아래와 같은 chaining을 만들 수 있씁니다.

// Normal approach

fun makeDir(path: String): File  {

    val result = File(path)

    result.mkdirs()

    return result

}


// Improved approach

fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }


Looking at all attributes

위에서 언급한 세가지 속성설명중에 언급되지 않았던 T.apply에 대해 얘기해 보겠습니다.


1. Extension function 이면서

2. this를 argument로 받고

3. this를 return 합니다. (T.also 처럼요)

// Normal approach

fun createInstance(args: Bundle) : MyFragment {

    val fragment = MyFragment()

    fragment.arguments = args

    return fragment

}


// Improved approach

fun createInstance(args: Bundle) 

              = MyFragment().apply { arguments = args }


또는 chain이 아닌 형태를 function chain으로 만들수도 있습니다.

// Normal approach

fun createIntent(intentData: String, intentAction: String): Intent {

    val intent = Intent()

    intent.action = intentAction

    intent.data=Uri.parse(intentData)

    return intent

}

// Improved approach, chaining

fun createIntent(intentData: String, intentAction: String) =

        Intent().apply { action = intentAction }

                .apply { data = Uri.parse(intentData) }



정리

표로 정리하면 아래와 같습니다.^^

Functions

블록내 argument

블록내 return 

Function Type

비고

 run()

 this

 블록내 마지막 객체

 normal

 

 with(T)

 this

 Unit

 normal

 

 T.apply()

 this

 T

 extension

 

 T.run()

 this

 블록내 마지막 객체

 extension

 

 T.let()

 it

 블록내 마지막 객체

 extension

it을 블록 내에서 rename 가능함. 

 T.also()

 it

 T 

 extension

 


반응형