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

SSY·2023년 1월 29일
0

Kotlin

목록 보기
1/8

😀시작하며

코틀린이라는 언어를 다루면서 '이것만큼은 꼭 알아야 한다'라는 개념이 있다. 참 여러가지가 있지만 그중 생각나는 것 중 하나는 바로 '코틀린 표준 스코프 함수'이다. apply, run, let, also + with가 있는데 이 포스팅에 모두 정리해버릴까 한다. 그리고 with는 따로 +로 분리해 놓았는데 그럴만한 이유가 있다. 그 이유도 글을 쓰며 정리하려 한다.

문법적인 개념도 함께 짚어 나아갈까 한다. 그래서 이 글을 통해 Kotlin언어의 문법적인 부분에서도 많은 도움이 될 수 있을거라 생각한다.

👌이해를 쉽게 하기 위해

앞으로 위 4가지 함수를 설명하기에 앞서 아래 4문장만 사용할 것이다.
(이 4문장들은 2 x 2형식을 통해 4가지가 된다.)

[람다 내부 파라미터 유형?]

  • 람다 내부에서 수신객체를 receiver형태로 암시적으로 전달받는다. ( = this )
  • 람다 내부에서 수신객체를 parameter형태로 명시적으로 전달받는다. ( = it )

[반환 유형?]

  • 람다 내부에서 초기화된 수신객체 자체를 반환한다.
  • 람다 내부의 마지막 라인을 반환한다.

🦶공통점

코틀린 표준 스코프 함수는 모두 공통점이 있다. 우선 첫 번쨰로는 이녀석들은 모두 확장함수를 통해 수신객체를 받는다라는 점이다. 코드로 보면 다음과 같다.

"hello".apply {  }
"hello".run {  }
"hello".also {  }
"hello".let {  }

또한 람다 내부에서 수신객체를 받는다는 점이다. 아래와 같이 말이다.

잠깐
사진을 통해 볼 수 있듯이, 이 4가지 확장함수의 차이는 람다 내부에 수신객체 전달 방식반환 방식에 있다. 이 부분은 아래에서 세부적으로 다뤄보려 한다.

하지만 여기서 with라는놈이 빠졌다. 그렇다. with라는 함수는 확장함수를 통해 수신객체를 받지 않는다. 첫 번째 파라미터를 통해서 수신객체를 부여받는다. 아래와 같이 말이다.

with("hello") { }

해당 포스팅의 전개 방식우선 with를 맨 마지막에 다루려 한다. apply, run, also, let을 이해하면 금방 이해할 수 있을거라 믿기에

👉apply

개념

  • 람다 내부에서 수신객체를 receiver형태로 암시적으로 전달받는다. ( = this )
  • 람다 내부에서 초기화된 수신객체 자체를 반환한다.

우선 apply함수는 1개의 Generic타입만 사용되며 수신객체에서 처음으로 사용되고 있다.

그리고 여기서 block이라는 parameter는 타입이 함수형 타입이다. 또한 그 함수형 타입은 확장함수 형태를 취하고 있으며 반환은 Unit(=void)를 반환하고 있다.

apply함수 자체의 반환 유형은 바로 T라는 수신 객체 타입과 동일하다. 즉, apply는 수신 받은 객체를 그대로 반환한다는 뜻이다.

이때, block 람다 내부에선 수신객체를 receiver형태로 암시적으로 접근할 수 있다 ( = this ) 그리고 그 안에서 T라는 객체에 변형을 가할 수 있다. ( = call by reference )

그리고 동기적으로 block 람다 수행이 끝나면 곧바로 'this'를 반환하고 있다. 즉, 수신 객체로 받은 T를 반환한다는 뜻이다.

쓰임새

val list = mutableListOf("hello1", "hello2")
            Log.i("scopeTest", "before : $list")
            list.apply {
                add("hello3")
                add("hello4")
            }
            Log.i("scopeTest", "after : $list")

결과

Call By Reference로 인해 'list'변수 내에 value들이 누적되는걸 볼 수 있다.

🤟run

개념

  • 람다 내부에서 수신객체를 receiver형태로 암시적으로 전달받는다. ( = this )
  • 람다 내부의 마지막 라인을 반환한다.

run함수를 한번 보자... 우선 2개의 Generic타입을 쓰고 있다. 그리고 첫 번째 Generic타입인 T를 run의 수신객체 타입으로 받고 있다.

그리고 block도 위 apply와 마찬가지로 자료형이 함수형이다. 그리고 그 함수가 확장 함수형태를 띄고 있다. 여기까지는 apply와 동일하다. 다만, 차이는 block함수의 반환 형태의 차이이다. block함수는 바로 '람다 내부에서 정의된 마지막 line의 값'을 반환하고 있다. 그리고 그 자료형을 Generic타입인 'R'로 칭하고 있다.

이제 run함수의 return문을 보자. run 함수는 block함수를 반환하고 있다. 즉, block함수가 반환하는 값이 그대로 run함수의 반환값이 된다는 뜻이다. 그러니 당연하게도 run함수의 반환 유형은 바로 2번째 Generic타입인 'R'로 선언되어 있다.

쓰임새

val resultStr = "hello".run { 
                "hello new Str0"
                "hello new Str1"
                "hello new Str2"
            }
            Log.i("scopeTest", "$resultStr")

**결과값

즉, run 람다 함수 내부가 어떻게 정의되든 상관 없이 마지막 라인이 반환되는걸 볼 수 있다.

이제 다 왔다. 여기까지 이해했으면 90%는 이해했다고 보면 된다. 나머지들도 다 이 원리들을 우려먹는거나 다름 없다.

✍️let

개념

  • 람다 내부에서 수신객체를 parameter형태로 명시적으로 전달받는다. ( = it )
  • 람다 내부의 마지막 라인을 반환한다.

let을 보자... 이녀석도 run처럼 2개의 Generic타입을 받고 있다. 그리고 그 중 T라는 타입은 let함수의 수신 객체 타입이다.

그리고 그 타입이 block이라는 함수의 인자 타입으로 쏙 들어가 있다. 하지만 run과 apply와 다른 점은 block함수가 확장 함수 타입이 아니라는 점이다. apply와 run의 경우 T.()형태의 인자였지만, let의 경우는 파라미터에 타입이 선언되어 있다. (T)형태로 말이다.

반환형도 run과 마찬가지이다. block이 반환하는 값(=block함수의 가장 마지막 line)을 그대로 반환하고 있는 것이다.

같은 원리로 block의 반환 유형( = R )을 let함수도 똑같이 반환하고 있다.

쓰임새

val resultStr = "hello".let {
                Log.i("scopeTest", "let 내부 : $it")
                "new Str"
            }
            Log.i("scopeTest", "$resultStr")

결과값

👌also

개념

  • 람다 내부에서 수신객체를 parameter형태로 명시적으로 전달받는다. ( = it )
  • 람다 내부에서 초기화된 수신객체 자체를 반환한다.

위 3가지 개념을 이해했으면 also도 이해하기가 매우 쉽다. 우선 also는 apply처럼 Generic타입이 한개이다.

그리고 그 타입을 block의 함수 파라미터에서 받는 형태를 취해주고 있다.

또한 also의 return타입을 보자. 이 타입도 apply와 마찬가지로 T타입의 객체 자체를 반환해주고 있다.

즉, block 람다 함수 내부에서 초기화된 객체 자체를 그대로 call by reference형태로 반환해주고 있다는 뜻이다.

쓰임새와 결과?
여기까지 이해를 했으면 굳이 안보여줘도 된다고 생각한다. 단지 apply의 람다 내부의 파라미터가 it으로 쓰이는것 뺴고 모두 같다.

💪with

개념

  • 수신 객체를 첫 번째 파라미터로 받는다.
  • 람다 내부에서 수신객체를 receiver형태로 암시적으로 전달받는다. ( = this )
  • 람다 내부의 마지막 라인을 반환한다.

이제 with가 왔다. 이 with는 위 4가지 함수와는 좀 다른 형태를 취하고 있다.

위 4가지 함수를 수신객체를 '확장 함수'의 형태로 받았었다. 하지만 with는 그렇지 않다. '첫 번째 파라미터'를 통해 수신객체를 받는다.

그리고 block함수의 경우는 T 자료형 그대로 확장 함수의 형태로 호출되고 있다.
( = apply, run과 동일)

하지만 반환형은 block함수의 마지막 라인이다. (= run, let과 동일)

쓰임새

val resultStr = with(mutableListOf("hello")) {
                add("hello1")
                add("hello2")
                add("hello3")
                "helloNewStr"
            }
            Log.i("scopeTest", resultStr)

결과

👋마치며

개발하다 너무 익숙함에 속아 제대로 중요성을 놓치기 쉬운 5가지 함수에 대해서 알아봤습니다. 위 함수를 제대로 이해하기 위해 내부 구조를 한번 더 관찰해보시면 이해가 훨씬 빠를거라 생각합니다.

여기까지 글을 읽어주신분들 너무 감사합니다.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글