코틀린은 특정 객체의 컨텍스트안에서 코드 블럭을 실행하도록 하는 함수들을 제공한다. 특정 객체에 대해서 영역 함수 (scope function) 들을 사용하면 임시적으로 영역 (scope) 가 형성되며, 영역안에서는 객체의 이름이 없이 컨텍스트 객체에 접근이 가능하다. scope function 에는 let, run, with, apply, also, 이렇게 5개 함수들이 포함되며 이러한 함수들을 사용하여 코드를 더욱 간결하고 가독성 좋게 작성할 수 있다.
1. Scope functions 비교
scope functions 는 기본적으로 코드 블럭을 실행한다는 점에서 동일하지만, 블럭안에서 컨텍스트 객체에 접근하는 방법과 표현식의 결과 등이 서로 다르다.
- Context object: this or it
scope function 에 전달된 람다식 내부에서는 컨텍스트 객체를 이름 대신 짧은 참조를 통해 접근이 가능하다. 각 함수들은 lambda receiver (this) 와 lambda argument (it) 두가지 방식 중 하나를 사용한다. 두 방식 모두 동일한 기능을 제공하기 때문에 사용하는 상황에 유리한 방식을 선택하여 사용할 수 있다.
1) this
run, with, apply 는 컨텍스트 객체를 lambda receiver (this) 로 참조한다. 따라서 함수안에서 일반 클래스 함수와 같이 객체를 사용할 수 있다.
대부분의 경우 컨텍스트 객체의 멤버에 접근할 때 this 를 생략할 수 있는데, 이러한 경우에 외부의 객체나 함수들과 구분이 어렵기 때문에 생략하는 것을 추천하지는 않는다.
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "London"
}
println(adam)
2) it
let, also 는 컨텍스트 객체를 lambda argument 을 통해 참조한다. 만약 인자의 이름이 선언되지 않았다면 기본적으로 'it' 이라는 이름을 기본값으로 가지게 된다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
println(i)
아래의 예시는 'it' 을 사용하는 것이 아니라 람다식 인자의 이름을 'value' 라고 따로 선언하여 사용한 코드이다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
- Return value
각 scope functions 는 반환값에서도 차이를 가지고 있는데, apply 와 also 는 컨텍스트 객체를 반환하고, let, run, with 은 람다식의 결과를 반환한다.
1) context object
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()
2) lamdba result
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.")
2. Scope Functions
- let
fun<T,R>T.let(block:(T)->R):R
let 함수는 컨텍스트 객체를 argument (it) 으로 참조하고 람다식 결과를 반환한다.
let 은 함수 호출 체인에 이어서 호출할 수 있다. 아래의 예제는 filter() 함수 뒤에 let 을 호출하여 실행하는 에제이다. 만약 let 에 포함되는 코드 블럭이 'it' 을 인자로 사용하는 단일 함수만 존재한다면 람다식 대신 메서드 참조자인 '::' 를 사용하여 호출할 수 있다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
numbers.map { it.length }.filter { it > 3 }.let(::println)
let 은 주로 non-null values 를 포함하는 코드 블럭을 실행하는데 사용된다. non-null 객체에 대해 작업을 수행하기 뒤해서는 safe call 연산자인 '?.' 를 사용하고 let 함수를 호출하도록 한다.
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
- with
fun<T,R>with(receiver:T,block:T.()->R):R
with 함수는 컨텍스트 객체에 receiver (this) 으로 참조하고 람다식 결과를 반환한다.
with 은 extension function 이 아니기 때문에 객체에 '.' 으로 호출할 수 없다. 아래의 예제와 같이 컨텍스트 객체를 인자로 받아서 수행하는데, 람다식 안에서는 this 로 객체를 참조할 수 있다. with 함수는 함수의 반환 결과를 사용하지 않는 경우에 사용하는 것을 추천한다. 코드에서 with 은 "이 객체를 이용하여 다음을 수행합니다." 라고 읽을 수 있다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
- run
fun<T,R>T.run(block:T.()->R):R
run 함수는 컨텍스트 객체에 receiver (this) 으로 참조하고 람다식 결과를 반환한다.
run 은 with 과 동일하지만 extension function 이기 때문에 객체에 '.' 으로 호출할 수 있다. 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}")
}
run 함수는 non-extension 함수로도 사용할 수 있다. non-extension 으로 사용하는 경우 컨텍스트 객체는 없지만 여전히 람다식 결과를 반환할 수 있다. run 은 코드상에서 "코드 블럭을 실행하고 결과를 계산한다" 라고 읽을 수 있다.
fun<R>run(block:()->R):R
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
fun<T>T.apply(block:T.()->Unit):T
apply 함수는 컨텍스트 객체를 receiver (this) 로 참조하고 객체 자체를 반환한다.
apply 는 컨텍스트 객체를 결과값으로 반환하기 때문에 값을 반환하지 않고 수신 객체의 멤버들에 대해서 동작하는 코드 블럭에 사용하는 것을 추천한다. 주로 객체의 설정과 같은 상황에 사용된다. 이때문에 apply 는 "객체에 다음 할당을 적용한다" 라는 의미를 가진다.
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
- also
fun<T>T.also(block:(T)->Unit):T
also 함수는 컨텍스트 겍체를 argument (it) 으로 참조하고 객체 자체를 반환한다.
also 는 컨텍스트 객체를 인자로 받아서 수행하는 작업에 유용하다. 객체의 속성이나 함수보다는 객체 그 자체를 참조하는 구문에 사용하는 것을 추천한다.
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
3. takeIf and takeUnless
추가적인 scope function 으로 기본 라이브러리는 takeIf 와 takeUnless 함수를 제공하고 있다. 이 함수들은 함수 호출 체인에서 객체의 상태를 체크하는데 사용된다.
fun<T>T.takeIf(predicate:(T)->Boolean):T?
fun<T>T.takeUnless(predicate:(T)->Boolean):T?
객체에서 takeIf 를 호출하면, takeIf 내부의 로직을 수행하여 이를 만족하면 해당 객체를 반환하고 아닌 경우 null 을 반환한다. takeUnless 는 takeIf 의 반대로 로직을 만족하면 null 을 그렇지 않으면 객체를 반환한다.
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
[Reference]
- https://kotlinlang.org/docs/scope-functions.html
- https://blog.yena.io/studynote/2020/04/15/Kotlin-Scope-Functions.html
'프로그래밍언어 > Kotlin' 카테고리의 다른 글
[Kotlin] 튜토리얼 따라가기 (5) (Null safety) (0) | 2024.07.31 |
---|---|
[Kotlin] 튜토리얼 따라가기 (4) (Classes) (0) | 2024.07.29 |
[Kotlin] 튜토리얼 따라가기 (3) (Functions) (1) | 2024.07.26 |
[Kotlin] 튜토리얼 따라가기 (2) (Control flow) (0) | 2024.07.25 |
[Kotlin] 튜토리얼 따라가기 (1) (Hello World, Variable, Collection) (2) | 2024.07.23 |