Scope Functions은 객체의 컨텍스트 내에서 코드 블록을 실행할 수 있게 하는 함수입니다. lambda expression
이 제공된 객체에서 Scope Function
을 호출하면 해당 함수는 일시적인 범위를 형성하며, 이 범위에서는 객체 이름 없이 객체에 접근할 수 있습니다.
Scope Function
의 일반적인 형태는 다음과 같습니다.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
만약 Scope Function
을 사용하지 않는다면 위의 코드는 아래와 같이 작성되어야 합니다.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
Scope Function
은 새로운 기술 기능을 도입하지는 않지만 코드를 보다 간결하고 읽기 쉽게 만들 수 있습니다.
Kotlin에서 제공하는 Scope Function
으로는 let
, run
, with
, apply
그리고 also
가 존재합니다. 기본적으로 이 함수들은 객체의 코드 블럭을 실행한다는 공통점을 갖습니다. 차이점은 코드 블럭 내에서 객체에 어떻게 접근하는지와 블럭 내 코드의 결과가 무엇이냐입니다.
this
or it
Scope Function
의 lambda expression
내부에서 컨텍스트 객체는 실제 이름 대신 짧은 참조를 통해 사용가능합니다. 이 짧은 참조는 lambda receiver
인 this
혹은 lambda argument
인 it
입니다.
fun main() {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // does the same
}
// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
this
run
, with
그리고 apply
는 컨텍스트 객체를 this
를 통해 사용합니다. 그래서 객체를 일반적인 클래스 함수 내부에서 사용하는 것처럼 사용할 수 있습니다. 대부분의 경우 객체 멤버에 할당을 할 때 this
를 생략할 수 있습니다. 다만 외부 객체나 함수와 구분하기 힘들어지므로 this
를 사용하는 것을 권장합니다.
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20 or adam.age = 20
city = "London"
}
it
let
과 also
는 컨텍스트 객체를 lambda argument
처럼 가지고 있습니다. 따라서 it
을 통해 컨텍스트 객체에 접근할 수 있습니다. it
은 this
보다 짧고 it
을 사용한 표현식은 보통 읽기 쉽습니다. 하지만 객체의 함수나 프로퍼티를 호출할 때 this
처럼 암시적으로 사용할 수는 없습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
추가로 컨텍스트 객체를 argument
로 전달한다면 컨텍스트 객체에 이름을 부여할 수 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
Scope Function
은 코드 블럭이 반환하는 결과에서 차이를 보입니다. 따라서 Scope Function
을 실행한 뒤 어떤 작업을 하냐에 따라 사용할 Scope Function
을 결정합니다.
apply
와 also
는 해당 코드 블럭의 컨텍스트 객체를 반환합니다. 따라서 이 두 함수는 같은 객체에 대한 call chain
을 가질 수 있습니다.
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()
또한 이들은 return 문에
사용될 수 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
let
, run
그리고 with
는 lambda result
를 반환합니다. 따라서 이 두 함수의 결과는 변수에 할당될 수 있습니다.
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.")
추가적으로 lambda result
는 무시될 수 있으며, Scope Function
을 변수에 대한 임시 범위를 생성하는데 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
let
let
은 컨텍스트 객체를 argument
(it
)처럼 사용할 수 있고 lambda result
를 반환합니다. let
은 호출 체인의 결과에 대해 하나 이상의 함수를 호출하는데 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
만약 코드 블럭이 it
을 인자로 가지는 함수 하나만 담고 있다면 lambda expression
대신 ::
을 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let
은 종종 non-null
객체에 코드 블럭을 생성할 때 사용합니다. 또 코드 가독성을 높이기 위해 코드 블럭에 지역 변수를 제공할 때 사용합니다.
with
with
는 컨텍스트 객체를 argument
로 전달하지만 내부에선 this
를 통해 사용가능합니다. 그리고 lambda result
를 반환합니다. with
는 코드 블럭 내에서 lambda result
반환없이 함수를 호출할 때 사용할 것을 추천합니다.
val numbers = mutableListOf("one", "two", "three")
// with this object, do the following.
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
run
run
은 컨텍스트 객체를 this
를 통해 사용하며, lambda result
를 반환합니다. run
은 코드 블럭 내에 객체 초기화와 반환 값 계산을 둘다 담고 있는 경우 유용합니다.
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
apply
apply
는 컨텍스트 객체를 this
를 통해 사용하며, 컨텍스트 객체를 반환합니다. apply
는 코드 블럭이 값을 반환하지 않고 주로 컨텍스트 객체의 멤버들로 동작하는 경우 사용합니다. 이에 해당하는 예시로는 객체 설정이 있습니다.
// apply the following assignments to the object.
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
also
는 컨텍스트 객체를argument
(it
)처럼 사용할 수 있고, 컨텍스트 객체를 반환합니다. also
는 컨텍스트 객체를 인자처럼 사용하는 액션에 유용합니다. 또 객체의 프로퍼티나 함수에 대한 참조가 아니라 객체 자체에 대한 참조가 필요한 액션에 유용합니다.
val numbers = mutableListOf("one", "two", "three")
// and also do the following with the object.
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
Function | Object reference | Return value | Is extension function |
---|---|---|---|
let | it | Lambda result | Yes |
run | this | Lambda result | Yes |
run | - | Lambda result | No: called without the context object |
with | this | Lambda result | No: takes the context object as an argument. |
apply | this | Context object | Yes |
also | it | Context object | Yes |
takeIf
and takeUnless
추가적으로 Scope Function
에는 takeIf
와 takeUnless
도 존재합니다. 이 함수들은 call chain
에 객체의 상태를 확인하는 작업을 추가하는 함수입니다. takeIf
는 조건문을 만족하면 해당 객체를 반환하며, 그렇지 않으면 null
을 반환합니다. takeUnless
는 takeIf
와 반대로 동작합니다. 이 두 함수는 컨텍스트 객체를 argument
(it
)처럼 사용합니다.
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
두 함수가 null
을 반환할 수 있기 때문에 반환된 값을 사용하려는 경우 null check
를 해야합니다. 또 두 함수는 다른 Scope Function
과 함께 사용할 때 유용합니다.
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}