Scope Function
에 대해 구글링을 해보면 정리해놓은 글마다 설명이 제각각이기 때문에 어떻게 사용해야하는지 혼동되기 쉽다.
또한 어떤 차이점이 있는지 정리해놓은 글은 많지만 정작 각각의 함수들이 어느상황에 어떻게 쓰이는지에 대해서는 제대로 알기가 힘들게 적어놓은 글들이 많다.
그래서 답답한 마음에 코틀린 공식 문서를 번역해서 이해해가며 정리해보았다.
(본 내용은 코틀린 공식문서의 내용을 번역한 번역본에 가까우며, 애매한 부분은 예시 코드를 보며 이해한대로 해석해서 적었다.)
kotlin
의 표준 라이브러리에는 Object
의 컨텍스트 내에서 코드들을 실행하기 위한 목적으로 만들어진 함수들이 몇가지 있다.
이러한 함수들은 람다표현식을 사용할 수 있는 객체에 대해서 호출되면 임시적인 Scope(범위)
를 형성하게 된다. 이러한 Scope
내에서 해당 함수를 사용한 객체의 이름을 명시하지 않아도 객체에 접근이 가능하다.
이렇게 Scope
를 생성해주는 함수들을 Scope Function
이라고 하며, Scope Function은 let
, run
, with
, apply
, also
총 5개가 있다.
기본적으로 이 함수들은 모두 하나의 객체에 대해서 코드블록의 내용을 실행하는 공통된 동작을 가지고있다.
차이점은 객체를 블록안에서 어떤 식으로 참조하는가
와 코드블록이 실행되고 난 후 반환되는 결과는 무었인가
이다.
Scope Function
을 사용하는 예는 다음과 같다.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
만약 위의 코드에서 let
함수 없이 똑같이 작동하는 코드를 만들기 위해서는 변수를 새롭게 생성해야하며, 사용할 때 마다 변수명을 반복해야한다.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
Scope Function
은 새로운 기능이나 새로운 기술은 없지만 코드의 가독성을 더 높여준다.
Scope Function
은 서로 비슷한 부분이 많아서 어떤 경우에 어떤 함수를 써야하는지 알기 힘든 경우가 많다.
어떤 함수를 사용하는게 적절한지는 전적으로 해당 프로젝트를 진행하는 팀이나 개발자 개인이 일관성있게 정하는게 적절하지만 이를 위해 기능과 차이점에 대한걸 알기위해 정리한다.
다음 표를 통해 목적에 맞는 Scope Function
을 선택하는데 도움이 되었으면 좋겠다.
각 함수의 기능에 대해서는 아래에서 자세히 정리하기로 한다.
바쁜 사람들을 위해 각 Scope Function
에 대한 기능을 간략하게 소개하자면
non-null
객체에 대해서 람다 식을 실행 : let
Scope
내에서 변수명을 따로 지정해서 사용할때 : let
apply
run
run
also
with
사실 Scope Function
기능 간에 겹치는 부분이 있기때문에 프로젝트에 사용할때 규칙을 정해서 어떻게 쓸지 정하는게 가장 적절하다.
Scope Function
을 사용하면 코드의 가독성이 좋아질 수 있지만 과도하게 사용할 경우 코드 가독성을 더 해칠수 있기때문에 적절하게 사용하는것이 좋다.
또한 Scope Function
을 중첩해서 사용하거나 연속해서 사용할 경우 참조하고 있는 객체가 혼동 될 수 있으며, it
과 this
에 대한 값 역시 혼동될 수 있기때문에 주의해서 사용해야한다.
Scope Function
의 기능은 본질적으로 비슷하기 때문에 각 함수간의 차이점을 알아야 한다. 모든 Scope Function
간에는 두 가지의 큰 차이점이 존재한다.
객체에 Scope Function
을 선언 할 경우 해당 객체는 Scope
내에서 객체의 이름 대신 람다 수신자인 this
나 람다 인수인 it
을 사용해 참조할 수 있다.
두 경우 모두 동일안 기능을 제공하지만 각각의 장단점에 따라 적절하게 사용할 수 있는 방법이 다르기 때문에 적절한 방법을 알아보자.
fun main() { // 둘 다 기능은 똑같다.
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // this.붙인거랑 안붙인거랑 똑같음
}
// it
str.let {
println("The string's length is ${it.length}")
}
}
run
, with
, apply
는 this
를 사용해 객체를 람다 수신자로 참조한다. 그래서 해당 함수들의 블록 내에서는 클래스의 메서드 처럼 객체에 접근하는게 가능하다.
객체의 멤버에 접근할 때 this
를 생략할 수 있기떄문에 코드를 더욱 간결하게 만들 수 있다. 하지만 this
를 생략했을때 외부 변수와 구분이 어려워 지는 경우가 생길 수 있기때문에
주로 객체 내부의 메서드를 호출하거나, 프로퍼티에 값을 할당하는 경우에 사용하는것이 권장된다.
val adam = Person("Adam").apply { // Person으로 생성된 객체의 프로퍼티에 값을 할당
age = 20 // this.age = 20 랑 똑같음
city = "London"
}
println(adam)
let
과 also
는 객체를 람다식의 인자로 참조한다. 만약 인자의 이름을 명시하지 않을 경우 기본적으로 it
으로 접근할 수 있다. it
은 this
보다 짧기 때문에 it
을 사용한 코드는 좀 더 읽기 쉽다.
하지만 it
은 this
처럼 생략이 불가능 하기 때문에 객체의 메서드나 프로퍼티에 접근할 경우 항상 it
을 사용해서 객체를 지정해줘야 한다. 따라서 객체가 함수의 매개변수로 전달되거나 코드블록 내에서 여러 변수를 사용하는 경우 it
을 통해 객체에 접근하는것이 더 효과적이다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
println(i)
객체를 it
이 아닌 다른 이름으로 지정할 경우 아래와 같이 작성이 가능하다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
Scope Function
은 다른 반환 값을 가진다.
apply
와 also
는 객체 자체를 반환한다.let
, run
, with
는 람가 결과값을 반환한다.코드내에서 다음에 실행할 작업이 무언인지에 따라서 반환해야 하는 값이 달라져야 하기 때문에 이에따라 가장 적합한 Scope Function
을 선택할 수 있다.
apply
와 also
는 참조한 객체 자체를 반환 한다. 따라서 이 함수들은 연속적으로 함수가 호출될때 부수적으로 사용할 수 있다. 즉 동일한 객체에 대해서 여러가지 함수를 연속적으로 호출하는게 가능하다.
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() // 정렬
//numberList를 출력할경우 apply에서 값을 추가해서 객체를 반환했고, .sort에서 정렬 됐기 때문에
// [1.0, 2.71, 3.14]로 나온다.
또한 어떤 함수가 객체를 반환하는 경우 return
값에 함께 사용할 수 있다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
let
, run
, 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.")
또한 반환값은 무시한채 Scope
내부에 지역 변수를 선언해 해당 변수에 대한 임시적인 Scope
를 지정할 수 있다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
내용이 길어져 각 함수에 대한 자세한 내용은 Scope Function (2)
에서 설명하도록 하겠다.