몰?.루();

코틀린 스코프 함수 정리 (apply, also, let, run, with) 본문

프로그래밍/안드로이드, 코틀린

코틀린 스코프 함수 정리 (apply, also, let, run, with)

toonraon 2022. 12. 8. 18:50

코틀린에는 스코프 함수 5형제가 있습니다.

알아두면 매우 유용하지만 그만큼 헷갈리는 친구들이기도 하기 때문에 정리해보겠습니다.

 

키워드 객체 참조 리턴값
.let it 마지막 줄
.also it 참조 객체
.run this 마지막 줄
with this 마지막 줄
.apply this 참조 객체

 

이미 스코프 함수를 아시는데 잠깐 까먹은 분들은 위의 표만으로 충분하지만

처음보면 뭔 소린지 알 수 없기 때문에 예시를 들어보겠습니다.

 

.let

let의 객체 참조는 it을 쓰고 리턴값은 스코프 마지막 줄입니다.

 

보통 null-check를 할 때 많이 씁니다

let을 사용하지 않고는 코드를 다음과 같이 써야합니다.

    val num: Int? = getNumberFromServer() // 서버로부터 오는 값
    var isNumOdd = false                  // num은 홀수인가?

    if (num != null) {
        if (num % 2 == 0) {
            isNumOdd = false
        } else {
            isNumOdd = true
        }
    }

보다시피 num이라는 변수에 서버로부터 받은 값을 받아 넣는데 이게 Int일 수도 있고 null일 수도 있습니다.

 

그렇기 때문에 if문으로 num != null인지 아닌지 체크를 하고 나서야 num을 2로 나눠서 짝순지 홀순지 파악할 수 있었습니다.

 

하지만 let을 쓴다면 쉽게 할 수 있습니다.

    val num: Int? = getNumberFromServer() // 서버로부터 오는 값
    var isNumOdd = num?.let {
        it % 2 == 1
    }

let의 객체 참조는 it 키워드를 쓰기 때문에 num % 2 대신 it % 2를 사용할 수 있습니다

또한 let은 마지막 줄이 자동으로 return 되기 때문에 return 할 필요 없이 그냥 저렇게 써놓으면 num이 홀수면 true가, 짝수면 false가 알아서 return되어서 isNumOdd에 저장됩니다.

 

게다가 num.let이 아니라 num?.let으로 Optional Chaining을 사용하였기 때문에 num이 만약 null이라면 아예 let 안의 구문을 실행하지 않고 null을 return합니다.

따라서 num이 null이면 isNumOdd도 null입니다.

 

그래서 사실 let만으로는 null-check를 할 수 없습니다. 하지만 ?.let을 사용하여 Optional Chaining을 같이 써서 null-check를 겸하는 경우가 대부분이며, 동시에 원하는 일을 해주는(위의 코드에서는 num이 홀수인지 판별하는) 용도로 사용하는 경우가 대부분입니다.

 

.also

also의 객체 참조는 it을 쓰고 리턴값은 객체 자체(it)입니다.

 

    var a = 1
    var b = 2

    a = b.also { b = a }

    println("$a $b") // 2 1 출력

흔히 swap 함수 만들 때 temp 변수 만들어서 쓰는데 그러지 않고도 .also를 이용하여 swap 할 수 있습니다.

 

어떻게 temp 없이도 두 변수의 값을 바꿀 수 있었냐면 코틀린은 call-by-value를 사용하며, .also의 리턴값이 객체 자체이기 때문입니다.

 

a = b.also { b = a }에서 무슨 일이 일어나냐면

 

먼저 b.also 이므로 also 안에 b의 값인 2가 인자로 넘어갑니다. b의 주소값이 아니라 2라는 값 자체를 복사해서 인자로 넘어간다는 뜻입니다. 코틀린은 call-by-value이기 때문입니다.

 

이 인자로 넘어간 값은 it에 저장됩니다. also의 객체 참조 방법은 this가 아니라 it이니까요.

즉, it이라는 자동으로 생성된 변수에 2가 저장되어 있는 상태입니다.

 

그 상황에서 also 안의 b = a를 실행합니다.

그럼 b == 1, a == 1이 됩니다.

 

.also 문이 끝나면서 it이 리턴됩니다. also의 리턴값은 마지막 줄이 아니라 참조 객체인 it이기 때문입니다.

그러면 결국 a = b.also { b = a }가 a = it이 되고 이건 a = 2라는 코드와 같기 때문에

a == 2, b == 1이 되며 마무리 됩니다.

 

.run

객체 참조는 this로 하며 리턴 값은 마지막 줄입니다.

 

솔직히 가장 안 씁니다. 저만 안 쓰는가 했더니 github에서 검색해봐도 run이 제일 안 쓰이는 거 같더라구요. 참고로 제일 많이 쓰는 건 역시 null-check용으로 자주 쓰이는 .let 키워드.

 

대충 사용 방법만 알아보고 가자면

        val buttonId = findViewById<Button>(R.id.myButton).run {
            text = "눌러보세요."
            setBackgroundColor(Color.RED)
            setOnClickListener { println("버튼이 눌러졌습니다.") }
            id // 리턴값
        }

뭐 대충 이런 식으로 myButton이라는 버튼에 텍스트, 배경색, 클릭리스너를 설정하고 마지막 줄에 있는 id가 리턴되어

buttonId에 저장됩니다.

 

with

객체 참조는 this로 하고 리턴값은 마지막 줄입니다.

이것만 보면 위의 .run과 동일하지만 다른 점이 있다면 with는 다른 함수와 달리 확장 함수가 아니라는 점입니다.

 

제가 다른 4가지 스코프 함수엔 .run, .let 이런 식으로 앞에 .을 붙이는데 with 앞엔 .을 붙이지 않는 이유가 이것 때문입니다.

 

즉 변수.with 이런식으로 쓰는 게 아니라 with(변수) { ... } 이런 식으로 쓰는 친구입니다.

백준에서 알고리즘 문제 풀 때 특히 자주 쓰이는 친구입니다.

 

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()

    // 대충 알고리즘 문제 푸는 코드
}

뭐 이런 식으로 씁니다. readLine()은 참고로 BufferedReader의 함수인데 with의 파라미터에 써놓은 BufferedReader가 with 스코프 안에서 this로 참조할 수 있기 때문에

bufferedReader.readLine()이라고 써야하는 걸 this.readLine()으로 쓸 수 있고,

this는 생략할 수 있기 때문에 this.readLine()을 그냥 readLine()으로 간단하게 쓴 모습입니다.

 

.apply

객체 참조는 this로 하고 리턴값은 객체 자체(this)입니다.

 

        val myButton = findViewById<Button>(R.id.myButton).apply {
            text = "눌러보세요."
            setBackgroundColor(Color.RED)
            setOnClickListener { println("버튼이 눌러졌습니다.") }
        }

이런 식으로 자주 사용 됩니다.

 

보시다시피 findViewById로 버튼을 XML에서 찾아서 myButton에 할당함과 동시에

myButton에 텍스트, 배경색, 클릭 리스너도 지정해주는 모습입니다.

그러면서 apply의 리턴값이 객체 자체이기 때문에 apply가 끝나면 this에 해당하는 버튼이 return 되어 myButton에 저장됩니다.

Comments