•
Kotlin 표준 라이브러리에는 객체의 맥락에서 코드 블록을 실행하기 위한 여러 함수가 있다.
•
이러한 함수는 객체에 대해 호출되고 제공된 람다 표현식이 있을 때 임시 범위를 형성한다.
•
이 범위 내에서 객체의 이름 없이 접근할 수 있다.
•
이러한 함수를 범위 함수라고 한다.
•
이들에는 let, run, with, apply, also 가 있다.
// let과 함께 작성
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)
Kotlin
복사
•
기본적으로 이 함수들은 모두 객체에서 코드 블록을 실행하는 동일한 작업을 수행한다.
•
그러나 객체가 블록 내에서 어떻게 사용되는지와 전체 표현식의 결과가 무엇인지에 따라 차이가 있다.
•
범위 함수 없이 코드를 작성하면 새 변수를 도입해야 하고 사용할 때마다 그 이름을 반복해야 한다.
•
범위 함수는 새로운 기술적 기능을 도입하지 않지만 코드를 더 간결하고 읽기 쉽게 만들 수 있다.
•
범위 함수들 사이의 유사성으로 인해 사용 사례에 맞는 올바른 함수를 선택하는 것이 어려울 수 있다.
•
선택은 주로 의도와 프로젝트 내 일관성에 따라 달라진다.
범위 함수 선택
함수 | 객체 참조 | 반환 값 | 확장 함수 여부 |
let | it | 람다 결과 | 예 |
run | this | 람다 결과 | 예 |
run | - | 람다 결과 | 아니오: 컨텍스트 객체 없이 호출됨 |
with | this | 람다 결과 | 아니오: 컨텍스트 객체를 인수로 받음 |
apply | this | 컨텍스트 객체 | 예 |
also | it | 컨텍스트 객체 | 예 |
•
의도에 따라 범위 함수를 선택하는 간단한 가이드
◦
null이 아닌 객체에서 람다 실행 → let
◦
표현식을 지역 범위의 변수로 도입 → let
◦
객체 구성 → apply
◦
객체 구성 및 결과 계산 → run
◦
표현식이 필요한 문 실행 → 비확장 run
◦
추가 효과 → also
◦
객체에 대한 함수 호출 그룹화 → with
•
범위 함수는 코드를 더 간결하게 만들 수 있지만 과도하게 사용하지 않도록 주의해야 한다.
•
이는 코드의 가독성을 떨어뜨리고 오류를 초래할 수 있다.
•
또한 범위 함수의 중첩을 피하고 체이닝 시 주의하는 것이 좋다.
차이
•
스코프 함수는 유사한 특성을 가지므로 이들 간의 차이를 이해하는 것이 중요하다.
•
각 스코프 함수는 컨텍스트 객체에 대한 참조 방식과 반환 값의 두 가지 주요한 차이점이 존재한다.
컨텍스트 객체에 대한 참조 방식
fun main() {
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // 같은 의미
}
// it
str.let {
println("The string's length is ${it.length}")
}
}
Kotlin
복사
•
스코프 함수에 전달된 람다 안에서는 컨텍스트 객체를 실제 이름 대신 짧은 참조로 사용할 수 있다.
•
각 스코프 함수는 람다 리시버(this) 또는 람다 인자(it)의 두 가지 방법 중 하나로 컨텍스트 객체를 참조한다.
•
두 방법 모두 동일한 기능을 제공하므로 각 사용 사례에 따른 장단점을 설명하고 추천 사항을 제공한다.
this
val adam = Person("Adam").apply {
age = 20 // this.age = 20와 동일
city = "London"
}
println(adam)
Kotlin
복사
•
run, with, apply 는 컨텍스트 객체를 람다 리시버로 참조한다.
•
따라서 람다 안에서 객체는 일반 클래스 함수와 동일하게 this로 사용할 수 있다.
•
대부분의 경우 리시버 객체의 멤버에 접근할 때 this를 생략할 수 있어 코드가 더 간결해진다.
•
그러나 this를 생략하면 리시버의 멤버와 외부 객체 또는 함수 간의 구분이 어려울 수 있다.
•
따라서 리시버 객체가 주로 자신의 멤버에 대해 작업하는 람다에서는 this를 사용하는 것이 권장된다.
it
// 기본 이름인 it 을 사용하는 예시
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
// 인자 이름을 value 로 사용하는 예시
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
Kotlin
복사
•
let 과 also 는 컨텍스트 객체를 람다 인자로 참조한다.
•
인자 이름이 지정되지 않으면 객체는 암시적인 기본 이름인 it 으로 접근된다.
•
it은 this보다 짧고 it을 사용하는 표현은 일반적으로 읽기 쉽다.
•
그러나 객체의 함수나 속성을 호출할 때는 this처럼 암시적으로 객체를 사용할 수 없으므로 it은 주로 함수 호출에서 인자로 객체를 사용할 때 더 적합하다.
•
또한 여러 변수를 사용하는 코드 블록에서도 유리하다.
반환값
•
스코프 함수는 반환 값에서 차이를 보인다.
◦
apply 와 also 는 컨텍스트 객체를 반환한다.
◦
let, run, with 는 람다 결과를 반환한다.
•
어떤 반환 값을 원하는지 신중히 고려해야 하며 이는 코드에서 다음에 수행할 작업에 따라 적절한 스코프 함수를 선택하는데 도움이 된다.
컨텍스트 객체 반환
// 호출 체인으로 사용
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()
// 컨텍스트 객체 자체 반환으로 사용
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
Kotlin
복사
•
apply 와 also 의 반환값은 컨텍스트 객체 자체이다.
•
따라서 이러한 함수는 호출 체인에 포함될 수 있으며 같은 객체에 대해 연속적으로 함수 호출을 이어갈 수 있다.
•
또한 컨텍스트 객체 자체를 반환하는 함수의 반환문에도 사용할 수 있다.
람다 결과 반환
// 결과를 변수에 할당
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.")
// 반환 값을 무시하고 지역 변수를 위한 임시 범위 생성
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
Kotlin
복사
•
let, run, with 는 람다 결과를 반환한다.
•
따라서 결과를 변수에 할당하거나 결과에 대해 연산을 연결할 때 사용할 수 있다.
•
또한 반환 값을 무시하고 스코프 함수를 사용하여 지역 변수를 위한 임시 범위를 생성할 수 있다.
스코프 함수
let
컨텍스트 객체 | 반환값 |
인자로 사용 가능(it) | 람다 결과 |
// let 을 사용하지 않은 예제
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
// let 을 사용하여 변수에 할당하지 않은 예제
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 필요한 경우 추가 함수 호출
}
Kotlin
복사
•
let 은 호출 체인의 결과에서 하나 이상의 함수를 호출하는데 사용할 수 있다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
Kotlin
복사
•
let 을 사용하면 람다에서 인자(it)를 사용한 단일 함수 호출 시 메서드 레퍼런스(::)를 사용할 수 있다.
val str: String? = "Hello"
val length = str?.let {
println("let() called on $it")
processNonNullString(it)
it.length
}
Kotlin
복사
•
let 은 주로 null이 아닌 값을 포함하는 코드 블록을 실행하는데 사용된다.
•
null인 객체에 대해 작업을 수행하려면 안전 호출 연산자(?.)를 사용하고 let으로 람다에서 작업을 수행한다.
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
Kotlin
복사
•
let 을 사용하여 제한된 범위를 가진 로컬 변수를 도입하여 코드를 더 쉽게 읽을 수 있다.
•
컨텍스트 객체에 대한 새 변수를 정의하려면 람다 인자로 이름을 제공하여 기본 it 대신 사용할 수 있다.
with
컨텍스트 객체 | 반환값 |
수신자로 사용 가능(this) | 람다 결과 |
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
Kotlin
복사
•
with 는 컨텍스트 객체의 함수 호출을 위한 것으로 반환 결과가 필요하지 않을 때 사용을 권장한다.
•
코드는 “이 객체와 함께 다음을 수행”으로 읽을 수 있다.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
Kotlin
복사
•
with 를 사용하여 헬퍼 객체의 속성이나 함수로 값을 계산하는데 사용할 수도 있다.
run
컨텍스트 객체 | 반환값 |
수신자로 사용 가능(this) | 람다 결과 |
// run 을 사용한 코드
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// let 을 사용한 코드
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
Kotlin
복사
•
run 은 with 와 동일하지만 확장 함수로 구현된다.
•
따라서 let 처럼 컨텍스트 객체에서 점 표기법을 사용하여 호출할 수 있다.
•
run 은 객체를 초기화하고 반환 값을 계산할 때 유용하다.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
Kotlin
복사
•
run 은 비확장 함수로도 호출할 수 있다.
•
비확장 run 은 컨텍스트 객체가 없지만 여전히 람다 결과를 반환한다.
apply
컨텍스트 객체 | 반환값 |
수신자로 사용 가능(this) | 객체 자체 |
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
Kotlin
복사
•
apply 는 컨텍스트 객체 자체를 반환하므로 주로 값이 반환되지 않고 수신자 객체의 멤버를 주로 조작하는 코드블록에 사용된다.
•
apply 의 가장 일반적인 사용 사례는 객체 구성이다.
also
컨텍스트 객체 | 반환값 |
인자로 사용 가능(it) | 객체 자체 |
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
Kotlin
복사
•
also 는 컨텍스트 객체를 인자로 받는 작업을 수행하는데 유용하다.
•
객체의 속성 및 함수보다 객체의 대한 참조가 필요한 작업에 사용하거나 외부 스코프의 this 참조를 가리고 싶지 않을 때 사용한다.
•
코드에서 also 는 “그리고 다음 작업을 수행하라” 고 읽을 수 있다.
takeIf 와 takeUnless
•
스코프 함수 외에도 표준 라이브러리에는 takeIf 와 takeUnless 함수가 포함되어 있다.
•
이러한 함수는 객체 상태의 검사를 호출 체인에 포함할 수 있게 해준다.
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
Kotlin
복사
•
takeIf
◦
객체가 주어진 조건을 만족하면 이 객체를 반환하고 그렇지 않으면 null을 반환한다.
◦
즉, takeIf는 단일 객체에 대한 필터링 함수이다.
•
takeUnless
◦
객체가 주어진 조건을 만족하면 null을 반환하고 그렇지 않으면 객체를 반환한다.
•
takeIf 와 takeUnless 를 사용할 때 객체는 람다 인자(it)로 사용 가능하다.
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
println(caps)
Kotlin
복사
•
takeIf 와 takeUnless 후에 다른 함수를 체인할 때 반환 값이 nullable 이므로 null 체크를 수행하거나 안전 호출 연산자(?.)를 사용해야 한다.
// 스코프 함수 없이 작성한 예제
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
// 스코프 함수로 작성한 예제
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.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
Kotlin
복사
•
takeIf 와 takeUnless 는 특히 스코프 함수와 조합하여 사용하는 것이 유용하다.
•
예를 들어, 객체가 주어진 조건을 만족할 때 코드 블록을 실행하기 위해 takeIf 를 호출한 후 안전 호출 연산자(?.)와 함께 let 을 사용할 수 있다.